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
+}