| // Copyright (c) 2017, the Dart project authors. Please see the AUTHORS file |
| // for details. All rights reserved. Use of this source code is governed by a |
| // BSD-style license that can be found in the LICENSE file. |
| |
| import 'package:analyzer/dart/ast/ast.dart'; |
| import 'package:analyzer/dart/ast/visitor.dart'; |
| import 'package:meta/meta.dart'; |
| |
| import '../analyzer.dart'; |
| |
| const _desc = r'Only use double quotes for strings containing single quotes.'; |
| |
| const _details = ''' |
| |
| **DO** use single quotes where they wouldn't require additional escapes. |
| |
| That means strings with an apostrophe may use double quotes so that the |
| apostrophe isn't escaped (note: we don't lint the other way around, ie, a single |
| quoted string with an escaped apostrophe is not flagged). |
| |
| It's also rare, but possible, to have strings within string interpolations. In |
| this case, its much more readable to use a double quote somewhere. So double |
| quotes are allowed either within, or containing, an interpolated string literal. |
| Arguably strings within string interpolations should be its own type of lint. |
| |
| **BAD:** |
| ``` |
| useStrings( |
| "should be single quote", |
| r"should be single quote", |
| r"""should be single quotes""") |
| ``` |
| |
| **GOOD:** |
| ``` |
| useStrings( |
| 'should be single quote', |
| r'should be single quote', |
| r\'''should be single quotes\''', |
| "here's ok", |
| "nested \${a ? 'strings' : 'can'} be wrapped by a double quote", |
| 'and nested \${a ? "strings" : "can be double quoted themselves"}'); |
| ``` |
| |
| '''; |
| |
| class PreferSingleQuotes extends LintRule implements NodeLintRule { |
| PreferSingleQuotes() |
| : super( |
| name: 'prefer_single_quotes', |
| description: _desc, |
| details: _details, |
| group: Group.style); |
| |
| @override |
| List<String> get incompatibleRules => const ['prefer_double_quotes']; |
| |
| @override |
| void registerNodeProcessors( |
| NodeLintRegistry registry, LinterContext context) { |
| final visitor = QuoteVisitor(this, useSingle: true); |
| registry.addSimpleStringLiteral(this, visitor); |
| registry.addStringInterpolation(this, visitor); |
| } |
| } |
| |
| class QuoteVisitor extends SimpleAstVisitor<void> { |
| final LintRule rule; |
| final bool useSingle; |
| |
| QuoteVisitor( |
| this.rule, { |
| @required this.useSingle, |
| }) : assert(useSingle != null); |
| |
| /// Strings interpolations can contain other string nodes. Check like this. |
| bool containsString(StringInterpolation string) { |
| final checkHasString = _IsOrContainsStringVisitor(); |
| return string.elements.any((child) => child.accept(checkHasString)); |
| } |
| |
| /// Strings can be within interpolations (ie, nested). Check like this. |
| bool isNestedString(AstNode node) => |
| // careful: node.getAncestor will check the node itself. |
| node.parent?.thisOrAncestorOfType<StringInterpolation>() != null; |
| |
| @override |
| void visitSimpleStringLiteral(SimpleStringLiteral string) { |
| if (useSingle && (string.isSingleQuoted || string.value.contains("'")) || |
| !useSingle && (!string.isSingleQuoted || string.value.contains('"'))) { |
| return; |
| } |
| |
| // Bail out on 'strings ${x ? "containing" : "other"} strings' |
| if (!isNestedString(string)) { |
| rule.reportLintForToken(string.literal); |
| } |
| } |
| |
| @override |
| void visitStringInterpolation(StringInterpolation node) { |
| if (useSingle && node.isSingleQuoted || |
| !useSingle && !node.isSingleQuoted) { |
| return; |
| } |
| |
| // slightly more complicated check there are no single quotes |
| if (node.elements.any((e) => |
| e is InterpolationString && |
| (useSingle && e.value.contains("'") || |
| !useSingle && e.value.contains('"')))) { |
| return; |
| } |
| |
| // Bail out on "strings ${x ? 'containing' : 'other'} strings" |
| if (!containsString(node) && !isNestedString(node)) { |
| rule.reportLint(node); |
| } |
| } |
| } |
| |
| /// The only way to get immediate children in a unified, typesafe way, is to |
| /// call visitChildren on that node, and pass in a visitor. This collects at the |
| /// top level and stops. |
| class _ImmediateChildrenVisitor extends UnifyingAstVisitor { |
| final _children = <AstNode>[]; |
| |
| @override |
| void visitNode(AstNode node) { |
| _children.add(node); |
| } |
| |
| static List<AstNode> childrenOf(AstNode node) { |
| final visitor = _ImmediateChildrenVisitor(); |
| node.visitChildren(visitor); |
| return visitor._children; |
| } |
| } |
| |
| /// Do a top-down analysis to search for string nodes. Note, do not pass in |
| /// string nodes directly to this visitor, or you will always get true. Pass in |
| /// its children. |
| class _IsOrContainsStringVisitor extends UnifyingAstVisitor<bool> { |
| /// Different way to express `accept` in a way that's clearer in this visitor. |
| bool isOrContainsString(AstNode node) => node.accept(this); |
| |
| /// Scan as little of the tree as possible, by bailing out on first match. For |
| /// all leaf nodes, they will either have a method defined here and return |
| /// true, or they will return false because leaves have no children. |
| @override |
| bool visitNode(AstNode node) => |
| _ImmediateChildrenVisitor.childrenOf(node).any(isOrContainsString); |
| |
| @override |
| bool visitSimpleStringLiteral(SimpleStringLiteral string) => true; |
| |
| @override |
| bool visitStringInterpolation(StringInterpolation string) => true; |
| } |