| // 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 UnnecessaryStringEscapes extends LintRule implements NodeLintRule { |
| UnnecessaryStringEscapes() |
| : super( |
| name: 'unnecessary_string_escapes', |
| 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', |
| ]; |
| } |