blob: 8030358d65723c7f15c850e266230d2196d9c287 [file] [log] [blame]
// 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/analysis_rule/rule_context.dart';
import 'package:analyzer/analysis_rule/rule_visitor_registry.dart';
import 'package:analyzer/dart/ast/ast.dart';
import 'package:analyzer/dart/ast/visitor.dart';
import 'package:analyzer/error/error.dart';
import '../analyzer.dart';
const _desc = r'Only use double quotes for strings containing single quotes.';
class PreferSingleQuotes extends LintRule {
PreferSingleQuotes()
: super(name: LintNames.prefer_single_quotes, description: _desc);
@override
DiagnosticCode get diagnosticCode => LinterLintCode.preferSingleQuotes;
@override
List<String> get incompatibleRules => const [LintNames.prefer_double_quotes];
@override
void registerNodeProcessors(
RuleVisitorRegistry registry,
RuleContext context,
) {
var 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});
/// Strings interpolations can contain other string nodes. Check like this.
bool containsString(StringInterpolation string) {
var checkHasString = _IsOrContainsStringVisitor();
return string.elements.any(
(child) => child.accept(checkHasString) ?? false,
);
}
/// 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 node) {
if (useSingle && (node.isSingleQuoted || node.value.contains("'")) ||
!useSingle && (!node.isSingleQuoted || node.value.contains('"'))) {
return;
}
// Bail out on 'strings ${x ? "containing" : "other"} strings'
if (!isNestedString(node)) {
rule.reportAtToken(node.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.reportAtNode(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<void> {
final _children = <AstNode>[];
@override
void visitNode(AstNode node) {
_children.add(node);
}
static List<AstNode> childrenOf(AstNode node) {
var 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) ?? false;
/// 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;
}