Merge pull request #252 from sass/quiet

Add a --quiet flag and a Logger class
This commit is contained in:
Natalie Weizenbaum 2018-03-11 21:27:11 -07:00 committed by GitHub
commit a5841c99d0
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
25 changed files with 465 additions and 163 deletions

View File

@ -6,6 +6,16 @@
* Add a `--load-path` command-line option (alias `-I`) for passing additional
paths to search for Sass files to import.
* Add a `--quiet` command-line option (alias `-q`) for silencing warnings.
### Dart API
* Add a `Logger` class that allows users to control how messages are printed by
stylesheets.
* Add a `logger` parameter to `compile()`, `compileAsync()`, `compileString()`,
and `compileStringAsync()`.
### Node JS API
* Import URLs passed to importers are no longer normalized. For example, if a

View File

@ -8,18 +8,24 @@ import 'src/callable.dart';
import 'src/compile.dart' as c;
import 'src/exception.dart';
import 'src/importer.dart';
import 'src/logger.dart';
import 'src/sync_package_resolver.dart';
import 'src/visitor/serialize.dart';
export 'src/callable.dart' show Callable, AsyncCallable;
export 'src/importer.dart';
export 'src/logger.dart';
export 'src/value.dart' show ListSeparator;
export 'src/value/external/value.dart';
export 'src/visitor/serialize.dart' show OutputStyle;
/// Loads the Sass file at [path], compiles it to CSS, and returns the result.
///
/// If [color] is `true`, this will use terminal colors in warnings.
/// If [color] is `true`, this will use terminal colors in warnings. It's
/// ignored if [logger] is passed.
///
/// If [logger] is passed, it's used to emit all messages that are generated by
/// Sass code. Users may pass custom subclasses of [Logger].
///
/// Imports are resolved by trying, in order:
///
@ -45,13 +51,14 @@ export 'src/visitor/serialize.dart' show OutputStyle;
/// Throws a [SassException] if conversion fails.
String compile(String path,
{bool color: false,
Logger logger,
Iterable<Importer> importers,
Iterable<String> loadPaths,
SyncPackageResolver packageResolver,
Iterable<Callable> functions,
OutputStyle style}) {
var result = c.compile(path,
color: color,
logger: logger ?? new Logger.stderr(color: color),
importers: importers,
loadPaths: loadPaths,
packageResolver: packageResolver,
@ -63,8 +70,13 @@ String compile(String path,
/// Compiles [source] to CSS and returns the result.
///
/// If [indented] is `true`, this parses [source] using indented syntax;
/// otherwise (and by default) it uses SCSS. If [color] is `true`, this will use
/// terminal colors in warnings.
/// otherwise (and by default) it uses SCSS.
///
/// If [color] is `true`, this will use terminal colors in warnings. It's
/// ignored if [logger] is passed.
///
/// If [logger] is passed, it's used to emit all messages that are generated by
/// Sass code. Users may pass custom subclasses of [Logger].
///
/// Imports are resolved by trying, in order:
///
@ -95,6 +107,7 @@ String compile(String path,
String compileString(String source,
{bool indented: false,
bool color: false,
Logger logger,
Iterable<Importer> importers,
SyncPackageResolver packageResolver,
Iterable<String> loadPaths,
@ -104,7 +117,7 @@ String compileString(String source,
url}) {
var result = c.compileString(source,
indented: indented,
color: color,
logger: logger ?? new Logger.stderr(color: color),
importers: importers,
packageResolver: packageResolver,
loadPaths: loadPaths,
@ -122,13 +135,14 @@ String compileString(String source,
/// slower, so [compile] should be preferred if possible.
Future<String> compileAsync(String path,
{bool color: false,
Logger logger,
Iterable<AsyncImporter> importers,
SyncPackageResolver packageResolver,
Iterable<String> loadPaths,
Iterable<AsyncCallable> functions,
OutputStyle style}) async {
var result = await c.compileAsync(path,
color: color,
logger: logger ?? new Logger.stderr(color: color),
importers: importers,
loadPaths: loadPaths,
packageResolver: packageResolver,
@ -145,6 +159,7 @@ Future<String> compileAsync(String path,
Future<String> compileStringAsync(String source,
{bool indented: false,
bool color: false,
Logger logger,
Iterable<AsyncImporter> importers,
SyncPackageResolver packageResolver,
Iterable<String> loadPaths,
@ -154,7 +169,7 @@ Future<String> compileStringAsync(String source,
url}) async {
var result = await c.compileStringAsync(source,
indented: indented,
color: color,
logger: logger ?? new Logger.stderr(color: color),
importers: importers,
packageResolver: packageResolver,
loadPaths: loadPaths,

View File

@ -2,6 +2,7 @@
// MIT-style license that can be found in the LICENSE file or at
// https://opensource.org/licenses/MIT.
import '../../logger.dart';
import '../../parse/media_query.dart';
import '../../utils.dart';
@ -28,8 +29,8 @@ class CssMediaQuery {
/// If passed, [url] is the name of the file from which [contents] comes.
///
/// Throws a [SassFormatException] if parsing fails.
static List<CssMediaQuery> parseList(String contents, {url}) =>
new MediaQueryParser(contents, url: url).parse();
static List<CssMediaQuery> parseList(String contents, {url, Logger logger}) =>
new MediaQueryParser(contents, url: url, logger: logger).parse();
/// Creates a media query specifies a type and, optionally, features.
CssMediaQuery(this.type, {this.modifier, Iterable<String> features})

View File

@ -5,6 +5,7 @@
import 'package:source_span/source_span.dart';
import '../../exception.dart';
import '../../logger.dart';
import '../../parse/scss.dart';
import '../../utils.dart';
import 'argument.dart';
@ -36,8 +37,9 @@ class ArgumentDeclaration implements SassNode {
/// If passed, [url] is the name of the file from which [contents] comes.
///
/// Throws a [SassFormatException] if parsing fails.
factory ArgumentDeclaration.parse(String contents, {url}) =>
new ScssParser("($contents)", url: url).parseArgumentDeclaration();
factory ArgumentDeclaration.parse(String contents, {url, Logger logger}) =>
new ScssParser("($contents)", url: url, logger: logger)
.parseArgumentDeclaration();
/// Throws a [SassScriptException] if [positional] and [names] aren't valid
/// for this argument declaration.

View File

@ -4,6 +4,7 @@
import 'package:collection/collection.dart';
import '../../logger.dart';
import '../../parse/at_root_query.dart';
import '../css.dart';
@ -54,8 +55,8 @@ class AtRootQuery {
/// If passed, [url] is the name of the file from which [contents] comes.
///
/// Throws a [SassFormatException] if parsing fails.
factory AtRootQuery.parse(String contents, {url}) =>
new AtRootQueryParser(contents, url: url).parse();
factory AtRootQuery.parse(String contents, {url, Logger logger}) =>
new AtRootQueryParser(contents, url: url, logger: logger).parse();
/// Returns whether [this] excludes [node].
bool excludes(CssParentNode node) {

View File

@ -5,6 +5,7 @@
import 'package:source_span/source_span.dart';
import '../../../visitor/interface/statement.dart';
import '../../../logger.dart';
import '../../../parse/sass.dart';
import '../../../parse/scss.dart';
import '../statement.dart';
@ -21,21 +22,19 @@ class Stylesheet extends ParentStatement {
/// Parses an indented-syntax stylesheet from [contents].
///
/// If passed, [url] is the name of the file from which [contents] comes. If
/// [color] is `true`, this will use terminal colors in warnings.
/// If passed, [url] is the name of the file from which [contents] comes.
///
/// Throws a [SassFormatException] if parsing fails.
factory Stylesheet.parseSass(String contents, {url, bool color: false}) =>
new SassParser(contents, url: url, color: color).parse();
factory Stylesheet.parseSass(String contents, {url, Logger logger}) =>
new SassParser(contents, url: url, logger: logger).parse();
/// Parses an SCSS stylesheet from [contents].
///
/// If passed, [url] is the name of the file from which [contents] comes. If
/// [color] is `true`, this will use terminal colors in warnings.
/// If passed, [url] is the name of the file from which [contents] comes.
///
/// Throws a [SassFormatException] if parsing fails.
factory Stylesheet.parseScss(String contents, {url, bool color: false}) =>
new ScssParser(contents, url: url, color: color).parse();
factory Stylesheet.parseScss(String contents, {url, Logger logger}) =>
new ScssParser(contents, url: url, logger: logger).parse();
T accept<T>(StatementVisitor<T> visitor) => visitor.visitStylesheet(this);

View File

@ -3,6 +3,7 @@
// https://opensource.org/licenses/MIT.
import '../../extend/functions.dart';
import '../../logger.dart';
import '../../parse/selector.dart';
import '../../utils.dart';
import '../../visitor/interface/selector.dart';
@ -57,8 +58,9 @@ class CompoundSelector extends Selector implements ComplexSelectorComponent {
///
/// Throws a [SassFormatException] if parsing fails.
factory CompoundSelector.parse(String contents,
{url, bool allowParent: true}) =>
new SelectorParser(contents, url: url, allowParent: allowParent)
{url, Logger logger, bool allowParent: true}) =>
new SelectorParser(contents,
url: url, logger: logger, allowParent: allowParent)
.parseCompoundSelector();
T accept<T>(SelectorVisitor<T> visitor) =>

View File

@ -3,6 +3,7 @@
// https://opensource.org/licenses/MIT.
import '../../extend/functions.dart';
import '../../logger.dart';
import '../../parse/selector.dart';
import '../../utils.dart';
import '../../exception.dart';
@ -52,8 +53,11 @@ class SelectorList extends Selector {
/// selector.
///
/// Throws a [SassFormatException] if parsing fails.
factory SelectorList.parse(String contents, {url, bool allowParent: true}) =>
new SelectorParser(contents, url: url, allowParent: allowParent).parse();
factory SelectorList.parse(String contents,
{url, Logger logger, bool allowParent: true}) =>
new SelectorParser(contents,
url: url, logger: logger, allowParent: allowParent)
.parse();
T accept<T>(SelectorVisitor<T> visitor) => visitor.visitSelectorList(this);

View File

@ -3,6 +3,7 @@
// https://opensource.org/licenses/MIT.
import '../../exception.dart';
import '../../logger.dart';
import '../../parse/selector.dart';
import '../selector.dart';
@ -34,8 +35,9 @@ abstract class SimpleSelector extends Selector {
///
/// Throws a [SassFormatException] if parsing fails.
factory SimpleSelector.parse(String contents,
{url, bool allowParent: true}) =>
new SelectorParser(contents, url: url, allowParent: allowParent)
{url, Logger logger, bool allowParent: true}) =>
new SelectorParser(contents,
url: url, logger: logger, allowParent: allowParent)
.parseSimpleSelector();
/// Returns a new [SimpleSelector] based on [this], as though it had been

View File

@ -9,6 +9,7 @@ import 'callable.dart';
import 'importer.dart';
import 'importer/node.dart';
import 'io.dart';
import 'logger.dart';
import 'sync_package_resolver.dart';
import 'util/path.dart';
import 'visitor/async_evaluate.dart';
@ -19,7 +20,7 @@ import 'visitor/serialize.dart';
/// node-sass compatible API.
CompileResult compile(String path,
{bool indented,
bool color: false,
Logger logger,
Iterable<Importer> importers,
NodeImporter nodeImporter,
SyncPackageResolver packageResolver,
@ -31,7 +32,7 @@ CompileResult compile(String path,
LineFeed lineFeed}) =>
compileString(readFile(path),
indented: indented ?? p.extension(path) == '.sass',
color: color,
logger: logger,
functions: functions,
importers: importers,
nodeImporter: nodeImporter,
@ -48,7 +49,7 @@ CompileResult compile(String path,
/// the node-sass compatible API.
CompileResult compileString(String source,
{bool indented: false,
bool color: false,
Logger logger,
Iterable<Importer> importers,
NodeImporter nodeImporter,
SyncPackageResolver packageResolver,
@ -61,8 +62,8 @@ CompileResult compileString(String source,
LineFeed lineFeed,
url}) {
var sassTree = indented
? new Stylesheet.parseSass(source, url: url, color: color)
: new Stylesheet.parseScss(source, url: url, color: color);
? new Stylesheet.parseSass(source, url: url, logger: logger)
: new Stylesheet.parseScss(source, url: url, logger: logger);
var evaluateResult = evaluate(sassTree,
importers: (importers?.toList() ?? [])
@ -70,7 +71,7 @@ CompileResult compileString(String source,
nodeImporter: nodeImporter,
importer: importer,
functions: functions,
color: color);
logger: logger);
var css = serialize(evaluateResult.stylesheet,
style: style,
useSpaces: useSpaces,
@ -84,7 +85,7 @@ CompileResult compileString(String source,
/// the node-sass compatible API.
Future<CompileResult> compileAsync(String path,
{bool indented,
bool color: false,
Logger logger,
Iterable<AsyncImporter> importers,
NodeImporter nodeImporter,
SyncPackageResolver packageResolver,
@ -96,7 +97,7 @@ Future<CompileResult> compileAsync(String path,
LineFeed lineFeed}) =>
compileStringAsync(readFile(path),
indented: indented ?? p.extension(path) == '.sass',
color: color,
logger: logger,
importers: importers,
nodeImporter: nodeImporter,
packageResolver: packageResolver,
@ -113,7 +114,7 @@ Future<CompileResult> compileAsync(String path,
/// support the node-sass compatible API.
Future<CompileResult> compileStringAsync(String source,
{bool indented: false,
bool color: false,
Logger logger,
Iterable<AsyncImporter> importers,
NodeImporter nodeImporter,
SyncPackageResolver packageResolver,
@ -126,8 +127,8 @@ Future<CompileResult> compileStringAsync(String source,
LineFeed lineFeed,
url}) async {
var sassTree = indented
? new Stylesheet.parseSass(source, url: url, color: color)
: new Stylesheet.parseScss(source, url: url, color: color);
? new Stylesheet.parseSass(source, url: url, logger: logger)
: new Stylesheet.parseScss(source, url: url, logger: logger);
var evaluateResult = await evaluateAsync(sassTree,
importers: (importers?.toList() ?? [])
@ -135,7 +136,7 @@ Future<CompileResult> compileStringAsync(String source,
nodeImporter: nodeImporter,
importer: importer,
functions: functions,
color: color);
logger: logger);
var css = serialize(evaluateResult.stylesheet,
style: style,
useSpaces: useSpaces,

View File

@ -31,6 +31,7 @@ main(List<String> args) async {
allowed: ['expanded', 'compressed'],
defaultsTo: 'expanded')
..addFlag('color', abbr: 'c', help: 'Whether to emit terminal colors.')
..addFlag('quiet', abbr: 'q', help: "Don't print warnings.")
..addFlag('trace', help: 'Print full Dart stack traces for exceptions.')
..addFlag('help',
abbr: 'h', help: 'Print this usage information.', negatable: false)
@ -67,6 +68,8 @@ main(List<String> args) async {
var color =
options.wasParsed('color') ? options['color'] as bool : hasTerminal;
var logger =
options['quiet'] as bool ? Logger.quiet : new Logger.stderr(color: color);
var style = options['style'] == 'compressed'
? OutputStyle.compressed
: OutputStyle.expanded;
@ -76,17 +79,24 @@ main(List<String> args) async {
String css;
if (stdinFlag) {
css = await _compileStdin(
style: style, loadPaths: loadPaths, asynchronous: asynchronous);
logger: logger,
style: style,
loadPaths: loadPaths,
asynchronous: asynchronous);
} else {
var input = options.rest.first;
if (input == '-') {
css = await _compileStdin(
style: style, loadPaths: loadPaths, asynchronous: asynchronous);
logger: logger,
style: style,
loadPaths: loadPaths,
asynchronous: asynchronous);
} else if (asynchronous) {
css = await compileAsync(input,
color: color, style: style, loadPaths: loadPaths);
logger: logger, style: style, loadPaths: loadPaths);
} else {
css = compile(input, color: color, style: style, loadPaths: loadPaths);
css =
compile(input, logger: logger, style: style, loadPaths: loadPaths);
}
}
@ -150,7 +160,7 @@ Future<String> _loadVersion() async {
/// Compiles Sass from standard input and returns the result.
Future<String> _compileStdin(
{bool color: false,
{Logger logger,
OutputStyle style,
List<String> loadPaths,
bool asynchronous: false}) async {
@ -158,10 +168,10 @@ Future<String> _compileStdin(
var importer = new FilesystemImporter('.');
if (asynchronous) {
return await compileStringAsync(text,
color: color, style: style, importer: importer, loadPaths: loadPaths);
logger: logger, style: style, importer: importer, loadPaths: loadPaths);
} else {
return compileString(text,
color: color, style: style, importer: importer, loadPaths: loadPaths);
logger: logger, style: style, importer: importer, loadPaths: loadPaths);
}
}

83
lib/src/logger.dart Normal file
View File

@ -0,0 +1,83 @@
// Copyright 2017 Google Inc. Use of this source code is governed by an
// MIT-style license that can be found in the LICENSE file or at
// https://opensource.org/licenses/MIT.
import 'package:source_span/source_span.dart';
import 'package:stack_trace/stack_trace.dart';
import 'io.dart';
import 'util/path.dart';
import 'utils.dart';
/// An interface for loggers that print messages produced by Sass stylesheets.
///
/// This may be implemented by user code.
abstract class Logger {
/// A logger that silently ignores all messages.
static final Logger quiet = new _QuietLogger();
/// Creates a logger that prints warnings to standard error, with terminal
/// colors if [color] is `true` (default `false`).
const factory Logger.stderr({bool color}) = _StderrLogger;
/// Emits a warning with the given [message].
///
/// If [span] is passed, it's the location in the Sass source that generated
/// the warning. If [trace] is passed, it's the Sass stack trace when the
/// warning was issued. If [deprecation] is `true`, it indicates that this is
/// a deprecation warning. Implementations should surface all this information
/// to the end user.
void warn(String message,
{FileSpan span, Trace trace, bool deprecation: false});
/// Emits a debugging message associated with the given [span].
void debug(String message, SourceSpan span);
}
/// A logger that emits no messages.
class _QuietLogger implements Logger {
void warn(String message,
{FileSpan span, Trace trace, bool deprecation: false}) {}
void debug(String message, SourceSpan span) {}
}
/// A logger that prints warnings to standard error.
class _StderrLogger implements Logger {
/// Whether to use terminal colors in messages.
final bool color;
const _StderrLogger({this.color: false});
void warn(String message,
{FileSpan span, Trace trace, bool deprecation: false}) {
if (color) {
// Bold yellow.
stderr.write('\u001b[33m\u001b[1m');
if (deprecation) stderr.write('Deprecation ');
stderr.write('Warning\u001b[0m');
} else {
if (deprecation) stderr.write('DEPRECATION ');
stderr.write('WARNING');
}
if (span == null) {
stderr.writeln(': $message');
} else if (trace != null) {
// If there's a span and a trace, the span's location information is
// probably duplicated in the trace, so we just use it for highlighting.
stderr.writeln(': $message\n\n${span.highlight(color: color)}');
} else {
stderr.writeln(' on ${span.message("\n" + message, color: color)}');
}
if (trace != null) stderr.writeln(indent(trace.toString().trimRight(), 4));
stderr.writeln();
}
void debug(String message, SourceSpan span) {
stderr
.write('${p.prettyUri(span.start.sourceUrl)}:${span.start.line + 1} ');
stderr.write(color ? '\u001b[1mDebug\u001b[0m' : 'DEBUG');
stderr.writeln(': $message');
}
}

View File

@ -5,11 +5,13 @@
import 'package:charcode/charcode.dart';
import '../ast/sass.dart';
import '../logger.dart';
import 'parser.dart';
/// A parser for `@at-root` queries.
class AtRootQueryParser extends Parser {
AtRootQueryParser(String contents, {url}) : super(contents, url: url);
AtRootQueryParser(String contents, {url, Logger logger})
: super(contents, url: url, logger: logger);
AtRootQuery parse() {
return wrapSpanFormatException(() {

View File

@ -4,12 +4,14 @@
import 'package:charcode/charcode.dart';
import '../logger.dart';
import '../util/character.dart';
import 'parser.dart';
/// A parser for `@keyframes` block selectors.
class KeyframeSelectorParser extends Parser {
KeyframeSelectorParser(String contents, {url}) : super(contents, url: url);
KeyframeSelectorParser(String contents, {url, Logger logger})
: super(contents, url: url, logger: logger);
List<String> parse() {
return wrapSpanFormatException(() {

View File

@ -5,12 +5,14 @@
import 'package:charcode/charcode.dart';
import '../ast/css.dart';
import '../logger.dart';
import '../utils.dart';
import 'parser.dart';
/// A parser for `@media` queries.
class MediaQueryParser extends Parser {
MediaQueryParser(String contents, {url}) : super(contents, url: url);
MediaQueryParser(String contents, {url, Logger logger})
: super(contents, url: url, logger: logger);
List<CssMediaQuery> parse() {
return wrapSpanFormatException(() {

View File

@ -8,6 +8,7 @@ import 'package:source_span/source_span.dart';
import 'package:string_scanner/string_scanner.dart';
import '../exception.dart';
import '../logger.dart';
import '../util/character.dart';
/// The abstract base class for all parsers.
@ -19,8 +20,12 @@ abstract class Parser {
/// The scanner that scans through the text being parsed.
final SpanScanner scanner;
Parser(String contents, {url})
: scanner = new SpanScanner(contents, sourceUrl: url);
/// The logger to use when emitting warnings.
final Logger _logger;
Parser(String contents, {url, Logger logger})
: scanner = new SpanScanner(contents, sourceUrl: url),
_logger = logger ?? const Logger.stderr();
// ## Tokens
@ -560,6 +565,10 @@ abstract class Parser {
return scanner.substring(start);
}
/// Prints a warning to standard error, associated with [span].
@protected
void warn(String message, FileSpan span) => _logger.warn(message, span: span);
/// Prints a source span highlight of the current location being scanned.
///
/// If [message] is passed, prints that as well. This is intended for use when

View File

@ -7,6 +7,7 @@ import 'package:string_scanner/string_scanner.dart';
import '../ast/sass.dart';
import '../interpolation_buffer.dart';
import '../logger.dart';
import '../util/character.dart';
import 'stylesheet.dart';
@ -36,8 +37,8 @@ class SassParser extends StylesheetParser {
bool get indented => true;
SassParser(String contents, {url, bool color: false})
: super(contents, url: url, color: color);
SassParser(String contents, {url, Logger logger})
: super(contents, url: url, logger: logger);
Interpolation styleRuleSelector() {
var start = scanner.state;

View File

@ -6,6 +6,7 @@ import 'package:charcode/charcode.dart';
import '../ast/sass.dart';
import '../interpolation_buffer.dart';
import '../logger.dart';
import '../util/character.dart';
import 'stylesheet.dart';
@ -14,8 +15,8 @@ class ScssParser extends StylesheetParser {
bool get indented => false;
int get currentIndentation => null;
ScssParser(String contents, {url, bool color: false})
: super(contents, url: url, color: color);
ScssParser(String contents, {url, Logger logger})
: super(contents, url: url, logger: logger);
Interpolation styleRuleSelector() => almostAnyValue();

View File

@ -5,6 +5,7 @@
import 'package:charcode/charcode.dart';
import '../ast/selector.dart';
import '../logger.dart';
import '../util/character.dart';
import '../utils.dart';
import 'parser.dart';
@ -21,9 +22,9 @@ class SelectorParser extends Parser {
/// Whether this parser allows the parent selector `&`.
final bool _allowParent;
SelectorParser(String contents, {url, bool allowParent: true})
SelectorParser(String contents, {url, Logger logger, bool allowParent: true})
: _allowParent = allowParent,
super(contents, url: url);
super(contents, url: url, logger: logger);
SelectorList parse() {
return wrapSpanFormatException(() {

View File

@ -14,6 +14,7 @@ import '../ast/sass.dart';
import '../exception.dart';
import '../color_names.dart';
import '../interpolation_buffer.dart';
import '../logger.dart';
import '../util/character.dart';
import '../utils.dart';
import '../value.dart';
@ -57,12 +58,8 @@ abstract class StylesheetParser extends Parser {
/// Whether the parser is currently within a parenthesized expression.
var _inParentheses = false;
/// Whether warnings should be emitted using terminal colors.
@protected
final bool color;
StylesheetParser(String contents, {url, this.color: false})
: super(contents, url: url);
StylesheetParser(String contents, {url, Logger logger})
: super(contents, url: url, logger: logger);
// ## Statements
@ -227,8 +224,7 @@ abstract class StylesheetParser extends Parser {
var children = this.children(_statement);
if (indented && children.isEmpty) {
warn("This selector doesn't have any properties and won't be rendered.",
selectorSpan,
color: color);
selectorSpan);
}
_inStyleRule = wasInStyleRule;
@ -1964,8 +1960,7 @@ abstract class StylesheetParser extends Parser {
warn(
'In Sass, "&&" means two copies of the parent selector. You '
'probably want to use "and" instead.',
scanner.spanFrom(start),
color: color);
scanner.spanFrom(start));
scanner.position--;
}

View File

@ -12,7 +12,6 @@ import 'package:source_span/source_span.dart';
import 'package:stack_trace/stack_trace.dart';
import 'ast/node.dart';
import 'io.dart';
import 'util/character.dart';
/// The URL used in stack traces when no source URL is available.
@ -25,6 +24,10 @@ String toSentence(Iterable iter, [String conjunction]) {
return iter.take(iter.length - 1).join(", ") + " $conjunction ${iter.last}";
}
/// Returns [string] with every line indented [indentation] spaces.
String indent(String string, int indentation) =>
string.split("\n").map((line) => (" " * indentation) + line).join("\n");
/// Returns [name] if [number] is 1, or the plural of [name] otherwise.
///
/// By default, this just adds "s" to the end of [name] to get the plural. If
@ -308,11 +311,3 @@ Future<Map<String, V2>> normalizedMapMapAsync<K, V1, V2>(Map<K, V1> map,
}
return result;
}
/// Prints a warning to standard error, associated with [span].
///
/// If [color] is `true`, this uses terminal colors.
void warn(String message, FileSpan span, {bool color: false}) {
var warning = color ? '\u001b[33m\u001b[1mWarning\u001b[0m' : 'WARNING';
stderr.writeln("$warning on ${span.message("\n$message", color: color)}\n");
}

View File

@ -21,7 +21,7 @@ import '../exception.dart';
import '../extend/extender.dart';
import '../importer.dart';
import '../importer/node.dart';
import '../io.dart';
import '../logger.dart';
import '../parse/keyframe_selector.dart';
import '../utils.dart';
import '../util/path.dart';
@ -40,24 +40,25 @@ typedef Future _ScopeCallback(Future callback());
/// If [environment] is passed, it's used as the lexical environment when
/// evaluating [stylesheet]. It should only contain global definitions.
///
/// If [color] is `true`, this will use terminal colors in warnings.
///
/// If [importer] is passed, it's used to resolve relative imports in
/// [stylesheet] relative to `stylesheet.span.sourceUrl`.
///
/// Warnings are emitted using [logger], or printed to standard error by
/// default.
///
/// Throws a [SassRuntimeException] if evaluation fails.
Future<EvaluateResult> evaluateAsync(Stylesheet stylesheet,
{Iterable<AsyncImporter> importers,
NodeImporter nodeImporter,
AsyncImporter importer,
Iterable<AsyncCallable> functions,
bool color: false}) =>
Logger logger}) =>
new _EvaluateVisitor(
importers: importers,
nodeImporter: nodeImporter,
importer: importer,
functions: functions,
color: color)
logger: logger)
.run(stylesheet);
/// A visitor that executes Sass code to produce a CSS tree.
@ -72,8 +73,8 @@ class _EvaluateVisitor
/// compiled to Node.js.
final NodeImporter _nodeImporter;
/// Whether to use terminal colors in warnings.
final bool _color;
/// The logger to use to print warnings.
final Logger _logger;
/// The current lexical environment.
var _environment = new AsyncEnvironment();
@ -163,11 +164,11 @@ class _EvaluateVisitor
NodeImporter nodeImporter,
AsyncImporter importer,
Iterable<AsyncCallable> functions,
bool color: false})
Logger logger})
: _importers = importers == null ? const [] : importers.toList(),
_importer = importer ?? Importer.noOp,
_nodeImporter = nodeImporter,
_color = color {
_logger = logger {
_environment.setFunction(
new BuiltInCallable("global-variable-exists", r"$name", (arguments) {
var variable = arguments[0].assertString("name");
@ -231,12 +232,11 @@ class _EvaluateVisitor
_callableSpan));
if (function is SassString) {
warn(
"DEPRECATION WARNING: Passing a string to call() is deprecated and "
"will be illegal\n"
_warn(
"Passing a string to call() is deprecated and will be illegal\n"
"in Sass 4.0. Use call(get-function($function)) instead.",
_callableSpan,
color: _color);
deprecation: true);
var expression = new FunctionExpression(
new Interpolation([function.text], _callableSpan), invocation);
@ -306,8 +306,8 @@ class _EvaluateVisitor
if (node.query != null) {
var resolved =
await _performInterpolation(node.query, warnForColor: true);
query = _adjustParseError(
node.query.span, () => new AtRootQuery.parse(resolved));
query = _adjustParseError(node.query.span,
() => new AtRootQuery.parse(resolved, logger: _logger));
}
var parent = _parent;
@ -452,10 +452,9 @@ class _EvaluateVisitor
}
Future<Value> visitDebugRule(DebugRule node) async {
var start = node.span.start;
var value = await node.expression.accept(this);
stderr.writeln("${p.prettyUri(start.sourceUrl)}:${start.line + 1} DEBUG: "
"${value is SassString ? value.text : value}");
_logger.debug(
value is SassString ? value.text : value.toString(), node.span);
return null;
}
@ -541,7 +540,7 @@ class _EvaluateVisitor
var target = _adjustParseError(
targetText.span,
() => new SimpleSelector.parse(targetText.value.trim(),
allowParent: false));
logger: _logger, allowParent: false));
_extender.addExtension(_styleRule.selector, target, node, _mediaQueries);
return null;
}
@ -753,8 +752,8 @@ class _EvaluateVisitor
}
return url.startsWith('file') && pUrl.extension(url) == '.sass'
? new Stylesheet.parseSass(contents, url: url, color: _color)
: new Stylesheet.parseScss(contents, url: url, color: _color);
? new Stylesheet.parseSass(contents, url: url, logger: _logger)
: new Stylesheet.parseScss(contents, url: url, logger: _logger);
}
/// Parses the contents of [result] into a [Stylesheet].
@ -773,9 +772,9 @@ class _EvaluateVisitor
var displayUrl = url.resolve(p.basename(canonicalUrl.path));
return result.isIndented
? new Stylesheet.parseSass(result.contents,
url: displayUrl, color: _color)
url: displayUrl, logger: _logger)
: new Stylesheet.parseScss(result.contents,
url: displayUrl, color: _color);
url: displayUrl, logger: _logger);
});
}
@ -898,8 +897,8 @@ class _EvaluateVisitor
await _performInterpolation(interpolation, warnForColor: true);
// TODO(nweiz): Remove this type argument when sdk#31398 is fixed.
return _adjustParseError<List<CssMediaQuery>>(
interpolation.span, () => CssMediaQuery.parseList(resolved));
return _adjustParseError<List<CssMediaQuery>>(interpolation.span,
() => CssMediaQuery.parseList(resolved, logger: _logger));
}
/// Returns a list of queries that selects for platforms that match both
@ -925,8 +924,10 @@ class _EvaluateVisitor
var selectorText = await _interpolationToValue(node.selector,
trim: true, warnForColor: true);
if (_inKeyframes) {
var parsedSelector = _adjustParseError(node.selector.span,
() => new KeyframeSelectorParser(selectorText.value).parse());
var parsedSelector = _adjustParseError(
node.selector.span,
() => new KeyframeSelectorParser(selectorText.value, logger: _logger)
.parse());
var rule = new CssKeyframeBlock(
new CssValue(
new List.unmodifiable(parsedSelector), node.selector.span),
@ -941,8 +942,8 @@ class _EvaluateVisitor
return null;
}
var parsedSelector = _adjustParseError(
node.selector.span, () => new SelectorList.parse(selectorText.value));
var parsedSelector = _adjustParseError(node.selector.span,
() => new SelectorList.parse(selectorText.value, logger: _logger));
parsedSelector = _addExceptionSpan(
node.selector.span,
() => parsedSelector.resolveParentSelectors(
@ -1055,18 +1056,13 @@ class _EvaluateVisitor
}
Future<Value> visitWarnRule(WarnRule node) async {
await _addExceptionSpanAsync(node.span, () async {
var value = await node.expression.accept(this);
var string = value is SassString
? value.text
: _serialize(value, node.expression.span);
stderr.writeln("WARNING: $string");
});
for (var line in _stackTrace(node.span).toString().split("\n")) {
stderr.writeln(" $line");
}
var value = await _addExceptionSpanAsync(
node.span, () => node.expression.accept(this));
_logger.warn(
value is SassString
? value.text
: _serialize(value, node.expression.span),
trace: _stackTrace(node.span));
return null;
}
@ -1614,14 +1610,14 @@ class _EvaluateVisitor
BinaryOperator.plus,
new StringExpression(new Interpolation([""], null), quotes: true),
expression);
warn(
_warn(
"You probably don't mean to use the color value "
"${namesByColor[result]} in interpolation here.\n"
"It may end up represented as $result, which will likely produce "
"invalid CSS.\n"
"Always quote color names when using them as strings or map keys "
'(for example, "${namesByColor[result]}").\n'
"If you really want to use the color value here, use '$alternative'.\n",
"If you really want to use the color value here, use '$alternative'.",
expression.span);
}
@ -1729,6 +1725,11 @@ class _EvaluateVisitor
return new Trace(frames.reversed);
}
/// Emits a warning with the given [message] about the given [span].
void _warn(String message, FileSpan span, {bool deprecation: false}) =>
_logger.warn(message,
span: span, trace: _stackTrace(span), deprecation: deprecation);
/// Throws a [SassRuntimeException] with the given [message] and [span].
SassRuntimeException _exception(String message, FileSpan span) =>
new SassRuntimeException(message, span, _stackTrace(span));

View File

@ -5,7 +5,7 @@
// DO NOT EDIT. This file was generated from async_evaluate.dart.
// See tool/synchronize.dart for details.
//
// Checksum: 53f8eb4f4e6ed2cad3fe359490a50e12911a278a
// Checksum: 441fccd1274e61316ec7403a381761fff3052706
import 'dart:math' as math;
@ -25,7 +25,7 @@ import '../exception.dart';
import '../extend/extender.dart';
import '../importer.dart';
import '../importer/node.dart';
import '../io.dart';
import '../logger.dart';
import '../parse/keyframe_selector.dart';
import '../utils.dart';
import '../util/path.dart';
@ -44,24 +44,25 @@ typedef void _ScopeCallback(void callback());
/// If [environment] is passed, it's used as the lexical environment when
/// evaluating [stylesheet]. It should only contain global definitions.
///
/// If [color] is `true`, this will use terminal colors in warnings.
///
/// If [importer] is passed, it's used to resolve relative imports in
/// [stylesheet] relative to `stylesheet.span.sourceUrl`.
///
/// Warnings are emitted using [logger], or printed to standard error by
/// default.
///
/// Throws a [SassRuntimeException] if evaluation fails.
EvaluateResult evaluate(Stylesheet stylesheet,
{Iterable<Importer> importers,
NodeImporter nodeImporter,
Importer importer,
Iterable<Callable> functions,
bool color: false}) =>
Logger logger}) =>
new _EvaluateVisitor(
importers: importers,
nodeImporter: nodeImporter,
importer: importer,
functions: functions,
color: color)
logger: logger)
.run(stylesheet);
/// A visitor that executes Sass code to produce a CSS tree.
@ -74,8 +75,8 @@ class _EvaluateVisitor
/// compiled to Node.js.
final NodeImporter _nodeImporter;
/// Whether to use terminal colors in warnings.
final bool _color;
/// The logger to use to print warnings.
final Logger _logger;
/// The current lexical environment.
var _environment = new Environment();
@ -165,11 +166,11 @@ class _EvaluateVisitor
NodeImporter nodeImporter,
Importer importer,
Iterable<Callable> functions,
bool color: false})
Logger logger})
: _importers = importers == null ? const [] : importers.toList(),
_importer = importer ?? Importer.noOp,
_nodeImporter = nodeImporter,
_color = color {
_logger = logger {
_environment.setFunction(
new BuiltInCallable("global-variable-exists", r"$name", (arguments) {
var variable = arguments[0].assertString("name");
@ -233,12 +234,11 @@ class _EvaluateVisitor
_callableSpan));
if (function is SassString) {
warn(
"DEPRECATION WARNING: Passing a string to call() is deprecated and "
"will be illegal\n"
_warn(
"Passing a string to call() is deprecated and will be illegal\n"
"in Sass 4.0. Use call(get-function($function)) instead.",
_callableSpan,
color: _color);
deprecation: true);
var expression = new FunctionExpression(
new Interpolation([function.text], _callableSpan), invocation);
@ -307,8 +307,8 @@ class _EvaluateVisitor
var query = AtRootQuery.defaultQuery;
if (node.query != null) {
var resolved = _performInterpolation(node.query, warnForColor: true);
query = _adjustParseError(
node.query.span, () => new AtRootQuery.parse(resolved));
query = _adjustParseError(node.query.span,
() => new AtRootQuery.parse(resolved, logger: _logger));
}
var parent = _parent;
@ -452,10 +452,9 @@ class _EvaluateVisitor
}
Value visitDebugRule(DebugRule node) {
var start = node.span.start;
var value = node.expression.accept(this);
stderr.writeln("${p.prettyUri(start.sourceUrl)}:${start.line + 1} DEBUG: "
"${value is SassString ? value.text : value}");
_logger.debug(
value is SassString ? value.text : value.toString(), node.span);
return null;
}
@ -538,7 +537,7 @@ class _EvaluateVisitor
var target = _adjustParseError(
targetText.span,
() => new SimpleSelector.parse(targetText.value.trim(),
allowParent: false));
logger: _logger, allowParent: false));
_extender.addExtension(_styleRule.selector, target, node, _mediaQueries);
return null;
}
@ -746,8 +745,8 @@ class _EvaluateVisitor
}
return url.startsWith('file') && pUrl.extension(url) == '.sass'
? new Stylesheet.parseSass(contents, url: url, color: _color)
: new Stylesheet.parseScss(contents, url: url, color: _color);
? new Stylesheet.parseSass(contents, url: url, logger: _logger)
: new Stylesheet.parseScss(contents, url: url, logger: _logger);
}
/// Parses the contents of [result] into a [Stylesheet].
@ -766,9 +765,9 @@ class _EvaluateVisitor
var displayUrl = url.resolve(p.basename(canonicalUrl.path));
return result.isIndented
? new Stylesheet.parseSass(result.contents,
url: displayUrl, color: _color)
url: displayUrl, logger: _logger)
: new Stylesheet.parseScss(result.contents,
url: displayUrl, color: _color);
url: displayUrl, logger: _logger);
});
}
@ -889,8 +888,8 @@ class _EvaluateVisitor
var resolved = _performInterpolation(interpolation, warnForColor: true);
// TODO(nweiz): Remove this type argument when sdk#31398 is fixed.
return _adjustParseError<List<CssMediaQuery>>(
interpolation.span, () => CssMediaQuery.parseList(resolved));
return _adjustParseError<List<CssMediaQuery>>(interpolation.span,
() => CssMediaQuery.parseList(resolved, logger: _logger));
}
/// Returns a list of queries that selects for platforms that match both
@ -915,8 +914,10 @@ class _EvaluateVisitor
var selectorText =
_interpolationToValue(node.selector, trim: true, warnForColor: true);
if (_inKeyframes) {
var parsedSelector = _adjustParseError(node.selector.span,
() => new KeyframeSelectorParser(selectorText.value).parse());
var parsedSelector = _adjustParseError(
node.selector.span,
() => new KeyframeSelectorParser(selectorText.value, logger: _logger)
.parse());
var rule = new CssKeyframeBlock(
new CssValue(
new List.unmodifiable(parsedSelector), node.selector.span),
@ -931,8 +932,8 @@ class _EvaluateVisitor
return null;
}
var parsedSelector = _adjustParseError(
node.selector.span, () => new SelectorList.parse(selectorText.value));
var parsedSelector = _adjustParseError(node.selector.span,
() => new SelectorList.parse(selectorText.value, logger: _logger));
parsedSelector = _addExceptionSpan(
node.selector.span,
() => parsedSelector.resolveParentSelectors(
@ -1044,18 +1045,13 @@ class _EvaluateVisitor
}
Value visitWarnRule(WarnRule node) {
_addExceptionSpan(node.span, () {
var value = node.expression.accept(this);
var string = value is SassString
? value.text
: _serialize(value, node.expression.span);
stderr.writeln("WARNING: $string");
});
for (var line in _stackTrace(node.span).toString().split("\n")) {
stderr.writeln(" $line");
}
var value =
_addExceptionSpan(node.span, () => node.expression.accept(this));
_logger.warn(
value is SassString
? value.text
: _serialize(value, node.expression.span),
trace: _stackTrace(node.span));
return null;
}
@ -1587,14 +1583,14 @@ class _EvaluateVisitor
BinaryOperator.plus,
new StringExpression(new Interpolation([""], null), quotes: true),
expression);
warn(
_warn(
"You probably don't mean to use the color value "
"${namesByColor[result]} in interpolation here.\n"
"It may end up represented as $result, which will likely produce "
"invalid CSS.\n"
"Always quote color names when using them as strings or map keys "
'(for example, "${namesByColor[result]}").\n'
"If you really want to use the color value here, use '$alternative'.\n",
"If you really want to use the color value here, use '$alternative'.",
expression.span);
}
@ -1697,6 +1693,11 @@ class _EvaluateVisitor
return new Trace(frames.reversed);
}
/// Emits a warning with the given [message] about the given [span].
void _warn(String message, FileSpan span, {bool deprecation: false}) =>
_logger.warn(message,
span: span, trace: _stackTrace(span), deprecation: deprecation);
/// Throws a [SassRuntimeException] with the given [message] and [span].
SassRuntimeException _exception(String message, FileSpan span) =>
new SassRuntimeException(message, span, _stackTrace(span));

View File

@ -168,6 +168,40 @@ void sharedTests(Future<TestProcess> runSass(Iterable<String> arguments)) {
await sass.shouldExit(0);
});
group("with --quiet", () {
test("doesn't emit @warn", () async {
await d.file("test.scss", "@warn heck").create();
var sass = await runSass(["--quiet", "test.scss"]);
expect(sass.stderr, emitsDone);
await sass.shouldExit(0);
});
test("doesn't emit @debug", () async {
await d.file("test.scss", "@debug heck").create();
var sass = await runSass(["--quiet", "test.scss"]);
expect(sass.stderr, emitsDone);
await sass.shouldExit(0);
});
test("doesn't emit parser warnings", () async {
await d.file("test.scss", "a {b: c && d}").create();
var sass = await runSass(["--quiet", "test.scss"]);
expect(sass.stderr, emitsDone);
await sass.shouldExit(0);
});
test("doesn't emit runner warnings", () async {
await d.file("test.scss", "#{blue} {x: y}").create();
var sass = await runSass(["--quiet", "test.scss"]);
expect(sass.stderr, emitsDone);
await sass.shouldExit(0);
});
});
group("reports errors", () {
test("from invalid arguments", () async {
var sass = await runSass(["--asdf"]);

View File

@ -0,0 +1,128 @@
// Copyright 2018 Google Inc. Use of this source code is governed by an
// MIT-style license that can be found in the LICENSE file or at
// https://opensource.org/licenses/MIT.
@TestOn('vm')
import 'package:test/test.dart';
import 'package:source_span/source_span.dart';
import 'package:stack_trace/stack_trace.dart';
import 'package:sass/sass.dart';
main() {
group("with @warn", () {
test("passes the message and stack trace to the logger", () {
var mustBeCalled = expectAsync0(() {});
compileString('''
@mixin foo {@warn heck}
@include foo;
''', logger:
new _TestLogger.withWarn((message, {span, trace, deprecation}) {
expect(message, equals("heck"));
expect(span, isNull);
expect(trace.frames.first.member, equals('foo()'));
expect(deprecation, isFalse);
mustBeCalled();
}));
});
test("stringifies the argument", () {
var mustBeCalled = expectAsync0(() {});
compileString('@warn #abc', logger:
new _TestLogger.withWarn((message, {span, trace, deprecation}) {
expect(message, equals("#abc"));
mustBeCalled();
}));
});
test("doesn't inspect the argument", () {
var mustBeCalled = expectAsync0(() {});
compileString('@warn null', logger:
new _TestLogger.withWarn((message, {span, trace, deprecation}) {
expect(message, isEmpty);
mustBeCalled();
}));
});
});
group("with @debug", () {
test("passes the message and span to the logger", () {
compileString('@debug heck',
logger: new _TestLogger.withDebug(expectAsync2((message, span) {
expect(message, equals("heck"));
expect(span.start.line, equals(0));
expect(span.start.column, equals(0));
expect(span.end.line, equals(0));
expect(span.end.column, equals(11));
})));
});
test("stringifies the argument", () {
compileString('@debug #abc',
logger: new _TestLogger.withDebug(expectAsync2((message, span) {
expect(message, equals("#abc"));
})));
});
test("inspects the argument", () {
compileString('@debug null',
logger: new _TestLogger.withDebug(expectAsync2((message, span) {
expect(message, equals("null"));
})));
});
});
test("with a parser warning passes the message and span", () {
var mustBeCalled = expectAsync0(() {});
compileString('a {b: c && d}',
logger: new _TestLogger.withWarn((message, {span, trace, deprecation}) {
expect(message, contains('"&&" means two copies'));
expect(span.start.line, equals(0));
expect(span.start.column, equals(8));
expect(span.end.line, equals(0));
expect(span.end.column, equals(10));
expect(trace, isNull);
expect(deprecation, isFalse);
mustBeCalled();
}));
});
test("with a runner warning passes the message, span, and trace", () {
var mustBeCalled = expectAsync0(() {});
compileString('''
@mixin foo {#{blue} {x: y}}
@include foo;
''',
logger: new _TestLogger.withWarn((message, {span, trace, deprecation}) {
expect(message, contains("color value blue"));
expect(span.start.line, equals(0));
expect(span.start.column, equals(22));
expect(span.end.line, equals(0));
expect(span.end.column, equals(26));
expect(trace.frames.first.member, equals('foo()'));
expect(deprecation, isFalse);
mustBeCalled();
}));
});
}
/// A [Logger] whose [warn] and [debug] methods are provided by callbacks.
class _TestLogger implements Logger {
final void Function(String, {FileSpan span, Trace trace, bool deprecation})
_warn;
final void Function(String, SourceSpan) _debug;
_TestLogger.withWarn(this._warn) : _debug = const Logger.stderr().debug;
_TestLogger.withDebug(this._debug) : _warn = const Logger.stderr().warn;
void warn(String message,
{FileSpan span, Trace trace, bool deprecation: false}) =>
_warn(message, span: span, trace: trace, deprecation: deprecation);
void debug(String message, SourceSpan span) => _debug(message, span);
}