├── .gitignore ├── AUTHORS ├── CHANGELOG.md ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md ├── LICENSE ├── README.md ├── analysis_options.yaml ├── bin └── sass_linter.dart ├── lib └── src │ ├── configuration.dart │ ├── engine.dart │ ├── exceptions.dart │ ├── lint.dart │ ├── linter.dart │ ├── rule.dart │ └── rules │ ├── no_debug.dart │ ├── no_empty_style.dart │ ├── no_loud_comment.dart │ ├── non_numeric_dimension.dart │ ├── quote_map_keys.dart │ ├── use_falsey_null.dart │ └── use_scale_color.dart ├── pubspec.yaml └── test ├── all_test.dart ├── cli_test.dart ├── configuration_test.dart ├── engine_test.dart ├── rule_test.dart └── rules ├── no_debug_test.dart ├── no_empty_style_test.dart ├── no_loud_comment_test.dart ├── non_numeric_dimension_test.dart ├── quote_map_keys_test.dart ├── use_falsey_null_test.dart └── use_scale_color_test.dart /.gitignore: -------------------------------------------------------------------------------- 1 | # Dart stuff 2 | .dart_tool 3 | .packages 4 | pubspec.lock 5 | -------------------------------------------------------------------------------- /AUTHORS: -------------------------------------------------------------------------------- 1 | # Below is a list of people and organizations that have contributed 2 | # to the project. Names should be added to the list like so: 3 | # 4 | # Name/Organization 5 | 6 | Google Inc. 7 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | ## 0.0.1 2 | 3 | * Both files and directories can be passed to the script. Directories will be 4 | searched recursively for files with the `.scss` extension. 5 | * New rules: `no_debug_rule`, `no_empty_style_rule`, `no_loud_comment_rule`, 6 | `non_numeric_dimension_rule`, `quote_map_keys_rule`, `use_falsey_null_rule`, 7 | `use_scale_color`, each with tests 8 | * Script supports arguments: `--config`, `--stdin` (and alias `-`), `--rules`, 9 | and `--stdin-file-url` 10 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | Sass is more than a technology; Sass is driven by the community of individuals 2 | that power its development and use every day. As a community, we want to embrace 3 | the very differences that have made our collaboration so powerful, and work 4 | together to provide the best environment for learning, growing, and sharing of 5 | ideas. It is imperative that we keep Sass a fun, welcoming, challenging, and 6 | fair place to play. 7 | 8 | [The full community guidelines can be found on the Sass website.][link] 9 | 10 | [link]: https://sass-lang.com/community-guidelines 11 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | Want to contribute? Great! First, read this page (including the small print at 2 | the end). 3 | 4 | ### Before you contribute 5 | Before we can use your code, you must sign the 6 | [Google Individual Contributor License Agreement](https://cla.developers.google.com/about/google-individual) 7 | (CLA), which you can do online. The CLA is necessary mainly because you own the 8 | copyright to your changes, even after your contribution becomes part of our 9 | codebase, so we need your permission to use and distribute your code. We also 10 | need to be sure of various other things—for instance that you'll tell us if you 11 | know that your code infringes on other people's patents. You don't have to sign 12 | the CLA until after you've submitted your code for review and a member has 13 | approved it, but you must do it before we can put your code into our codebase. 14 | 15 | Before you start working on a larger contribution, you should get in touch with 16 | us first through the issue tracker with your idea so that we can help out and 17 | possibly guide you. Coordinating up front makes it much easier to avoid 18 | frustration later on. 19 | 20 | ### Code reviews 21 | All submissions, including submissions by project members, require review. 22 | 23 | ### File headers 24 | All files in the project must start with the following header. 25 | 26 | // Copyright (c) 2018, the Dart project authors. Please see the AUTHORS file 27 | // for details. All rights reserved. Use of this source code is governed by a 28 | // BSD-style license that can be found in the LICENSE file. 29 | 30 | ### The small print 31 | Contributions made by corporations are covered by a different agreement than the 32 | one above, the 33 | [Software Grant and Corporate Contributor License Agreement](https://developers.google.com/open-source/cla/corporate). 34 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2018, Google Inc. 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining 4 | a copy of this software and associated documentation files (the 5 | "Software"), to deal in the Software without restriction, including 6 | without limitation the rights to use, copy, modify, merge, publish, 7 | distribute, sublicense, and/or sell copies of the Software, and to 8 | permit persons to whom the Software is furnished to do so, subject to 9 | the following conditions: 10 | 11 | The above copyright notice and this permission notice shall be 12 | included in all copies or substantial portions of the Software. 13 | 14 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 15 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 16 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 17 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 18 | LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 19 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 20 | WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 21 | 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Sass Linter 2 | 3 | The Sass Linter is the official linter of the Sass and SCSS languages. 4 | 5 | # Using the Sass Linter 6 | 7 | The Sass Linter does not yet have a runnable script. Stay tuned. 8 | 9 | # Goals 10 | 11 | There have been several stellar Sass linting tools, which help developers 12 | adhere to style guides, avoid typos and pitfalls, etc. These improve developer 13 | productivity by preventing bugs and reducing time in code review. 14 | 15 | However, none of these linters is written in Dart, the language of the official 16 | Sass implementation, and thus none use the primary Sass parser or AST which will 17 | be supported in the future. The [scss-lint] project in particular, written in 18 | Ruby, using the original parser, will fall behind as the Sass language moves 19 | beyond the Ruby implementation. 20 | 21 | The Sass Linter will be an officially supported client of the Dart Sass's AST, 22 | and will stay up-to-date with language changes as they are implemented in the 23 | official implementation, Dart Sass. 24 | 25 | [scss-lint]: https://github.com/brigade/scss-lint 26 | 27 | # TODO 28 | 29 | Don't release this stuff till these TODOs are done or deprioritized: 30 | 31 | * [x] DebugStatement 32 | * [x] SilentComment 33 | * [ ] MergeableSelector 34 | * [ ] EmptyRule 35 | * [ ] ImportPath 36 | * [ ] Ignore via `ignore: ` 37 | * [ ] README 38 | * [x] COTNRIBUTING.md 39 | * [ ] Lint rules README 40 | * [-] Tests 41 | * [-] Tests of message, line, column 42 | * [ ] Fixes 43 | * [x] Command line 44 | * [x] Flag: --stdin 45 | * [x] Flag: --stdin-file-path 46 | * [ ] Flag: --color 47 | * [x] Flag: --help 48 | * [ ] FLag: --options-file 49 | * [ ] Options file 50 | * [ ] Ignore lint rules 51 | * [ ] Ignore globs 52 | * [ ] Travis 53 | * [ ] Coverage 54 | * [ ] Performance 55 | -------------------------------------------------------------------------------- /analysis_options.yaml: -------------------------------------------------------------------------------- 1 | analyzer: 2 | strong-mode: 3 | implicit-casts: false 4 | -------------------------------------------------------------------------------- /bin/sass_linter.dart: -------------------------------------------------------------------------------- 1 | // Copyright 2018 Google Inc. Use of this source code is governed by an 2 | // MIT-style license that can be found in the LICENSE file or at 3 | // https://opensource.org/licenses/MIT. 4 | 5 | import 'dart:io' as io; 6 | 7 | import 'package:args/args.dart'; 8 | import 'package:meta/meta.dart'; 9 | import 'package:sass_linter/src/configuration.dart'; 10 | import 'package:sass_linter/src/engine.dart'; 11 | import 'package:sass_linter/src/exceptions.dart'; 12 | import 'package:sass_linter/src/lint.dart'; 13 | import 'package:sass_linter/src/rule.dart'; 14 | import 'package:source_span/source_span.dart'; 15 | 16 | void main(List args) { 17 | var argParser = new ArgParser() 18 | ..addOption('config', 19 | help: 'Path to a config YAML file; all options parsed at the ' 20 | 'command-line will override options in the config file.') 21 | ..addMultiOption('rules', help: 'List of rules to check') 22 | ..addFlag('stdin', help: 'Read Sass source from stdin.', negatable: false) 23 | ..addOption('stdin-file-url', 24 | help: 'Use this file url when reporting lint from stdin.') 25 | ..addFlag('help', abbr: 'h', help: 'Print help text.', negatable: false); 26 | var argResults = argParser.parse(args); 27 | 28 | try { 29 | Configuration config; 30 | try { 31 | if (argResults['help'] == true) _usage('Report lint found in Sass'); 32 | 33 | // Generate a [Configuration], either by reading a configuration file, or 34 | // by generating an empty one. 35 | config = argResults.wasParsed('config') 36 | ? Configuration.parse(argResults['config'] as String) 37 | : Configuration.empty; 38 | } on SourceSpanException catch (error) { 39 | print(error.toString(color: _supportsAnsiEscapes)); 40 | io.exit(255); 41 | } 42 | 43 | List rules; 44 | try { 45 | rules = argResults.wasParsed('rules') 46 | ? parseRules(argResults['rules'] as List).toList() 47 | : config.rules; 48 | } on UnknownRuleException catch (error) { 49 | throw UsageException('Unknown rule: ${error.ruleName}'); 50 | } 51 | 52 | if (argResults['stdin'] == true) { 53 | var engine = new Engine(['-'], 54 | rules: rules, stdinFileUrl: argResults['stdin-file-url'] as String); 55 | _report(engine.run()); 56 | } else { 57 | var paths = argResults.rest; 58 | if (paths.isEmpty) paths = config.paths; 59 | 60 | if (paths.isEmpty) _usage('Report lint found in Sass'); 61 | 62 | var engine = new Engine(paths, 63 | rules: rules, stdinFileUrl: argResults['stdin-file-url'] as String); 64 | _report(engine.run()); 65 | } 66 | } on UsageException catch (error) { 67 | print('${error.message}\n\n' 68 | 'Usage: sass_linter [input.scss input/ ...]\n' 69 | ' sass_linter -\n\n' 70 | '${argParser.usage}'); 71 | io.exitCode = 64; 72 | } 73 | } 74 | 75 | @alwaysThrows 76 | // Throws a [UsageException] with the given [message]. 77 | void _usage(String message) => throw new UsageException(message); 78 | 79 | void _report(Iterable lints) { 80 | for (var lint in lints) { 81 | var url = lint.url ?? '[missing url]'; 82 | print('${lint.message} at $url line ${lint.line + 1} ' 83 | '(${lint.rule.name})'); 84 | } 85 | } 86 | 87 | /// Returns whether the current platform supports ANSI escape codes. 88 | bool get _supportsAnsiEscapes => 89 | io.stdout.hasTerminal && 90 | // We don't trust [io.stdout.supportsAnsiEscapes] except on Windows because 91 | // it relies on the TERM environment variable which has many false 92 | // negatives. 93 | (!io.Platform.isWindows || io.stdout.supportsAnsiEscapes); 94 | -------------------------------------------------------------------------------- /lib/src/configuration.dart: -------------------------------------------------------------------------------- 1 | // Copyright 2018 Google Inc. Use of this source code is governed by an 2 | // MIT-style license that can be found in the LICENSE file or at 3 | // https://opensource.org/licenses/MIT. 4 | 5 | import 'dart:io'; 6 | 7 | import 'package:path/path.dart' as p; 8 | import 'package:yaml/yaml.dart'; 9 | import 'package:sass_linter/src/engine.dart'; 10 | import 'package:sass_linter/src/exceptions.dart'; 11 | import 'package:sass_linter/src/rule.dart'; 12 | 13 | /// A collection of configuration options. 14 | class Configuration { 15 | /// An empty configuration with only default values. 16 | static final empty = Configuration._(null, []); 17 | 18 | /// Rules to use when linting. 19 | final List rules; 20 | 21 | /// Paths which the linter will examine. 22 | final List paths; 23 | 24 | /// Load a [YamlNode] from [source] and ensure that it is a [YamlMap]. 25 | static YamlMap _loadYamlMap(String source, String path) { 26 | try { 27 | var document = loadYamlNode(source, sourceUrl: p.toUri(path)); 28 | 29 | if (document is! Map) { 30 | throw ConfigParseException( 31 | 'The configuration file must be a Yaml map.', document.span); 32 | } 33 | 34 | return document as YamlMap; 35 | } on YamlException catch (error) { 36 | throw ConfigParseException(error.toString(), error.span); 37 | } 38 | } 39 | 40 | /// Asserts that [field] is a list and runs [forElement] for each element it 41 | /// contains. 42 | /// 43 | /// Returns a list of values returned by [forElement]. 44 | static List _getList( 45 | YamlMap document, String field, T forElement(YamlNode elementNode)) { 46 | var node = document.nodes[field]; 47 | if (node.value == null) return []; 48 | _validate(node, '"$field" in the configuration file must be a List.', 49 | (value) => value is List); 50 | return (node as YamlList).nodes.map(forElement).toList(); 51 | } 52 | 53 | /// Throws an exception with [message] if [test] returns `false` when passed 54 | /// [node]'s value. 55 | static void _validate(YamlNode node, String message, bool test(value)) { 56 | if (test(node.value)) return; 57 | throw ConfigParseException(message, node.span); 58 | } 59 | 60 | /// Parse a [Configuration] from a YAML file at [path]. 61 | factory Configuration.parse(String path) { 62 | var source = File(path).readAsStringSync(); 63 | if (source.isEmpty) return Configuration.empty; 64 | 65 | var document = Configuration._loadYamlMap(source, path); 66 | if (document.value == null) return Configuration.empty; 67 | 68 | List rules; 69 | 70 | // Use `null` for a missing or `null` value, and let the engine populate a 71 | // default list. 72 | if (document.containsKey('rules') && document['rules'] != null) { 73 | var ruleNameNodes = _getList(document, 'rules', (ruleNode) { 74 | _validate( 75 | ruleNode, 'Rules must be strings.', (value) => value is String); 76 | return ruleNode; 77 | }); 78 | rules = []; 79 | for (var ruleNameNode in ruleNameNodes) { 80 | try { 81 | rules.add(parseRule(ruleNameNode.value)); 82 | } on UnknownRuleException { 83 | throw ConfigParseException('Unknown rule', ruleNameNode.span); 84 | } 85 | } 86 | } 87 | 88 | List paths = []; 89 | 90 | if (document.containsKey('paths')) { 91 | paths = _getList(document, 'paths', (pathNode) { 92 | _validate( 93 | pathNode, 'Paths must be strings.', (value) => value is String); 94 | return pathNode.value; 95 | }); 96 | } 97 | return Configuration._(rules, paths); 98 | } 99 | 100 | Configuration._(this.rules, this.paths); 101 | } 102 | -------------------------------------------------------------------------------- /lib/src/engine.dart: -------------------------------------------------------------------------------- 1 | // Copyright 2018 Google Inc. Use of this source code is governed by an 2 | // MIT-style license that can be found in the LICENSE file or at 3 | // https://opensource.org/licenses/MIT. 4 | 5 | import 'dart:io'; 6 | 7 | import 'exceptions.dart'; 8 | import 'lint.dart'; 9 | import 'linter.dart'; 10 | import 'rule.dart'; 11 | import 'rules/no_debug.dart'; 12 | import 'rules/no_empty_style.dart'; 13 | import 'rules/no_loud_comment.dart'; 14 | import 'rules/non_numeric_dimension.dart'; 15 | import 'rules/quote_map_keys.dart'; 16 | import 'rules/use_falsey_null.dart'; 17 | 18 | /// Literally all of the rules defined in this package. Whether the binary will 19 | /// check all of the rules, or a subset, or none, by default, may change how 20 | /// this list is used. That cannot happen until the binary supports a config 21 | /// file or a flag with a list of lint rules. 22 | final allRules = [ 23 | new NoDebugRule(), 24 | new NoEmptyStyleRule(), 25 | new NoLoudCommentRule(), 26 | new NonNumericDimensionRule(), 27 | new QuoteMapKeysRule(), 28 | new UseFalseyNullRule(), 29 | ]; 30 | 31 | /// The engine maintains a context for linting. 32 | /// 33 | /// This just includes simple things like a list of paths to be linted, etc. 34 | class Engine { 35 | /// The paths of Sass files to be linted. 36 | /// 37 | /// This list may also include "-", the special path that represents `stdin`. 38 | final List paths; 39 | 40 | /// [Rule]s which will be run by this engine. 41 | final List rules; 42 | 43 | /// The file URL to use when reporting lints from stdin. 44 | final String stdinFileUrl; 45 | 46 | /// Create a new [Engine] to process files in [paths] with [rules]. 47 | /// 48 | /// If no [rules] are passed in, the Engine will process with all available 49 | /// rules. 50 | /// 51 | /// For any path equal to "-", stdin will be processed. Pass in 52 | /// [stdinFileUrl] in order to report a specific path for lint found in 53 | /// stdin. 54 | Engine(this.paths, {Iterable rules, this.stdinFileUrl}) 55 | : this.rules = new List.unmodifiable(rules ?? allRules); 56 | 57 | /// Run all of the defined rules against the Sass input(s). 58 | Iterable run() { 59 | // TODO(srawlins): This currently produces a list of lint sorted first by 60 | // path, then by lint rule, then, theoretically, by line number. They should 61 | // instead be sorted by path (sorted, even though [paths] may not be 62 | // sorted?), then by line number, then maybe by lint rule (maybe sorting by 63 | // lint rule at the end is not important, but I think at least stability is 64 | // important). 65 | return paths.map((path) { 66 | if (path == '-') { 67 | var source = new StringBuffer(); 68 | while (true) { 69 | var line = stdin.readLineSync(); 70 | if (line == null) break; 71 | source.writeln(line); 72 | } 73 | return new Linter(source.toString(), rules, url: stdinFileUrl).run(); 74 | } else if (FileSystemEntity.isDirectorySync(path)) { 75 | var lint = []; 76 | for (var filePath in _scssFilesInDir(path)) { 77 | var source = new File(filePath).readAsStringSync(); 78 | lint.addAll(new Linter(source, rules, url: filePath).run()); 79 | } 80 | return lint; 81 | } else if (FileSystemEntity.isFileSync(path)) { 82 | var source = new File(path).readAsStringSync(); 83 | return new Linter(source, rules, url: path).run(); 84 | } 85 | }).expand((lint) => lint); 86 | } 87 | } 88 | 89 | /// Parse [rules] for linter [Rule]s. 90 | /// 91 | /// Translates user-written String names for rules into instances of [Rule]s, 92 | /// allowing for String-based APIs (e.g. command line). Rule names that both 93 | /// include and exclude the "_rule" suffix can be parsed. 94 | Iterable parseRules(List rules) => rules.map(parseRule); 95 | 96 | Rule parseRule(String ruleName) { 97 | var sanitizedName = 98 | ruleName.endsWith('_rule') ? ruleName : '${ruleName}_rule'; 99 | try { 100 | return allRules.firstWhere((r) => r.name == sanitizedName); 101 | } on StateError { 102 | throw UnknownRuleException(ruleName); 103 | } 104 | } 105 | 106 | Iterable _scssFilesInDir(String path) => new Directory(path) 107 | .listSync(recursive: true) 108 | .where((entity) => entity is File) 109 | .map((entity) => entity.path) 110 | .where((path) => path.endsWith('.scss')); 111 | -------------------------------------------------------------------------------- /lib/src/exceptions.dart: -------------------------------------------------------------------------------- 1 | // Copyright 2018 Google Inc. Use of this source code is governed by an 2 | // MIT-style license that can be found in the LICENSE file or at 3 | // https://opensource.org/licenses/MIT. 4 | 5 | import 'package:source_span/source_span.dart'; 6 | 7 | /// An exception indicating that invalid values were found in a config YAML 8 | /// file. 9 | class ConfigParseException extends SourceSpanFormatException { 10 | ConfigParseException(String message, SourceSpan span) : super(message, span); 11 | } 12 | 13 | class UnknownRuleException implements Exception { 14 | final String ruleName; 15 | 16 | UnknownRuleException(this.ruleName); 17 | } 18 | 19 | /// An exception indicating that invalid arguments were passed. 20 | class UsageException implements Exception { 21 | final String message; 22 | 23 | UsageException(this.message); 24 | } 25 | -------------------------------------------------------------------------------- /lib/src/lint.dart: -------------------------------------------------------------------------------- 1 | // Copyright 2018 Google Inc. Use of this source code is governed by an 2 | // MIT-style license that can be found in the LICENSE file or at 3 | // https://opensource.org/licenses/MIT. 4 | 5 | import 'package:meta/meta.dart'; 6 | import 'package:source_span/source_span.dart'; 7 | 8 | import 'rule.dart'; 9 | 10 | /// A violation of a lint rule; a piece of lint. 11 | class Lint { 12 | /// The rule which generated this violation. 13 | final Rule rule; 14 | 15 | /// A helpful description of this violation. 16 | final String message; 17 | 18 | /// The location of the violation in the source. 19 | final FileSpan span; 20 | 21 | Lint({@required this.rule, @required this.message, @required this.span}); 22 | 23 | /// The URL of the source of this violation. 24 | Uri get url => span.sourceUrl; 25 | 26 | /// The 1-based line number of this violation in the source. 27 | int get line => span.start.line; 28 | 29 | /// The 1-based column number of this violation in the source. 30 | int get column => span.start.column; 31 | } 32 | -------------------------------------------------------------------------------- /lib/src/linter.dart: -------------------------------------------------------------------------------- 1 | // Copyright 2018 Google Inc. Use of this source code is governed by an 2 | // MIT-style license that can be found in the LICENSE file or at 3 | // https://opensource.org/licenses/MIT. 4 | 5 | // The sass package's API is not necessarily stable. It is being imported with 6 | // the Sass team's explicit knowledge and approval. See 7 | // https://github.com/sass/dart-sass/issues/236. 8 | import 'package:sass/src/ast/sass.dart'; 9 | 10 | import 'lint.dart'; 11 | import 'rule.dart'; 12 | 13 | /// The Linter finds lint in a Sass document by examining it with different lint rules. 14 | class Linter { 15 | /// The root [Stylesheet] node of a Sass document. 16 | final Stylesheet tree; 17 | 18 | /// The rules which will examine the [tree]. 19 | final List rules; 20 | 21 | /// Set up a Linter to examine [source] with [rules]. 22 | /// 23 | /// Specify the [url] of [source] for reporting purposes. 24 | Linter(String source, Iterable rules, {url}) 25 | : this.tree = new Stylesheet.parseScss(source, url: url), 26 | this.rules = new List.unmodifiable(rules); 27 | 28 | /// Runs the [rules] over [tree], returning any found lint. 29 | List run() => rules.expand((rule) => tree.accept(rule)).toList(); 30 | } 31 | -------------------------------------------------------------------------------- /lib/src/rule.dart: -------------------------------------------------------------------------------- 1 | // Copyright 2018 Google Inc. Use of this source code is governed by an 2 | // MIT-style license that can be found in the LICENSE file or at 3 | // https://opensource.org/licenses/MIT. 4 | 5 | // The sass package's API is not necessarily stable. It is being imported with 6 | // the Sass team's explicit knowledge and approval. See 7 | // https://github.com/sass/dart-sass/issues/236. 8 | import 'package:sass/src/ast/sass.dart'; 9 | import 'package:sass/src/visitor/interface/expression.dart'; 10 | import 'package:sass/src/visitor/interface/statement.dart'; 11 | 12 | import 'lint.dart'; 13 | 14 | /// A parent class for all lint rules, which visits all nodes in a [Stylesheet]. 15 | /// 16 | /// The implementations of each visitor will eventually guarantee a traversal 17 | /// of an entire [Stylesheet]. Extenders need only visit individual nodes that 18 | /// they might act on. 19 | abstract class Rule 20 | implements StatementVisitor>, ExpressionVisitor> { 21 | /// The name of the lint rule. 22 | /// 23 | /// The [name] acts as an identifier, and may be used in output. It should be 24 | /// underscore_case, unique, and brief. 25 | final String name; 26 | 27 | Rule(this.name); 28 | 29 | @override 30 | List visitAtRootRule(AtRootRule node) { 31 | var lint = []; 32 | if (node.query != null) lint.addAll(_visitInterpolation(node.query)); 33 | for (var child in node.children) { 34 | lint.addAll(child.accept(this)); 35 | } 36 | return lint; 37 | } 38 | 39 | @override 40 | List visitAtRule(AtRule node) { 41 | throw new UnimplementedError(); 42 | } 43 | 44 | @override 45 | List visitBinaryOperationExpression(BinaryOperationExpression node) { 46 | return node.left.accept(this) + node.right.accept(this); 47 | } 48 | 49 | @override 50 | List visitBooleanExpression(BooleanExpression node) { 51 | return []; 52 | } 53 | 54 | @override 55 | List visitColorExpression(ColorExpression node) { 56 | return []; 57 | } 58 | 59 | @override 60 | List visitContentRule(ContentRule node) { 61 | throw new UnimplementedError(); 62 | } 63 | 64 | @override 65 | List visitDebugRule(DebugRule node) { 66 | return node.expression.accept(this); 67 | } 68 | 69 | @override 70 | List visitDeclaration(Declaration node) { 71 | // TODO(srawlins): Visit and test children. 72 | return _visitInterpolation(node.name) + node.value.accept(this); 73 | } 74 | 75 | @override 76 | List visitEachRule(EachRule node) { 77 | var lint = node.list.accept(this); 78 | for (var child in node.children) { 79 | lint.addAll(child.accept(this)); 80 | } 81 | return lint; 82 | } 83 | 84 | @override 85 | List visitErrorRule(ErrorRule node) { 86 | return node.expression.accept(this); 87 | } 88 | 89 | @override 90 | List visitExtendRule(ExtendRule node) { 91 | return _visitInterpolation(node.selector); 92 | } 93 | 94 | @override 95 | List visitForRule(ForRule node) { 96 | var lint = node.from.accept(this) + node.to.accept(this); 97 | for (var child in node.children) { 98 | lint.addAll(child.accept(this)); 99 | } 100 | return lint; 101 | } 102 | 103 | @override 104 | List visitFunctionExpression(FunctionExpression node) { 105 | return _visitInterpolation(node.name) + 106 | _visitArgumentInvocation(node.arguments); 107 | } 108 | 109 | @override 110 | List visitFunctionRule(FunctionRule node) { 111 | // TODO(srawlins): visit and test `arguments`. 112 | var lint = []; 113 | for (var child in node.children) { 114 | lint.addAll(child.accept(this)); 115 | } 116 | return lint; 117 | } 118 | 119 | @override 120 | List visitIfExpression(IfExpression node) { 121 | throw new UnimplementedError(); 122 | } 123 | 124 | @override 125 | List visitIfRule(IfRule node) { 126 | var lint = []; 127 | for (var clause in node.clauses) { 128 | lint.addAll(clause.expression.accept(this)); 129 | for (var child in clause.children) { 130 | lint.addAll(child.accept(this)); 131 | } 132 | } 133 | if (node.lastClause != null) { 134 | for (var child in node.lastClause.children) { 135 | lint.addAll(child.accept(this)); 136 | } 137 | } 138 | return lint; 139 | } 140 | 141 | @override 142 | List visitImportRule(ImportRule node) { 143 | throw new UnimplementedError(); 144 | } 145 | 146 | @override 147 | List visitIncludeRule(IncludeRule node) { 148 | throw new UnimplementedError(); 149 | } 150 | 151 | @override 152 | List visitListExpression(ListExpression node) { 153 | var lint = []; 154 | for (var value in node.contents) { 155 | lint.addAll(value.accept(this)); 156 | } 157 | return lint; 158 | } 159 | 160 | @override 161 | List visitLoudComment(LoudComment node) { 162 | return _visitInterpolation(node.text); 163 | } 164 | 165 | @override 166 | List visitMapExpression(MapExpression node) { 167 | var lint = []; 168 | for (var pair in node.pairs) { 169 | lint.addAll(pair.item1.accept(this)); 170 | lint.addAll(pair.item2.accept(this)); 171 | } 172 | return lint; 173 | } 174 | 175 | @override 176 | List visitMediaRule(MediaRule node) { 177 | throw new UnimplementedError(); 178 | } 179 | 180 | @override 181 | List visitMixinRule(MixinRule node) { 182 | throw new UnimplementedError(); 183 | } 184 | 185 | @override 186 | List visitNullExpression(NullExpression node) { 187 | return []; 188 | } 189 | 190 | @override 191 | List visitNumberExpression(NumberExpression node) { 192 | return []; 193 | } 194 | 195 | @override 196 | List visitParenthesizedExpression(ParenthesizedExpression node) { 197 | return node.expression.accept(this); 198 | } 199 | 200 | @override 201 | List visitReturnRule(ReturnRule node) { 202 | return node.expression.accept(this); 203 | } 204 | 205 | @override 206 | List visitSelectorExpression(SelectorExpression node) { 207 | throw new UnimplementedError(); 208 | } 209 | 210 | @override 211 | List visitSilentComment(SilentComment node) { 212 | return []; 213 | } 214 | 215 | @override 216 | List visitStringExpression(StringExpression node) { 217 | // TODO(srawlins): visit and test `text`. 218 | return []; 219 | } 220 | 221 | @override 222 | List visitStyleRule(StyleRule node) { 223 | var lint = _visitInterpolation(node.selector); 224 | for (var child in node.children) { 225 | lint.addAll(child.accept(this)); 226 | } 227 | return lint; 228 | } 229 | 230 | @override 231 | List visitStylesheet(Stylesheet node) { 232 | return node.children.expand((child) => child.accept(this)).toList(); 233 | } 234 | 235 | @override 236 | List visitSupportsRule(SupportsRule node) { 237 | throw new UnimplementedError(); 238 | } 239 | 240 | @override 241 | List visitUnaryOperationExpression(UnaryOperationExpression node) { 242 | throw new UnimplementedError(); 243 | } 244 | 245 | @override 246 | List visitValueExpression(ValueExpression node) { 247 | throw new UnimplementedError(); 248 | } 249 | 250 | @override 251 | List visitVariableDeclaration(VariableDeclaration node) { 252 | return node.expression.accept(this); 253 | } 254 | 255 | @override 256 | List visitVariableExpression(VariableExpression node) { 257 | return []; 258 | } 259 | 260 | @override 261 | List visitWarnRule(WarnRule node) { 262 | return node.expression.accept(this); 263 | } 264 | 265 | @override 266 | List visitWhileRule(WhileRule node) { 267 | throw new UnimplementedError(); 268 | } 269 | 270 | /// Visit [node], an ArgumentInvocation, returning the lint found within. 271 | List _visitArgumentInvocation(ArgumentInvocation node) { 272 | var lint = []; 273 | for (var argument in node.positional) { 274 | lint.addAll(argument.accept(this)); 275 | } 276 | node.named.forEach((name, value) { 277 | lint.addAll(value.accept(this)); 278 | }); 279 | if (node.rest != null) lint.addAll(node.rest.accept(this)); 280 | if (node.keywordRest != null) lint.addAll(node.keywordRest.accept(this)); 281 | return lint; 282 | } 283 | 284 | List _visitInterpolation(Interpolation node) { 285 | var lint = []; 286 | for (var value in node.contents) { 287 | if (value is String) continue; 288 | lint.addAll((value as Expression).accept(this)); 289 | } 290 | return lint; 291 | } 292 | } 293 | -------------------------------------------------------------------------------- /lib/src/rules/no_debug.dart: -------------------------------------------------------------------------------- 1 | // Copyright 2018 Google Inc. Use of this source code is governed by an 2 | // MIT-style license that can be found in the LICENSE file or at 3 | // https://opensource.org/licenses/MIT. 4 | 5 | // The sass package's API is not necessarily stable. It is being imported with 6 | // the Sass team's explicit knowledge and approval. See 7 | // https://github.com/sass/dart-sass/issues/236. 8 | import 'package:sass/src/ast/sass.dart'; 9 | 10 | import '../lint.dart'; 11 | import '../rule.dart'; 12 | 13 | /// A lint rule that reports on the existence of @debug at-rules. 14 | /// 15 | /// These at-rules should not be found in Sass documents, for example checked 16 | /// into source control. 17 | class NoDebugRule extends Rule { 18 | NoDebugRule() : super('no_debug_rule'); 19 | 20 | @override 21 | List visitDebugRule(DebugRule node) { 22 | return [ 23 | new Lint( 24 | rule: this, 25 | span: node.span, 26 | message: '@debug directives should be removed.') 27 | ]; 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /lib/src/rules/no_empty_style.dart: -------------------------------------------------------------------------------- 1 | // Copyright 2018 Google Inc. Use of this source code is governed by an 2 | // MIT-style license that can be found in the LICENSE file or at 3 | // https://opensource.org/licenses/MIT. 4 | 5 | // The sass package's API is not necessarily stable. It is being imported with 6 | // the Sass team's explicit knowledge and approval. See 7 | // https://github.com/sass/dart-sass/issues/236. 8 | import 'package:sass/src/ast/sass.dart'; 9 | 10 | import '../lint.dart'; 11 | import '../rule.dart'; 12 | 13 | /// A lint rule that reports on empty style rules. 14 | /// 15 | /// A style rule is considered "empty" if it contains no "outputting" children 16 | /// statements. For example, a variable declaration does not, itself, output 17 | /// anything in compiled CSS. 18 | class NoEmptyStyleRule extends Rule { 19 | NoEmptyStyleRule() : super('no_empty_style_rule'); 20 | 21 | @override 22 | List visitStyleRule(StyleRule node) { 23 | var lint = []; 24 | 25 | // A StyleRule has style declarations if it is non-empty (`every` will 26 | // always return `true` on an empty list), and if it contains anything 27 | // other than these non-outputting rules. 28 | var hasStyleDeclaration = !node.children.every((child) => 29 | child is DebugRule || 30 | child is ErrorRule || 31 | child is FunctionRule || 32 | child is MixinRule || 33 | child is SilentComment || 34 | child is VariableDeclaration || 35 | child is WarnRule); 36 | 37 | if (!hasStyleDeclaration) { 38 | lint.add( 39 | Lint(rule: this, span: node.span, message: 'Style rule is empty.')); 40 | } 41 | 42 | return lint..addAll(super.visitStyleRule(node)); 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /lib/src/rules/no_loud_comment.dart: -------------------------------------------------------------------------------- 1 | // Copyright 2018 Google Inc. Use of this source code is governed by an 2 | // MIT-style license that can be found in the LICENSE file or at 3 | // https://opensource.org/licenses/MIT. 4 | 5 | import 'package:charcode/charcode.dart'; 6 | 7 | // The sass package's API is not necessarily stable. It is being imported with 8 | // the Sass team's explicit knowledge and approval. See 9 | // https://github.com/sass/dart-sass/issues/236. 10 | import 'package:sass/src/ast/sass.dart'; 11 | 12 | import '../lint.dart'; 13 | import '../rule.dart'; 14 | 15 | /// A lint rule that reports on the existence of "loud" (`/* */`) comments. 16 | /// 17 | /// Comments should typically be "silent" (`//`), so that they are not shipped 18 | /// to browsers, in the CSS output. 19 | class NoLoudCommentRule extends Rule { 20 | NoLoudCommentRule() : super('no_loud_comment_rule'); 21 | 22 | @override 23 | List visitLoudComment(LoudComment node) { 24 | var textContents = node.text.contents; 25 | if (textContents.isNotEmpty && 26 | textContents.first is String && 27 | textContents.first.codeUnitAt(2) == $exclamation) { 28 | // Loud "preserved" comments are fine. See 29 | // https://sass-lang.com/documentation/file.SASS_REFERENCE.html#comments. 30 | return []; 31 | } 32 | 33 | return [ 34 | new Lint( 35 | rule: this, 36 | span: node.span, 37 | message: 'Comments should be written with the silent (`//`) syntax.') 38 | ]; 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /lib/src/rules/non_numeric_dimension.dart: -------------------------------------------------------------------------------- 1 | // Copyright 2018 Google Inc. Use of this source code is governed by an 2 | // MIT-style license that can be found in the LICENSE file or at 3 | // https://opensource.org/licenses/MIT. 4 | 5 | // The sass package's API is not necessarily stable. It is being imported with 6 | // the Sass team's explicit knowledge and approval. See 7 | // https://github.com/sass/dart-sass/issues/236. 8 | import 'package:sass/src/ast/sass.dart'; 9 | 10 | import '../lint.dart'; 11 | import '../rule.dart'; 12 | 13 | // Units which may be seen "appended" to a number. 14 | const List _units = const [ 15 | // Font-relative lengths: 16 | // https://www.w3.org/TR/css-values-4/#font-relative-lengths 17 | 'em', 'ex', 'cap', 'ch', 'ic', 'rem', 'lh', 'rlh', 18 | 19 | // Viewport-relative lengths: 20 | // https://www.w3.org/TR/css-values-4/#viewport-relative-lengths 21 | 'vw', 'vh', 'vi', 'vb', 'vmin', 'vmax', 22 | 23 | // Absolute lengths: 24 | // https://www.w3.org/TR/css-values-4/#absolute-lengths 25 | 'cm', 'mm', 'Q', 'in', 'pc', 'pt', 'px', 26 | 27 | // Angle units: 28 | // https://www.w3.org/TR/css-values-4/#angles 29 | 'deg', 'grad', 'rad', 'turn', 30 | 31 | // Duration units: 32 | // https://www.w3.org/TR/css-values-4/#time 33 | 's', 'ms', 34 | 35 | // Frequency units: 36 | // https://www.w3.org/TR/css-values-4/#frequency 37 | 'Hz', 'kHz', 38 | 39 | // Resolution units: 40 | // https://www.w3.org/TR/css-values-4/#resolution 41 | 'dpi', 'dpcm', 'dppx', 'x', 42 | 43 | // Flexible lengths: 44 | // https://www.w3.org/TR/css-grid-1/#fr-unit 45 | 'fr', 46 | 47 | // Percentage: 48 | // https://www.w3.org/TR/css-values-4/#percentages 49 | '%' 50 | ]; 51 | 52 | /// A lint rule that reports on non-numeric dimensions which are created by 53 | /// interpolating a value with a unit. 54 | /// 55 | /// Interpolating a value with a unit (e.g. `#{$value}px`) results in a 56 | /// _string_ value, not as numeric value. This value then cannot be used in 57 | /// numerical operations. It is better to use arithmetic to apply a unit to a 58 | /// number (e.g. `$value * 1px`). 59 | class NonNumericDimensionRule extends Rule { 60 | NonNumericDimensionRule() : super('non_numeric_dimension_rule'); 61 | 62 | @override 63 | List visitStringExpression(StringExpression node) { 64 | var lint = []; 65 | 66 | if (node.hasQuotes) { 67 | // A quoted string is much more likely a deliberate non-numeric 68 | // dimension. Leave it alone. 69 | return lint; 70 | } 71 | 72 | if (node.text.contents.length != 2) { 73 | // More than two items are also much more likely a deliberate non-numeric 74 | // dimension. Leave it alone. 75 | return lint; 76 | } 77 | 78 | var firstItem = node.text.contents[0]; 79 | if (firstItem is String) { 80 | return lint; 81 | } 82 | 83 | var secondItem = node.text.contents[1]; 84 | if (secondItem is String && _units.contains(secondItem)) { 85 | var suggestionNeedsParens = firstItem is BinaryOperationExpression && 86 | firstItem.operator.precedence < BinaryOperator.times.precedence; 87 | var replacementSuggestion = suggestionNeedsParens 88 | ? '($firstItem) * 1$secondItem' 89 | : '$firstItem * 1$secondItem'; 90 | lint.add(new Lint( 91 | rule: this, 92 | span: node.span, 93 | message: 'Apply a unit to a numerical value via arithmetic, ' 94 | 'rather than interpolation; e.g. `$replacementSuggestion`. See ' 95 | 'https://sass-lang.com/documentation/values/numbers#units for ' 96 | 'more information.')); 97 | } 98 | 99 | return lint; 100 | } 101 | } 102 | -------------------------------------------------------------------------------- /lib/src/rules/quote_map_keys.dart: -------------------------------------------------------------------------------- 1 | // Copyright 2018 Google Inc. Use of this source code is governed by an 2 | // MIT-style license that can be found in the LICENSE file or at 3 | // https://opensource.org/licenses/MIT. 4 | 5 | // The sass package's API is not necessarily stable. It is being imported with 6 | // the Sass team's explicit knowledge and approval. See 7 | // https://github.com/sass/dart-sass/issues/236. 8 | import 'package:sass/src/ast/sass.dart'; 9 | 10 | import '../lint.dart'; 11 | import '../rule.dart'; 12 | 13 | /// A lint rule that reports on unquoted map keys in a map literal. 14 | /// 15 | /// Some tokens (like `green`) parse as colors rather than strings. It is safer 16 | /// to always quote map keys. 17 | class QuoteMapKeysRule extends Rule { 18 | QuoteMapKeysRule() : super('quote_map_keys_rule'); 19 | 20 | @override 21 | List visitMapExpression(MapExpression node) { 22 | var lint = []; 23 | 24 | for (var key in node.pairs.map((p) => p.item1)) { 25 | if (key is StringExpression && !key.hasQuotes) { 26 | lint.add(Lint( 27 | rule: this, 28 | span: key.span, 29 | message: 'String literal map keys should be quoted.')); 30 | } 31 | } 32 | 33 | return lint..addAll(super.visitMapExpression(node)); 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /lib/src/rules/use_falsey_null.dart: -------------------------------------------------------------------------------- 1 | // Copyright 2018 Google Inc. Use of this source code is governed by an 2 | // MIT-style license that can be found in the LICENSE file or at 3 | // https://opensource.org/licenses/MIT. 4 | 5 | // The sass package's API is not necessarily stable. It is being imported with 6 | // the Sass team's explicit knowledge and approval. See 7 | // https://github.com/sass/dart-sass/issues/236. 8 | import 'package:sass/src/ast/sass.dart'; 9 | 10 | import '../lint.dart'; 11 | import '../rule.dart'; 12 | 13 | /// A lint rule that reports on using null in a binary expression. 14 | /// 15 | /// In the Sass language, `null` is falsey, meaning binary operation expressions 16 | /// treat it as `false`. For brevity, `@if index(...) { ... }` is preferred to 17 | /// `@if index(...) != null { ... }, and `@if not index(...) { ... }` is 18 | /// preferred to `@if index(...) == null { ... }`. 19 | class UseFalseyNullRule extends Rule { 20 | String _equalityMessage(Expression otherSide) => 21 | 'Check for equality to null is unnecessarily explicit; ' 22 | 'prefer "not ${otherSide.span.text}", since null is "falsey", ' 23 | 'meaning it is equivalent to false in boolean expressions.'; 24 | 25 | static final _inequalityMessage = 26 | '"!= null" is unnecessary; null is "falsey" in Sass, ' 27 | 'meaning it is equivalent to false in boolean expressions.'; 28 | 29 | UseFalseyNullRule() : super('use_falsey_null_rule'); 30 | 31 | @override 32 | List visitBinaryOperationExpression(BinaryOperationExpression node) { 33 | var lint = []; 34 | // This rule concerns itself with `null` as an expression on either side of 35 | // `==` or `!=`. Using `null` as an expression on either side of other 36 | // binary operations is also strange (`7 < null`, but a very different 37 | // issue). 38 | if (node.operator == BinaryOperator.equals) { 39 | if (node.left is NullExpression) { 40 | lint.add(Lint( 41 | rule: this, 42 | span: node.left.span, 43 | message: _equalityMessage(node.right))); 44 | } 45 | if (node.right is NullExpression) { 46 | lint.add(Lint( 47 | rule: this, 48 | span: node.right.span, 49 | message: _equalityMessage(node.left))); 50 | } 51 | } else if (node.operator == BinaryOperator.notEquals) { 52 | if (node.left is NullExpression) { 53 | lint.add(Lint( 54 | rule: this, span: node.left.span, message: _inequalityMessage)); 55 | } 56 | if (node.right is NullExpression) { 57 | lint.add(Lint( 58 | rule: this, span: node.right.span, message: _inequalityMessage)); 59 | } 60 | } 61 | 62 | return lint..addAll(super.visitBinaryOperationExpression(node)); 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /lib/src/rules/use_scale_color.dart: -------------------------------------------------------------------------------- 1 | // Copyright 2018 Google Inc. Use of this source code is governed by an 2 | // MIT-style license that can be found in the LICENSE file or at 3 | // https://opensource.org/licenses/MIT. 4 | 5 | // The sass package's API is not necessarily stable. It is being imported with 6 | // the Sass team's explicit knowledge and approval. See 7 | // https://github.com/sass/dart-sass/issues/236. 8 | import 'package:sass/src/ast/sass.dart'; 9 | 10 | import '../lint.dart'; 11 | import '../rule.dart'; 12 | 13 | /// A lint rule that reports on the use of older, non-scaling color adjustment 14 | /// functions. 15 | /// 16 | /// These functions each add/subtract a number to/from some color property or 17 | /// other, rather than scaling the property. This is especially confusing when 18 | /// a user passes a _percentage_ as an argument, such as `darken($color, 30%)`. 19 | /// 20 | /// The `scale-color()` function should be used instead. 21 | class UseScaleColorRule extends Rule { 22 | /// The set of non-scaling functions to be avoided. 23 | static final _oldFunctions = Set.of([ 24 | 'saturate', 25 | 'desaturate', 26 | 'darken', 27 | 'lighten', 28 | 'opacify', 29 | 'fade-in', 30 | 'transparentize', 31 | 'fade-out' 32 | ]); 33 | 34 | UseScaleColorRule() : super('use_scale_color_rule'); 35 | 36 | @override 37 | List visitFunctionExpression(FunctionExpression node) { 38 | if (node.name.contents.length > 1) return []; 39 | var name = node.name.contents.single; 40 | if (_oldFunctions.contains(name)) { 41 | var documentationUrl = 42 | 'https://sass-lang.com/documentation/functions/color#$name'; 43 | return [ 44 | new Lint( 45 | rule: this, 46 | span: node.span, 47 | message: 48 | '"$name" is a non-scaling function; use scale-color instead. ' 49 | 'See $documentationUrl for more information.') 50 | ]; 51 | } else { 52 | return []; 53 | } 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /pubspec.yaml: -------------------------------------------------------------------------------- 1 | name: sass_linter 2 | version: 0.0.1-dev 3 | description: A Sass linter 4 | author: Dart Team 5 | 6 | environment: 7 | sdk: '>=2.0.0 <3.0.0' 8 | 9 | dependencies: 10 | args: "^1.4.0" 11 | charcode: "^1.0.0" 12 | meta: "^1.0.0" 13 | sass: "^1.14.3" 14 | source_span: "^1.4.0" 15 | yaml: "^2.1.0" 16 | 17 | dev_dependencies: 18 | path: "^1.6.0" 19 | test: ">=0.12.29 <2.0.0" 20 | test_descriptor: "^1.0.0" 21 | test_process: "^1.0.0" 22 | -------------------------------------------------------------------------------- /test/all_test.dart: -------------------------------------------------------------------------------- 1 | // Copyright 2018 Google Inc. Use of this source code is governed by an 2 | // MIT-style license that can be found in the LICENSE file or at 3 | // https://opensource.org/licenses/MIT. 4 | 5 | import 'rule_test.dart' as rule_test; 6 | import 'rules/no_debug_test.dart' as no_debug_test; 7 | 8 | // This file is here for easy coverage gathering; one file to execute with 9 | // `dart`. 10 | void main() { 11 | rule_test.main(); 12 | no_debug_test.main(); 13 | } 14 | -------------------------------------------------------------------------------- /test/cli_test.dart: -------------------------------------------------------------------------------- 1 | // Copyright 2018 Google Inc. Use of this source code is governed by an 2 | // MIT-style license that can be found in the LICENSE file or at 3 | // https://opensource.org/licenses/MIT. 4 | 5 | import 'dart:async'; 6 | import 'dart:io'; 7 | 8 | import 'package:path/path.dart' as p; 9 | import 'package:test_descriptor/test_descriptor.dart' as d; 10 | import 'package:test_process/test_process.dart'; 11 | import 'package:test/test.dart'; 12 | 13 | void main() { 14 | test('prints usage when no args are passed', () async { 15 | var linter = await runLinter([]); 16 | expect(linter.stdout, emits('Report lint found in Sass')); 17 | await linter.shouldExit(64); 18 | }); 19 | 20 | test('prints usage when "--help" is passed', () async { 21 | var linter = await runLinter(['--help']); 22 | expect(linter.stdout, emits('Report lint found in Sass')); 23 | await linter.shouldExit(64); 24 | }); 25 | 26 | test('lints a file', () async { 27 | await d.file('a.scss', r'$red: #ff0000;').create(); 28 | var linter = await runLinter(['a.scss']); 29 | expect(linter.stdout, emitsDone); 30 | await linter.shouldExit(0); 31 | }); 32 | 33 | test('reports lint found in a file', () async { 34 | await d.file('a.scss', '@debug("here");').create(); 35 | var linter = await runLinter(['a.scss']); 36 | expect( 37 | linter.stdout, 38 | emits('@debug directives should be removed. ' 39 | 'at a.scss line 1 (no_debug_rule)')); 40 | await linter.shouldExit(0); 41 | }); 42 | 43 | test('reports lint found in a directory', () async { 44 | await d.dir('directory', [ 45 | d.file('a.scss', '@debug("here");'), 46 | d.file('b.scss', '@debug("there");'), 47 | ]).create(); 48 | var linter = await runLinter(['directory']); 49 | expect( 50 | linter.stdout, 51 | emitsInAnyOrder([ 52 | contains('at directory/a.scss line 1 (no_debug_rule)'), 53 | contains('at directory/b.scss line 1 (no_debug_rule)'), 54 | ])); 55 | await linter.shouldExit(0); 56 | }); 57 | 58 | test('lints source from stdin', () async { 59 | var linter = await runLinter(['-']); 60 | linter.stdin.writeln(r'$red: #ff0000;'); 61 | linter.stdin.close(); 62 | expect(linter.stdout, emitsDone); 63 | await linter.shouldExit(0); 64 | }); 65 | 66 | test('reports lint found in stdin using "-"', () async { 67 | var linter = await runLinter(['-']); 68 | linter.stdin.writeln('@debug("here");'); 69 | linter.stdin.close(); 70 | expect( 71 | linter.stdout, 72 | emits('@debug directives should be removed. ' 73 | 'at [missing url] line 1 (no_debug_rule)')); 74 | await linter.shouldExit(0); 75 | }); 76 | 77 | test('reports lint found in stdin using "--stdin"', () async { 78 | var linter = await runLinter(['--stdin']); 79 | linter.stdin.writeln('@debug("here");'); 80 | linter.stdin.close(); 81 | expect( 82 | linter.stdout, 83 | emits('@debug directives should be removed. ' 84 | 'at [missing url] line 1 (no_debug_rule)')); 85 | await linter.shouldExit(0); 86 | }); 87 | 88 | test('reports lint with a path using "--stdin-file-url"', () async { 89 | var linter = await runLinter(['--stdin-file-url=/a/b/c.scss', '-']); 90 | linter.stdin.writeln('@debug("here");'); 91 | linter.stdin.close(); 92 | expect( 93 | linter.stdout, 94 | emits('@debug directives should be removed. ' 95 | 'at /a/b/c.scss line 1 (no_debug_rule)')); 96 | await linter.shouldExit(0); 97 | }); 98 | 99 | test('reports lint for a single specified lint rule', () async { 100 | await d.file('a.scss', '@debug("here");').create(); 101 | var linter = await runLinter(['--rules', 'no_debug_rule', 'a.scss']); 102 | expect( 103 | linter.stdout, 104 | emits('@debug directives should be removed. ' 105 | 'at a.scss line 1 (no_debug_rule)')); 106 | await linter.shouldExit(0); 107 | }); 108 | 109 | test('does not report lint for an unspecified lint rule', () async { 110 | await d.file('a.scss', '@debug("here");').create(); 111 | var linter = await runLinter(['--rules', 'no_empty_style_rule', 'a.scss']); 112 | expect(linter.stdout, emitsDone); 113 | await linter.shouldExit(0); 114 | }); 115 | 116 | test('reports lint for multiple specified lint rules', () async { 117 | await d.file('a.scss', 'p {}\n@debug("here");').create(); 118 | var linter = await runLinter( 119 | ['--rules', 'no_debug_rule,no_empty_style_rule', 'a.scss']); 120 | expect( 121 | linter.stdout, 122 | emitsInOrder([ 123 | '@debug directives should be removed. ' 124 | 'at a.scss line 2 (no_debug_rule)', 125 | 'Style rule is empty. at a.scss line 1 (no_empty_style_rule)', 126 | ])); 127 | await linter.shouldExit(0); 128 | }); 129 | 130 | test('allows users to drop "_rule" suffix in `--rules` arg', () async { 131 | await d.file('a.scss', 'p {}\n@debug("here");').create(); 132 | var linter = 133 | await runLinter(['--rules', 'no_debug,no_empty_style', 'a.scss']); 134 | expect( 135 | linter.stdout, 136 | emitsInOrder([ 137 | '@debug directives should be removed. ' 138 | 'at a.scss line 2 (no_debug_rule)', 139 | 'Style rule is empty. at a.scss line 1 (no_empty_style_rule)', 140 | ])); 141 | await linter.shouldExit(0); 142 | }); 143 | 144 | test('prints an error when an unkwown rule is passed', () async { 145 | await d.file('a.scss', '@debug("here");').create(); 146 | var linter = await runLinter(['--rules', 'not_a_rule', 'a.scss']); 147 | expect(linter.stdout, emits('Unknown rule: not_a_rule')); 148 | await linter.shouldExit(64); 149 | }); 150 | 151 | test('reports lint for rule specified in config', () async { 152 | var configSource = r''' 153 | rules: 154 | - no_debug_rule 155 | - no_loud_comment_rule 156 | '''; 157 | await d.file('a.yaml', configSource).create(); 158 | await d.file('a.scss', '@debug("here");').create(); 159 | var linter = await runLinter(['--config', '${d.sandbox}/a.yaml', 'a.scss']); 160 | 161 | expect( 162 | linter.stdout, 163 | emits('@debug directives should be removed. ' 164 | 'at a.scss line 1 (no_debug_rule)')); 165 | await linter.shouldExit(0); 166 | }); 167 | 168 | test('does not report lint for rule not specified in config', () async { 169 | var configSource = r''' 170 | rules: 171 | - no_loud_comment_rule 172 | '''; 173 | await d.file('a.yaml', configSource).create(); 174 | await d.file('a.scss', '@debug("here");').create(); 175 | var linter = await runLinter(['--config', '${d.sandbox}/a.yaml', 'a.scss']); 176 | 177 | expect(linter.stdout, emitsDone); 178 | await linter.shouldExit(0); 179 | }); 180 | 181 | test('reports lint when no rules specified in config', () async { 182 | await d.file('a.yaml', '').create(); 183 | await d.file('a.scss', '@debug("here");').create(); 184 | var linter = await runLinter(['--config', '${d.sandbox}/a.yaml', 'a.scss']); 185 | 186 | expect( 187 | linter.stdout, 188 | emits('@debug directives should be removed. ' 189 | 'at a.scss line 1 (no_debug_rule)')); 190 | await linter.shouldExit(0); 191 | }); 192 | 193 | test('ignores paths in config when paths passed at cmdline', () async { 194 | var configSource = r''' 195 | paths: 196 | - b.scss 197 | '''; 198 | await d.file('a.yaml', configSource).create(); 199 | await d.file('a.scss', '@debug("here");').create(); 200 | await d.file('b.scss', '@debug("here");').create(); 201 | var linter = await runLinter(['--config', '${d.sandbox}/a.yaml', 'a.scss']); 202 | 203 | expect( 204 | linter.stdout, 205 | emits('@debug directives should be removed. ' 206 | 'at a.scss line 1 (no_debug_rule)')); 207 | expect(linter.stdout, emitsDone); 208 | await linter.shouldExit(0); 209 | }); 210 | 211 | test('reports lint for paths in config when no paths passed at cmdline', 212 | () async { 213 | var configSource = r''' 214 | paths: 215 | - a.scss 216 | - b.scss 217 | '''; 218 | await d.file('a.yaml', configSource).create(); 219 | await d.file('a.scss', '@debug("here");').create(); 220 | await d.file('b.scss', '@debug("here");').create(); 221 | var linter = await runLinter(['--config', '${d.sandbox}/a.yaml']); 222 | 223 | expect( 224 | linter.stdout, 225 | emitsInAnyOrder([ 226 | contains('at a.scss line 1 (no_debug_rule)'), 227 | contains('at b.scss line 1 (no_debug_rule)'), 228 | ])); 229 | expect(linter.stdout, emitsDone); 230 | await linter.shouldExit(0); 231 | }); 232 | 233 | test('reports when a rule from config cannot be parsed', () async { 234 | var configSource = r''' 235 | rules: 236 | - unknown_rule 237 | '''; 238 | await d.file('a.yaml', configSource).create(); 239 | await d.file('a.scss', '@debug("here");').create(); 240 | var linter = await runLinter(['--config', '${d.sandbox}/a.yaml', 'a.scss']); 241 | 242 | expect(linter.stdout, 243 | emits('Error on line 2, column 5 of a.yaml: Unknown rule')); 244 | await linter.shouldExit(255); 245 | }); 246 | 247 | test('reports when the config cannot be parsed as YAML', () async { 248 | var configSource = r''' 249 | 1.2: 2.3 250 | a::b 251 | '''; 252 | await d.file('a.yaml', configSource).create(); 253 | await d.file('a.scss', '@debug("here");').create(); 254 | var linter = await runLinter(['--config', '${d.sandbox}/a.yaml', 'a.scss']); 255 | 256 | expect( 257 | linter.stdout, emits(contains('Error on line 3, column 1 of a.yaml'))); 258 | await linter.shouldExit(255); 259 | }); 260 | 261 | test('reports when the config is not a YAML map', () async { 262 | var configSource = r''' 263 | - no_debug_rule 264 | - no_loud_comment_rule 265 | '''; 266 | await d.file('a.yaml', configSource).create(); 267 | await d.file('a.scss', '@debug("here");').create(); 268 | var linter = await runLinter(['--config', '${d.sandbox}/a.yaml', 'a.scss']); 269 | 270 | expect(linter.stdout, 271 | emits(contains('The configuration file must be a Yaml map.'))); 272 | await linter.shouldExit(255); 273 | }); 274 | 275 | test('reports when the config values cannot be parsed', () async { 276 | var configSource = r''' 277 | rules: no_debug_rule, no_loud_comment_rule 278 | '''; 279 | await d.file('a.yaml', configSource).create(); 280 | await d.file('a.scss', '@debug("here");').create(); 281 | var linter = await runLinter(['--config', '${d.sandbox}/a.yaml', 'a.scss']); 282 | 283 | expect(linter.stdout, 284 | emits(contains('"rules" in the configuration file must be a List.'))); 285 | await linter.shouldExit(255); 286 | }); 287 | } 288 | 289 | Future runLinter(Iterable arguments) => TestProcess.start( 290 | Platform.executable, 291 | ['--checked', p.absolute('bin/sass_linter.dart')]..addAll(arguments), 292 | workingDirectory: d.sandbox, 293 | description: 'sass_linter'); 294 | -------------------------------------------------------------------------------- /test/configuration_test.dart: -------------------------------------------------------------------------------- 1 | // Copyright 2018 Google Inc. Use of this source code is governed by an 2 | // MIT-style license that can be found in the LICENSE file or at 3 | // https://opensource.org/licenses/MIT. 4 | 5 | import 'package:sass_linter/src/configuration.dart'; 6 | import 'package:sass_linter/src/exceptions.dart'; 7 | import 'package:sass_linter/src/rules/no_debug.dart'; 8 | import 'package:sass_linter/src/rules/no_loud_comment.dart'; 9 | import 'package:test_descriptor/test_descriptor.dart' as d; 10 | import 'package:test/test.dart'; 11 | import 'package:yaml/yaml.dart'; 12 | 13 | void main() { 14 | group('empty configuration', () { 15 | var configuration = Configuration.empty; 16 | 17 | test('has null (unspecified) value for rules', () { 18 | expect(configuration.rules, isNull); 19 | }); 20 | 21 | test('has empty (no specified) value for rules', () { 22 | expect(configuration.paths, isEmpty); 23 | }); 24 | }); 25 | 26 | test('configuration can be parsed from empty file', () async { 27 | await d.file('a.yaml', '').create(); 28 | var configuration = Configuration.parse('${d.sandbox}/a.yaml'); 29 | expect(configuration.rules, isNull); 30 | expect(configuration.paths, isEmpty); 31 | }); 32 | 33 | test('configuration can be parsed from file with single rule', () async { 34 | var configSource = r''' 35 | rules: 36 | - no_debug_rule 37 | '''; 38 | await d.file('a.yaml', configSource).create(); 39 | var configuration = Configuration.parse('${d.sandbox}/a.yaml'); 40 | expect(configuration.rules, contains(TypeMatcher())); 41 | expect(configuration.paths, isEmpty); 42 | }); 43 | 44 | test('configuration can be parsed from file with single path', () async { 45 | var configSource = r''' 46 | paths: 47 | - foo/bar.scss 48 | '''; 49 | await d.file('a.yaml', configSource).create(); 50 | var configuration = Configuration.parse('${d.sandbox}/a.yaml'); 51 | expect(configuration.rules, isNull); 52 | expect(configuration.paths, contains('foo/bar.scss')); 53 | }); 54 | 55 | test('configuration can be parsed from file with rules and paths', () async { 56 | var configSource = r''' 57 | rules: 58 | - no_debug_rule 59 | - no_loud_comment_rule 60 | paths: 61 | - foo/bar.scss 62 | - foo/baz.scss 63 | '''; 64 | await d.file('a.yaml', configSource).create(); 65 | var configuration = Configuration.parse('${d.sandbox}/a.yaml'); 66 | expect(configuration.rules, hasLength(2)); 67 | expect(configuration.rules, contains(TypeMatcher())); 68 | expect(configuration.rules, contains(TypeMatcher())); 69 | expect(configuration.paths, hasLength(2)); 70 | expect(configuration.paths, contains('foo/bar.scss')); 71 | expect(configuration.paths, contains('foo/baz.scss')); 72 | }); 73 | 74 | test( 75 | 'configuration can be parsed from file with specified but "null" rules ' 76 | 'and paths', () async { 77 | var configSource = r''' 78 | rules: 79 | paths: 80 | '''; 81 | await d.file('a.yaml', configSource).create(); 82 | var configuration = Configuration.parse('${d.sandbox}/a.yaml'); 83 | expect(configuration.rules, isNull); 84 | expect(configuration.paths, isEmpty); 85 | }); 86 | 87 | test('throws when a configuration file cannot be parsed as YAML', () async { 88 | await _assertError(r''' 89 | 1.2: 2.3 90 | a::b 91 | '''); 92 | }); 93 | 94 | test('throws when a configuration file cannot be parsed as a map', () async { 95 | await _assertError(r''' 96 | - no_debug_rule 97 | - no_loud_comment_rule 98 | '''); 99 | }); 100 | 101 | test('throws when a configuration file cannot be parsed', () async { 102 | await _assertError(r''' 103 | rules: 104 | - unknown_rule 105 | '''); 106 | }); 107 | 108 | test('throws when the value of "rules" is not a list', () async { 109 | await _assertError(r''' 110 | rules: 7 111 | '''); 112 | }); 113 | 114 | test('throws when the value of "paths" is not a list', () async { 115 | await _assertError(r''' 116 | paths: 7 117 | '''); 118 | }); 119 | 120 | test('throws when the values of the "rules" list are not strings', () async { 121 | await _assertError(r''' 122 | rules: 123 | - no_debug_rule: foo 124 | bar: false 125 | '''); 126 | }); 127 | 128 | test('throws when the values of the "paths" list are not strings', () async { 129 | await _assertError(r''' 130 | paths: 131 | - foo.scss: true 132 | bar: false 133 | '''); 134 | }); 135 | } 136 | 137 | Future _assertError(String configSource) async { 138 | await d.file('a.yaml', configSource).create(); 139 | expect(() => Configuration.parse('${d.sandbox}/a.yaml'), 140 | throwsA(TypeMatcher())); 141 | } 142 | -------------------------------------------------------------------------------- /test/engine_test.dart: -------------------------------------------------------------------------------- 1 | // Copyright 2018 Google Inc. Use of this source code is governed by an 2 | // MIT-style license that can be found in the LICENSE file or at 3 | // https://opensource.org/licenses/MIT. 4 | 5 | import 'package:path/path.dart' as p; 6 | import 'package:sass_linter/src/engine.dart'; 7 | import 'package:test/test.dart'; 8 | import 'package:test_descriptor/test_descriptor.dart' as d; 9 | 10 | void main() { 11 | test('runs the linter against a single file', () async { 12 | await d.file('a.scss', ''' 13 | @debug("debug message one here"); 14 | @debug("debug message two here"); 15 | ''').create(); 16 | 17 | var path = p.join(d.sandbox, 'a.scss'); 18 | 19 | var engine = new Engine([path]); 20 | var lints = engine.run().toList(); 21 | 22 | expect(lints, hasLength(2)); 23 | expect(lints[0].url.path, equals(path)); 24 | expect(lints[1].url.path, equals(path)); 25 | // The engine returns 0-indexed line numbers. 26 | expect(lints[0].line, equals(0)); 27 | expect(lints[1].line, equals(1)); 28 | }); 29 | 30 | test('runs the linter against multiple files', () async { 31 | await d.file('a.scss', ''' 32 | @debug("message one here"); 33 | ''').create(); 34 | await d.file('b.scss', ''' 35 | @debug("message two here"); 36 | ''').create(); 37 | 38 | var pathA = p.join(d.sandbox, 'a.scss'); 39 | var pathB = p.join(d.sandbox, 'b.scss'); 40 | var engine = new Engine([pathA, pathB]); 41 | var lints = engine.run().toList(); 42 | 43 | expect(lints, hasLength(2)); 44 | expect(lints[0].url.path, equals(pathA)); 45 | expect(lints[1].url.path, equals(pathB)); 46 | // The engine returns 0-indexed line numbers. 47 | expect(lints[0].line, equals(0)); 48 | expect(lints[1].line, equals(0)); 49 | }); 50 | 51 | test('runs the linter against a single directory', () async { 52 | await d.dir('parent', [ 53 | d.file('a.scss', ''' 54 | @debug("debug message one here"); 55 | @debug("debug message two here"); 56 | '''), 57 | d.dir('child', [ 58 | d.file('b.scss', ''' 59 | @debug("debug message three here"); 60 | '''), 61 | ]), 62 | ]).create(); 63 | 64 | var parentPath = p.join(d.sandbox, 'parent'); 65 | var pathA = p.join(parentPath, 'a.scss'); 66 | var pathB = p.join(parentPath, 'child', 'b.scss'); 67 | 68 | var engine = new Engine([parentPath]); 69 | var lints = engine.run().toList(); 70 | 71 | expect(lints, hasLength(3)); 72 | expect(lints[0].url.path, equals(pathA)); 73 | expect(lints[1].url.path, equals(pathA)); 74 | expect(lints[2].url.path, equals(pathB)); 75 | }); 76 | 77 | test('skips non-Sass files', () async { 78 | await d.dir('parent', [ 79 | d.file('a.txt', ''' 80 | // This file is not Sass. But what if it really looked like Sass? 81 | @debug("debug message one here"); 82 | '''), 83 | ]).create(); 84 | 85 | var path = p.join(d.sandbox, 'parent'); 86 | var engine = new Engine([path]); 87 | expect(engine.run(), isEmpty); 88 | }); 89 | } 90 | -------------------------------------------------------------------------------- /test/rule_test.dart: -------------------------------------------------------------------------------- 1 | // Copyright 2018 Google Inc. Use of this source code is governed by an 2 | // MIT-style license that can be found in the LICENSE file or at 3 | // https://opensource.org/licenses/MIT. 4 | 5 | import 'package:sass/src/ast/sass.dart'; 6 | import 'package:sass_linter/src/lint.dart'; 7 | import 'package:sass_linter/src/linter.dart'; 8 | import 'package:sass_linter/src/rule.dart'; 9 | import 'package:test/test.dart'; 10 | 11 | /// A configurable linter rule. 12 | /// 13 | /// This rule only overrides a few leaf visit methods. This rule can be used in 14 | /// tests to verify that [Rule]'s visit methods walk down the AST, and report 15 | /// lint back up. 16 | class _DummyRule extends Rule { 17 | final bool boolean; 18 | final bool number; 19 | final bool variable; 20 | 21 | _DummyRule({this.boolean = false, this.number = false, this.variable = false}) 22 | : super('dummy_rule'); 23 | 24 | @override 25 | visitBooleanExpression(BooleanExpression node) { 26 | return boolean 27 | ? [Lint(rule: this, span: node.span, message: 'Found a boolean.')] 28 | : []; 29 | } 30 | 31 | @override 32 | visitNumberExpression(NumberExpression node) { 33 | return number 34 | ? [Lint(rule: this, span: node.span, message: 'Found a number.')] 35 | : []; 36 | } 37 | 38 | @override 39 | visitVariableExpression(VariableExpression node) { 40 | return variable 41 | ? [Lint(rule: this, span: node.span, message: 'Found a variable.')] 42 | : []; 43 | } 44 | } 45 | 46 | final url = 'a.scss'; 47 | final booleanRule = new _DummyRule(boolean: true); 48 | final numberRule = new _DummyRule(number: true); 49 | final variableRule = new _DummyRule(variable: true); 50 | 51 | List getBooleanLints(String source) => 52 | new Linter(source, [booleanRule], url: url).run(); 53 | 54 | List getNumberLints(String source) => 55 | new Linter(source, [numberRule], url: url).run(); 56 | 57 | List getVariableLints(String source) => 58 | new Linter(source, [variableRule], url: url).run(); 59 | 60 | void main() { 61 | test('reports lint found within an @at-root query', () { 62 | var lints = getNumberLints(r''' 63 | .parent { 64 | @at-root .child-#{1} { 65 | a: red; 66 | } 67 | } 68 | '''); 69 | 70 | expect(lints, hasLength(1)); 71 | 72 | var lint = lints.single; 73 | expect(lint.line, 1); 74 | expect(lint.column, 28); 75 | }); 76 | 77 | test('reports lint found within an @at-root body', () { 78 | var lints = getBooleanLints(r''' 79 | .parent { 80 | @at-root .child { 81 | $a: true; 82 | } 83 | } 84 | '''); 85 | 86 | expect(lints, hasLength(1)); 87 | 88 | var lint = lints.single; 89 | expect(lint.line, 2); 90 | expect(lint.column, 16); 91 | }); 92 | 93 | test('reports lint found within a binary operation', () { 94 | var lints = getNumberLints(r'$a: 1 + 2;'); 95 | 96 | expect(lints, hasLength(2)); 97 | 98 | expect(lints[0].line, 0); 99 | expect(lints[0].column, 4); 100 | expect(lints[1].line, 0); 101 | expect(lints[1].column, 8); 102 | }); 103 | 104 | test('reports lint found within a @debug at-rule', () { 105 | var lints = getBooleanLints(r'@debug true;'); 106 | 107 | expect(lints, hasLength(1)); 108 | 109 | var lint = lints.single; 110 | expect(lint.message, equals('Found a boolean.')); 111 | expect(lint.url, new Uri.file(url)); 112 | expect(lint.line, 0); 113 | expect(lint.column, 7); 114 | }); 115 | 116 | test('reports lint found within a declaration name', () { 117 | var lints = getBooleanLints(r''' 118 | p { 119 | foo-#{true}: red; 120 | } 121 | '''); 122 | 123 | expect(lints, hasLength(1)); 124 | 125 | var lint = lints.single; 126 | expect(lint.line, 1); 127 | expect(lint.column, 16); 128 | }); 129 | 130 | test('reports lint found within a declaration value', () { 131 | var lints = getBooleanLints(r''' 132 | p { 133 | foo: true; 134 | } 135 | '''); 136 | 137 | expect(lints, hasLength(1)); 138 | 139 | var lint = lints.single; 140 | expect(lint.line, 1); 141 | expect(lint.column, 15); 142 | }); 143 | 144 | test('reports lint found within an @each list', () { 145 | var lints = getBooleanLints(r''' 146 | @each $animal in true, false { 147 | $a: red; 148 | } 149 | '''); 150 | 151 | expect(lints, hasLength(2)); 152 | 153 | expect(lints[0].line, 0); 154 | expect(lints[0].column, 25); 155 | expect(lints[1].line, 0); 156 | expect(lints[1].column, 31); 157 | }); 158 | 159 | test('reports lint found within an @each loop body', () { 160 | var lints = getBooleanLints(r''' 161 | @each $animal in puma, sea-slug, egret, salamander { 162 | $a: true; 163 | } 164 | '''); 165 | 166 | expect(lints, hasLength(1)); 167 | 168 | var lint = lints.single; 169 | expect(lint.line, 1); 170 | expect(lint.column, 14); 171 | }); 172 | 173 | test('reports lint found within an @error at-rule', () { 174 | var lints = getBooleanLints(r'@error true;'); 175 | 176 | expect(lints, hasLength(1)); 177 | 178 | var lint = lints.single; 179 | expect(lint.message, equals('Found a boolean.')); 180 | expect(lint.url, new Uri.file(url)); 181 | expect(lint.line, 0); 182 | expect(lint.column, 7); 183 | }); 184 | 185 | test('reports lint found within an @extend selector', () { 186 | var lints = getNumberLints(r''' 187 | .error-1 { 188 | color: red; 189 | } 190 | 191 | .seriousError { 192 | @extend .error-#{1}; 193 | font-weight: bold; 194 | } 195 | '''); 196 | 197 | expect(lints, hasLength(1)); 198 | 199 | var lint = lints.single; 200 | expect(lint.line, 5); 201 | expect(lint.column, 27); 202 | }); 203 | 204 | test('reports lint found within a @for condition', () { 205 | var lints = getNumberLints(r''' 206 | @for $i from 1 through 3 { 207 | $a: red; 208 | } 209 | '''); 210 | 211 | expect(lints, hasLength(2)); 212 | 213 | expect(lints[0].line, 0); 214 | expect(lints[0].column, 21); 215 | expect(lints[1].line, 0); 216 | expect(lints[1].column, 31); 217 | }); 218 | 219 | test('reports lint found within a @for body', () { 220 | var lints = getBooleanLints(r''' 221 | @for $i from 1 through 3 { 222 | $a: true; 223 | } 224 | '''); 225 | 226 | expect(lints, hasLength(1)); 227 | 228 | var lint = lints.last; 229 | expect(lint.line, 1); 230 | expect(lint.column, 14); 231 | }); 232 | 233 | test('reports lint found within a @function body', () { 234 | var lints = getBooleanLints(r''' 235 | @function grid-width($n) { 236 | $a: true; 237 | @return $a; 238 | } 239 | '''); 240 | 241 | expect(lints, hasLength(1)); 242 | 243 | var lint = lints.single; 244 | expect(lint.line, 1); 245 | expect(lint.column, 14); 246 | }); 247 | 248 | test('reports lint found within a function invocation name', () { 249 | var lints = getNumberLints(r''' 250 | p { 251 | // Imagine CSS provides a function, "attr2". 252 | color: attr#{2}("data-color"); 253 | } 254 | '''); 255 | 256 | expect(lints, hasLength(1)); 257 | 258 | var lint = lints.single; 259 | expect(lint.line, 2); 260 | expect(lint.column, 23); 261 | }); 262 | 263 | test('reports lint found within a function invocation positional arg', () { 264 | var lints = getNumberLints(r''' 265 | $my-red: darken(red, 10%); 266 | '''); 267 | 268 | expect(lints, hasLength(1)); 269 | 270 | var lint = lints.single; 271 | expect(lint.line, 0); 272 | expect(lint.column, 29); 273 | }); 274 | 275 | test('reports lint found within a function invocation keyword arg', () { 276 | var lints = getNumberLints(r''' 277 | $my-red: darken(red, $amount: 10%); 278 | '''); 279 | 280 | expect(lints, hasLength(1)); 281 | 282 | var lint = lints.single; 283 | expect(lint.line, 0); 284 | expect(lint.column, 38); 285 | }); 286 | 287 | test('reports lint found within a function invocation varargs', () { 288 | var lints = getVariableLints(r''' 289 | $args: red 10%; 290 | $my-red: darken($args...); 291 | '''); 292 | 293 | expect(lints, hasLength(1)); 294 | 295 | var lint = lints.single; 296 | expect(lint.line, 1); 297 | expect(lint.column, 24); 298 | }); 299 | 300 | test('reports lint found within a function invocation keyword varargs', () { 301 | var lints = getVariableLints(r''' 302 | $attrs: (hue: 0, saturation: 0); 303 | $my-red: adjust-color(red, (1, 2, 3)..., $attrs...); 304 | '''); 305 | 306 | expect(lints, hasLength(1)); 307 | 308 | var lint = lints.single; 309 | expect(lint.line, 1); 310 | expect(lint.column, 49); 311 | }); 312 | 313 | test('reports lint found within an @if clause expression', () { 314 | var lints = getBooleanLints(r''' 315 | p { 316 | @if true { 317 | $a: red; 318 | } 319 | } 320 | '''); 321 | 322 | expect(lints, hasLength(1)); 323 | 324 | var lint = lints.single; 325 | expect(lint.line, 1); 326 | expect(lint.column, 14); 327 | }); 328 | 329 | test('reports lint found within an @if else clause body', () { 330 | var lints = getBooleanLints(r''' 331 | $type: monster; 332 | p { 333 | @if $type == ocean { 334 | $a: red; 335 | } @else { 336 | $a: true; 337 | } 338 | } 339 | '''); 340 | 341 | expect(lints, hasLength(1)); 342 | 343 | var lint = lints.single; 344 | expect(lint.line, 5); 345 | expect(lint.column, 16); 346 | }); 347 | 348 | test('reports lint found within an @if clause body', () { 349 | var lints = getBooleanLints(r''' 350 | $type: monster; 351 | p { 352 | @if $type == ocean { 353 | $a: true; 354 | } 355 | } 356 | '''); 357 | 358 | expect(lints, hasLength(1)); 359 | 360 | var lint = lints.single; 361 | expect(lint.line, 3); 362 | expect(lint.column, 16); 363 | }); 364 | 365 | test('reports lint found within a map literal', () { 366 | var lints = getBooleanLints(r''' 367 | $map: ( 368 | key1: false, 369 | true: value2, 370 | ); 371 | '''); 372 | 373 | expect(lints, hasLength(2)); 374 | 375 | var boolInValue = lints[0]; 376 | expect(boolInValue.line, 1); 377 | expect(boolInValue.column, 11); 378 | 379 | var boolInKey = lints[1]; 380 | expect(boolInKey.line, 2); 381 | expect(boolInKey.column, 5); 382 | }); 383 | 384 | test('reports lint found within a parenthesized expression', () { 385 | var lints = getBooleanLints(r''' 386 | @if foo or 387 | (bar and false) or 388 | (true) { 389 | p: {color: red; } 390 | } 391 | '''); 392 | 393 | expect(lints, hasLength(2)); 394 | 395 | var boolInBinary = lints[0]; 396 | expect(boolInBinary.line, 1); 397 | expect(boolInBinary.column, 17); 398 | 399 | var boolInParens = lints[1]; 400 | expect(boolInParens.line, 2); 401 | expect(boolInParens.column, 9); 402 | }); 403 | 404 | test('reports lint found within a style selector', () { 405 | var lints = getNumberLints(r''' 406 | p-#{1} { 407 | a: red; 408 | } 409 | '''); 410 | 411 | expect(lints, hasLength(1)); 412 | 413 | var lint = lints.single; 414 | expect(lint.line, 0); 415 | expect(lint.column, 12); 416 | }); 417 | 418 | test('reports lint found within a style rule', () { 419 | var lints = getBooleanLints(r''' 420 | p { 421 | $a: true; 422 | } 423 | '''); 424 | 425 | expect(lints, hasLength(1)); 426 | 427 | var lint = lints.single; 428 | expect(lint.line, 1); 429 | expect(lint.column, 14); 430 | }); 431 | 432 | test('reports lint found within a variable declaration', () { 433 | var lints = getBooleanLints(r'$a: true;'); 434 | 435 | expect(lints, hasLength(1)); 436 | 437 | var lint = lints.single; 438 | expect(lint.message, equals('Found a boolean.')); 439 | expect(lint.url, new Uri.file(url)); 440 | expect(lint.line, 0); 441 | expect(lint.column, 4); 442 | }); 443 | 444 | test('reports lint found within an @warn at-rule', () { 445 | var lints = getBooleanLints(r'@warn true;'); 446 | 447 | expect(lints, hasLength(1)); 448 | 449 | var lint = lints.single; 450 | expect(lint.message, equals('Found a boolean.')); 451 | expect(lint.url, new Uri.file(url)); 452 | expect(lint.line, 0); 453 | expect(lint.column, 6); 454 | }); 455 | } 456 | -------------------------------------------------------------------------------- /test/rules/no_debug_test.dart: -------------------------------------------------------------------------------- 1 | // Copyright 2018 Google Inc. Use of this source code is governed by an 2 | // MIT-style license that can be found in the LICENSE file or at 3 | // https://opensource.org/licenses/MIT. 4 | 5 | import 'package:sass_linter/src/rules/no_debug.dart'; 6 | import 'package:sass_linter/src/lint.dart'; 7 | import 'package:sass_linter/src/linter.dart'; 8 | import 'package:test/test.dart'; 9 | 10 | final url = 'a.scss'; 11 | final rule = new NoDebugRule(); 12 | 13 | void main() { 14 | test('does not report lint when no @debug directive is found', () { 15 | var lints = getLints(r'$red: #f00;'); 16 | 17 | expect(lints, isEmpty); 18 | }); 19 | 20 | test('reports lint when @debug directive is found', () { 21 | var lints = getLints(r'@debug 10em + 12em;'); 22 | 23 | expect(lints, hasLength(1)); 24 | 25 | var lint = lints.single; 26 | expect(lint.rule, rule); 27 | expect(lint.message, contains('@debug directives should be removed.')); 28 | expect(lint.url, new Uri.file(url)); 29 | expect(lint.line, 0); 30 | expect(lint.column, 0); 31 | }); 32 | } 33 | 34 | List getLints(String source) => 35 | new Linter(source, [rule], url: url).run(); 36 | -------------------------------------------------------------------------------- /test/rules/no_empty_style_test.dart: -------------------------------------------------------------------------------- 1 | // Copyright 2018 Google Inc. Use of this source code is governed by an 2 | // MIT-style license that can be found in the LICENSE file or at 3 | // https://opensource.org/licenses/MIT. 4 | 5 | import 'package:sass_linter/src/rules/no_empty_style.dart'; 6 | import 'package:sass_linter/src/lint.dart'; 7 | import 'package:sass_linter/src/linter.dart'; 8 | import 'package:test/test.dart'; 9 | 10 | final url = 'a.scss'; 11 | final rule = new NoEmptyStyleRule(); 12 | 13 | void main() { 14 | test('does not report lint when style rule has style declaration', () { 15 | var lints = getLints(r'p { color: red; }'); 16 | 17 | expect(lints, isEmpty); 18 | }); 19 | 20 | test('does not report lint when style rule has nested style rule', () { 21 | var lints = getLints('div.foo {\n' 22 | ' div.bar {\n' 23 | ' color: red;\n' 24 | ' }\n' 25 | '}'); 26 | 27 | expect(lints, isEmpty); 28 | }); 29 | 30 | test('reports lint when style rule has no style declaration', () { 31 | var lints = getLints('p {\n' 32 | ' \$red: #f00;\n' 33 | ' @debug("Hello.");\n' 34 | '}'); 35 | 36 | expect(lints, hasLength(1)); 37 | 38 | var lint = lints.single; 39 | expect(lint.rule, rule); 40 | expect(lint.message, contains('Style rule is empty.')); 41 | expect(lint.url, new Uri.file(url)); 42 | expect(lint.line, 0); 43 | expect(lint.column, 0); 44 | }); 45 | 46 | test('reports nested empty rules', () { 47 | var lints = getLints('div.foo {\n' 48 | ' div.bar {\n' 49 | ' }\n' 50 | ' div.baz {\n' 51 | ' }\n' 52 | '}'); 53 | 54 | expect(lints, hasLength(2)); 55 | 56 | var lintOnBar = lints[0]; 57 | expect(lintOnBar.rule, rule); 58 | expect(lintOnBar.message, contains('Style rule is empty.')); 59 | expect(lintOnBar.url, new Uri.file(url)); 60 | expect(lintOnBar.line, 1); 61 | expect(lintOnBar.column, 2); 62 | 63 | var lintOnBaz = lints[1]; 64 | expect(lintOnBaz.rule, rule); 65 | expect(lintOnBaz.message, contains('Style rule is empty.')); 66 | expect(lintOnBaz.url, new Uri.file(url)); 67 | expect(lintOnBaz.line, 3); 68 | expect(lintOnBaz.column, 2); 69 | }); 70 | } 71 | 72 | List getLints(String source) => 73 | new Linter(source, [rule], url: url).run(); 74 | -------------------------------------------------------------------------------- /test/rules/no_loud_comment_test.dart: -------------------------------------------------------------------------------- 1 | // Copyright 2018 Google Inc. Use of this source code is governed by an 2 | // MIT-style license that can be found in the LICENSE file or at 3 | // https://opensource.org/licenses/MIT. 4 | 5 | import 'package:sass_linter/src/rules/no_loud_comment.dart'; 6 | import 'package:sass_linter/src/lint.dart'; 7 | import 'package:sass_linter/src/linter.dart'; 8 | import 'package:test/test.dart'; 9 | 10 | final url = 'a.scss'; 11 | final rule = new NoLoudCommentRule(); 12 | 13 | void main() { 14 | test('does not report lint when no loud comment is found', () { 15 | var lints = getLints(r'// silent comment'); 16 | 17 | expect(lints, isEmpty); 18 | }); 19 | 20 | test('reports lint when loud comment is found', () { 21 | var lints = getLints(r'/* loud comment */'); 22 | 23 | expect(lints, hasLength(1)); 24 | 25 | var lint = lints.single; 26 | expect(lint.rule, rule); 27 | expect(lint.message, 28 | contains('Comments should be written with the silent (`//`) syntax.')); 29 | expect(lint.url, new Uri.file(url)); 30 | expect(lint.line, 0); 31 | expect(lint.column, 0); 32 | }); 33 | 34 | test('does not report lint when loud, **preserved** comment is found', () { 35 | var lints = getLints(r'/*! Copyright notice */'); 36 | 37 | expect(lints, isEmpty); 38 | }); 39 | } 40 | 41 | List getLints(String source) => 42 | new Linter(source, [rule], url: url).run(); 43 | -------------------------------------------------------------------------------- /test/rules/non_numeric_dimension_test.dart: -------------------------------------------------------------------------------- 1 | // Copyright 2018 Google Inc. Use of this source code is governed by an 2 | // MIT-style license that can be found in the LICENSE file or at 3 | // https://opensource.org/licenses/MIT. 4 | 5 | import 'package:sass_linter/src/rules/non_numeric_dimension.dart'; 6 | import 'package:sass_linter/src/lint.dart'; 7 | import 'package:sass_linter/src/linter.dart'; 8 | import 'package:test/test.dart'; 9 | 10 | final url = 'a.scss'; 11 | final rule = new NonNumericDimensionRule(); 12 | 13 | void main() { 14 | test('does not report lint when no dimension-interpolating is found', () { 15 | var lints = getLints(r'$pad: 2; $doublePad: $pad * 1px;'); 16 | 17 | expect(lints, isEmpty); 18 | }); 19 | 20 | test('does not report lint when no understood units are used', () { 21 | var lints = getLints(r'$pad: 2; $doublePad: #{$pad}pxx;'); 22 | 23 | expect(lints, isEmpty); 24 | }); 25 | 26 | test('does not report lint when a unit is followed by an interpolation', () { 27 | var lints = getLints(r'$pad: 2; $doublePad: #{$pad}px#{$pad};'); 28 | 29 | expect(lints, isEmpty); 30 | }); 31 | 32 | test('does not report lint when a unit is preceded by another string', () { 33 | var lints = getLints(r'$pad: 2; $doublePad: px#{$pad}px;'); 34 | 35 | expect(lints, isEmpty); 36 | }); 37 | 38 | test('reports lint when variable used in interpolation', () { 39 | var lints = getLints(r'$pad: 2; $padPx: #{$pad}px;'); 40 | 41 | expect(lints, hasLength(1)); 42 | 43 | var lint = lints.single; 44 | expect(lint.rule, rule); 45 | expect(lint.message, 46 | contains('Apply a unit to a numerical value via arithmetic')); 47 | expect(lint.message, contains(r'e.g. `$pad * 1px`.')); 48 | expect(lint.url, new Uri.file(url)); 49 | expect(lint.line, 0); 50 | expect(lint.column, 17); 51 | }); 52 | 53 | test('reports lint when expression used in interpolation', () { 54 | var lints = getLints(r'$pad: 2; $padAndMore: #{$pad + 5}px;'); 55 | 56 | expect(lints, hasLength(1)); 57 | 58 | var lint = lints.single; 59 | expect(lint.rule, rule); 60 | expect(lint.message, contains(r'`($pad + 5) * 1px`.')); 61 | expect(lint.url, new Uri.file(url)); 62 | expect(lint.line, 0); 63 | expect(lint.column, 22); 64 | }); 65 | } 66 | 67 | List getLints(String source) => 68 | new Linter(source, [rule], url: url).run(); 69 | -------------------------------------------------------------------------------- /test/rules/quote_map_keys_test.dart: -------------------------------------------------------------------------------- 1 | // Copyright 2018 Google Inc. Use of this source code is governed by an 2 | // MIT-style license that can be found in the LICENSE file or at 3 | // https://opensource.org/licenses/MIT. 4 | 5 | import 'package:sass_linter/src/rules/quote_map_keys.dart'; 6 | import 'package:sass_linter/src/lint.dart'; 7 | import 'package:sass_linter/src/linter.dart'; 8 | import 'package:test/test.dart'; 9 | 10 | final url = 'a.scss'; 11 | final rule = new QuoteMapKeysRule(); 12 | 13 | void main() { 14 | test('reports lint when map literal key is unquoted', () { 15 | var lints = getLints('\$map: (\n' 16 | ' key1: value1,\n' 17 | ' "key2": value2,\n' 18 | ');'); 19 | 20 | expect(lints, hasLength(1)); 21 | 22 | var lint = lints.single; 23 | expect(lint.rule, rule); 24 | expect(lint.message, contains('String literal map keys should be quoted.')); 25 | expect(lint.url, new Uri.file(url)); 26 | expect(lint.line, 1); 27 | expect(lint.column, 4); 28 | }); 29 | } 30 | 31 | List getLints(String source) => 32 | new Linter(source, [rule], url: url).run(); 33 | -------------------------------------------------------------------------------- /test/rules/use_falsey_null_test.dart: -------------------------------------------------------------------------------- 1 | // Copyright 2018 Google Inc. Use of this source code is governed by an 2 | // MIT-style license that can be found in the LICENSE file or at 3 | // https://opensource.org/licenses/MIT. 4 | 5 | import 'package:sass_linter/src/rules/use_falsey_null.dart'; 6 | import 'package:sass_linter/src/lint.dart'; 7 | import 'package:sass_linter/src/linter.dart'; 8 | import 'package:test/test.dart'; 9 | 10 | final url = 'a.scss'; 11 | final rule = new UseFalseyNullRule(); 12 | 13 | void main() { 14 | test('does not report lint when nothing is checked for null', () { 15 | var lints = getLints(r'@if 1 != 7 { @debug("True."); }'); 16 | 17 | expect(lints, isEmpty); 18 | }); 19 | 20 | test('reports lint when expression is checked for equality to `null`', () { 21 | var lints = getLints(r'@if 1 == null { @debug("True."); }'); 22 | 23 | expect(lints, hasLength(1)); 24 | 25 | var lint = lints.single; 26 | expect(lint.rule, rule); 27 | expect(lint.message, 28 | contains('Check for equality to null is unnecessarily explicit')); 29 | expect(lint.message, contains('prefer "not 1"')); 30 | expect(lint.url, new Uri.file(url)); 31 | expect(lint.line, 0); 32 | expect(lint.column, 9); 33 | }); 34 | 35 | test('reports lint when expression is checked for inequality to `null`', () { 36 | var lints = getLints(r'@if null != 7 { @debug("True."); }'); 37 | 38 | expect(lints, hasLength(1)); 39 | 40 | var lint = lints.single; 41 | expect(lint.rule, rule); 42 | expect(lint.message, contains('"!= null" is unnecessary')); 43 | expect(lint.url, new Uri.file(url)); 44 | expect(lint.line, 0); 45 | expect(lint.column, 4); 46 | }); 47 | 48 | test('reports lint on inner binary expressions', () { 49 | var lints = getLints(r'@if 1 == 2 or 3 == null { @debug("True."); }'); 50 | 51 | expect(lints, hasLength(1)); 52 | 53 | var lint = lints.single; 54 | expect(lint.rule, rule); 55 | expect(lint.message, 56 | contains('Check for equality to null is unnecessarily explicit')); 57 | expect(lint.url, new Uri.file(url)); 58 | expect(lint.line, 0); 59 | expect(lint.column, 19); 60 | }); 61 | } 62 | 63 | List getLints(String source) => 64 | new Linter(source, [rule], url: url).run(); 65 | -------------------------------------------------------------------------------- /test/rules/use_scale_color_test.dart: -------------------------------------------------------------------------------- 1 | // Copyright 2018 Google Inc. Use of this source code is governed by an 2 | // MIT-style license that can be found in the LICENSE file or at 3 | // https://opensource.org/licenses/MIT. 4 | 5 | import 'package:sass_linter/src/rules/use_scale_color.dart'; 6 | import 'package:sass_linter/src/lint.dart'; 7 | import 'package:sass_linter/src/linter.dart'; 8 | import 'package:test/test.dart'; 9 | 10 | final url = 'a.scss'; 11 | final rule = new UseScaleColorRule(); 12 | 13 | void main() { 14 | test('does not report lint when an acceptable function is used', () { 15 | var lints = getLints(r'p { color: grayscale(red); }'); 16 | 17 | expect(lints, isEmpty); 18 | }); 19 | 20 | test('reports lint when an older color function is used', () { 21 | var lints = getLints('p { color: darken(red, 50%); }'); 22 | 23 | expect(lints, hasLength(1)); 24 | 25 | var lint = lints.single; 26 | expect(lint.rule, rule); 27 | expect( 28 | lint.message, 29 | contains( 30 | '"darken" is a non-scaling function; use scale-color instead.')); 31 | expect(lint.url, new Uri.file(url)); 32 | expect(lint.line, 0); 33 | expect(lint.column, 11); 34 | }); 35 | } 36 | 37 | List getLints(String source) => 38 | new Linter(source, [rule], url: url).run(); 39 | --------------------------------------------------------------------------------