Add unnecessary_escapes_in_string (#1996)
* unnecessary_escapes_in_string
* address review comments
* handle multiline strings
diff --git a/example/all.yaml b/example/all.yaml
index 98f78f1..2d6055c 100644
--- a/example/all.yaml
+++ b/example/all.yaml
@@ -140,6 +140,7 @@
- unnecessary_await_in_return
- unnecessary_brace_in_string_interps
- unnecessary_const
+ - unnecessary_escapes_in_string
- unnecessary_final
- unnecessary_getters_setters
- unnecessary_lambdas
diff --git a/lib/src/rules.dart b/lib/src/rules.dart
index c57d171..f0583e5 100644
--- a/lib/src/rules.dart
+++ b/lib/src/rules.dart
@@ -143,6 +143,7 @@
import 'rules/unnecessary_await_in_return.dart';
import 'rules/unnecessary_brace_in_string_interps.dart';
import 'rules/unnecessary_const.dart';
+import 'rules/unnecessary_escapes_in_string.dart';
import 'rules/unnecessary_final.dart';
import 'rules/unnecessary_getters_setters.dart';
import 'rules/unnecessary_lambdas.dart';
@@ -309,6 +310,7 @@
..register(UnnecessaryAwaitInReturn())
..register(UnnecessaryBraceInStringInterps())
..register(UnnecessaryConst())
+ ..register(UnnecessaryEscapesInString())
..register(UnnecessaryFinal())
..register(UnnecessaryNew())
..register(UnnecessaryNullAwareAssignments())
diff --git a/lib/src/rules/unnecessary_escapes_in_string.dart b/lib/src/rules/unnecessary_escapes_in_string.dart
new file mode 100644
index 0000000..5da21a7
--- /dev/null
+++ b/lib/src/rules/unnecessary_escapes_in_string.dart
@@ -0,0 +1,149 @@
+// Copyright (c) 2020, 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/token.dart';
+import 'package:analyzer/dart/ast/visitor.dart';
+import 'package:meta/meta.dart';
+
+import '../analyzer.dart';
+
+const _desc = r'Remove unnecessary backslashes in strings.';
+
+const _details = r'''
+
+Remove unnecessary backslashes in strings.
+
+**BAD:**
+```
+'this string contains 2 \"double quotes\" ';
+"this string contains 2 \'single quotes\' ";
+```
+
+**GOOD:**
+```
+'this string contains 2 "double quotes" ';
+"this string contains 2 'single quotes' ";
+```
+
+''';
+
+class UnnecessaryEscapesInString extends LintRule implements NodeLintRule {
+ UnnecessaryEscapesInString()
+ : super(
+ name: 'unnecessary_escapes_in_string',
+ description: _desc,
+ details: _details,
+ group: Group.style);
+
+ @override
+ void registerNodeProcessors(NodeLintRegistry registry,
+ [LinterContext context]) {
+ final visitor = _Visitor(this);
+ registry.addSimpleStringLiteral(this, visitor);
+ registry.addStringInterpolation(this, visitor);
+ }
+}
+
+class _Visitor extends SimpleAstVisitor<void> {
+ final LintRule rule;
+
+ _Visitor(this.rule);
+
+ @override
+ void visitSimpleStringLiteral(SimpleStringLiteral node) {
+ if (node.isRaw) return;
+
+ visitLexeme(
+ node.literal,
+ isSingleQuoted: node.isSingleQuoted,
+ isMultiline: node.isMultiline,
+ contentsOffset: node.contentsOffset,
+ contentsEnd: node.contentsEnd,
+ );
+ }
+
+ @override
+ void visitStringInterpolation(StringInterpolation node) {
+ for (var element in node.elements.whereType<InterpolationString>()) {
+ visitLexeme(
+ element.contents,
+ isSingleQuoted: node.isSingleQuoted,
+ isMultiline: node.isMultiline,
+ // TODO(a14n): should be the following line but the values look buggy
+ // contentsOffset: element.contentsOffset,
+ // contentsEnd: element.contentsEnd,
+ contentsOffset: element.offset +
+ (element != node.elements.first ? 0 : node.isMultiline ? 3 : 1),
+ contentsEnd: element.end -
+ (element != node.elements.last ? 0 : node.isMultiline ? 3 : 1),
+ );
+ }
+ }
+
+ void visitLexeme(
+ Token token, {
+ @required bool isSingleQuoted,
+ @required bool isMultiline,
+ @required int contentsOffset,
+ @required int contentsEnd,
+ }) {
+ // For multiline string we keep the list on pending quotes.
+ // Starting from 3 consecutive quotes, we allow escaping.
+ // index -> escaped
+ final pendingQuotes = <int, bool>{};
+ void checkPendingQuotes() {
+ if (isMultiline && pendingQuotes.length < 3) {
+ final escapeIndexes =
+ pendingQuotes.entries.where((e) => e.value).map((e) => e.key);
+ for (var index in escapeIndexes) {
+ // case for '''___\'''' : without last backslash it leads a parsing error
+ if (contentsEnd != token.end && index + 2 == contentsEnd) continue;
+ rule.reporter.reportErrorForOffset(rule.lintCode, index, 1);
+ }
+ }
+ }
+
+ final lexeme = token.lexeme
+ .substring(contentsOffset - token.offset, contentsEnd - token.offset);
+ for (var i = 0; i < lexeme.length; i++) {
+ var current = lexeme[i];
+ var escaped = false;
+ if (current == r'\') {
+ escaped = true;
+ i += 1;
+ current = lexeme[i];
+ if (isSingleQuoted && current == '"' ||
+ !isSingleQuoted && current == "'" ||
+ !allowedEscapedChars.contains(current)) {
+ rule.reporter
+ .reportErrorForOffset(rule.lintCode, contentsOffset + i - 1, 1);
+ }
+ }
+ if (isSingleQuoted ? current == "'" : current == '"') {
+ pendingQuotes[contentsOffset + i - (escaped ? 1 : 0)] = escaped;
+ } else {
+ checkPendingQuotes();
+ pendingQuotes.clear();
+ }
+ }
+ checkPendingQuotes();
+ }
+
+ /// The special escaped chars listed in language specification
+ static const allowedEscapedChars = [
+ '"',
+ "'",
+ r'$',
+ r'\',
+ 'n',
+ 'r',
+ 'f',
+ 'b',
+ 't',
+ 'v',
+ 'x',
+ 'u',
+ ];
+}
diff --git a/test/rules/unnecessary_escapes_in_string.dart b/test/rules/unnecessary_escapes_in_string.dart
new file mode 100644
index 0000000..ff2ceac
--- /dev/null
+++ b/test/rules/unnecessary_escapes_in_string.dart
@@ -0,0 +1,60 @@
+// Copyright (c) 2020, 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.
+
+// test w/ `pub run test -N unnecessary_escapes_in_string`
+
+f(o){
+ f("\'");// LINT
+ f('\"');// LINT
+ f("\"");// OK
+ f('\'');// OK
+
+ f("\'$f");// LINT
+ f('\"$f');// LINT
+ f("\"$f");// OK
+ f('\'$f');// OK
+ f("$f\'");// LINT
+ f('$f\"');// LINT
+ f("$f\"");// OK
+ f('$f\'');// OK
+ f("""\'$f""");// LINT
+ f('''\"$f''');// LINT
+ f("""\"$f""");// LINT
+ f('''\'$f''');// LINT
+ f("""$f\'""");// LINT
+ f('''$f\"''');// LINT
+ f("""$f\"""");// OK
+ f('''$f\'''');// OK
+
+ f('\:'); // LINT
+ f('\a'); // LINT
+ f('\uFFFF'); // OK
+ f('\t'); // OK
+ f('\n'); // OK
+ f('\r'); // OK
+ f('\$'); // OK
+ f('\x00'); // OK
+ f('\\'); // OK
+
+ f(r"\'");// OK
+ f(r'\"');// OK
+
+ f('''_\'_'''); // LINT
+ f('''_\''''); // OK otherwise parsing error
+ f('''\'_'''); // LINT
+ f('''_\"_'''); // LINT
+ f('''_\''_'''); // LINT
+ f('''_\'\'\'_'''); // OK otherwise parsing error
+ f('''_'\''_'''); // OK otherwise parsing error
+ f('''_\'\'\'\'\'\'_'''); // OK for >= 3 consecutive quotes
+
+ f("""_\"_"""); // LINT
+ f("""_\""""); // OK otherwise parsing error
+ f("""\"_"""); // LINT
+ f("""_\'_"""); // LINT
+ f("""_\""_"""); // LINT
+ f("""_\"\"\"_"""); // OK otherwise parsing error
+ f("""_"\""_"""); // OK otherwise parsing error
+ f("""_\"\"\"\"\"\"_"""); // OK for >= 3 consecutive quotes
+}