| // Copyright (c) 2019, 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:_fe_analyzer_shared/src/scanner/characters.dart'; | |
| import 'package:analyzer/dart/ast/ast.dart'; | |
| import 'package:analyzer/dart/ast/token.dart'; | |
| import 'package:analyzer/src/diagnostic/diagnostic.dart' as diag; | |
| import 'package:analyzer/src/error/listener.dart'; | |
| /// Finds invalid, or misplaced language override comments. | |
| class LanguageVersionOverrideVerifier { | |
| static final _overrideCommentLine = RegExp(r'^\s*//\s*@dart\s*=\s*\d+\.\d+'); | |
| final DiagnosticReporter _diagnosticReporter; | |
| LanguageVersionOverrideVerifier(this._diagnosticReporter); | |
| void verify(CompilationUnit unit) { | |
| _verifyMisplaced(unit); | |
| Token beginToken = unit.beginToken; | |
| if (beginToken.type == TokenType.SCRIPT_TAG) { | |
| beginToken = beginToken.next!; | |
| } | |
| Token? commentToken = beginToken.precedingComments; | |
| while (commentToken != null) { | |
| if (_findLanguageVersionOverrideComment(commentToken)) { | |
| // A valid language version override was found. Do not search for any | |
| // later invalid language version comments. | |
| return; | |
| } | |
| commentToken = commentToken.next; | |
| } | |
| } | |
| /// Look for comments which look almost like a Dart language version override, | |
| /// according to the spec [1]. | |
| /// | |
| /// The idea of a comment which looks "almost" like a language version | |
| /// override is a tricky dance. It is important that this function _not_ | |
| /// falsely report comment lines as an "invalid language version override" | |
| /// when the user was likely not trying to override the language version. Here | |
| /// is the general algorithm for deciding what to report: | |
| /// | |
| /// * When a comment begins with "@dart" or "dart" (letters in any case), | |
| /// followed by optional whitespace, followed by optional non-alphanumeric, | |
| /// non-whitespace characters, followed by optional whitespace, followed by | |
| /// an optional alphabetical character, followed by a digit, followed by the | |
| /// end of the line or a non-alphabetical character, then the comment is | |
| /// considered to be an attempt at a language version override comment | |
| /// (with one exception, below). | |
| /// * If the "@" character is missing before "dart", _and_ the | |
| /// non-alphabetical characters are not present, the comment is too | |
| /// different from a valid language version override comment, and is not | |
| /// considered to be an attempt. Examples include: "/// dart2 is great." | |
| /// * If the comment began with more than two slashes, report the comment. | |
| /// For example: "/// dart = 2". | |
| /// * If the "@" character is missing before "dart", report the comment. | |
| /// Examples include: "// dart = 2.0", "// dart @ 2.0". | |
| /// * If the letters, "dart", are not all lower case, report the comment. | |
| /// For example: "// @Dart = 2". | |
| /// * If the non-alphabetical characters are not present or are not the | |
| /// single character "=", report the comment. Examples include: | |
| /// "// @dart: 2", "// @dart > 2.0", "// @dart >= 2.0", "// @dart 2.0". | |
| /// * If the optional alphabetical letter is present, report the comment. | |
| /// For example: "// @dart = v2". | |
| /// * If the digit is not immediately followed by a "." character, then | |
| /// another digit, then optional whitespace, then the end of the line, | |
| /// report the comment. Examples include: "// @dart = 2", | |
| /// "// @dart = 2,0", "// @dart = 2.15", "// @dart = 2.2.2", | |
| /// "// @dart = 2.2 or so". | |
| /// * Otherwise, the comment is a valid language version override comment. | |
| /// * Otherwise, the comment is not considered to be an attempt at a language | |
| /// version override comment. Nothing is reported. Examples include: | |
| /// "/// dart", "/// dart is great", "// dartisans are great", | |
| /// "// dart = java, basically". | |
| /// | |
| /// [1] https://github.com/dart-lang/language/blob/master/accepted/future-releases/language-versioning/feature-specification.md#individual-library-language-version-override | |
| bool _findLanguageVersionOverrideComment(Token commentToken) { | |
| String comment = commentToken.lexeme; | |
| int offset = commentToken.offset; | |
| int length = comment.length; | |
| int index = 0; | |
| // TODO(srawlins): Actual whitespace. | |
| bool isWhitespace(int character) => character == 0x09 || character == 0x20; | |
| bool isNumeric(int character) => character >= 0x30 && character <= 0x39; | |
| bool isAlphabetical(int character) => | |
| (character >= 0x41 && character <= 0x5A) || | |
| (character >= 0x61 && character <= 0x7A); | |
| void skipWhitespaces() { | |
| while (index < length && isWhitespace(comment.codeUnitAt(index))) { | |
| index++; | |
| } | |
| } | |
| // Count the number of `/` characters at the beginning. | |
| while (index < length && comment.codeUnitAt(index) == 0x2F) { | |
| index++; | |
| } | |
| int slashCount = index; | |
| skipWhitespaces(); | |
| if (index == length) { | |
| // This is not an attempted language version override comment. | |
| return false; | |
| } | |
| bool atSignPresent = comment.codeUnitAt(index) == 0x40; | |
| if (atSignPresent) { | |
| index++; | |
| } | |
| if (length - index < 4) { | |
| // This is not an attempted language version override comment. | |
| return false; | |
| } | |
| String possibleDart = comment.substring(index, index + 4); | |
| if (possibleDart.toLowerCase() != 'dart') { | |
| // This is not an attempted language version override comment. | |
| return false; | |
| } | |
| index += 4; | |
| skipWhitespaces(); | |
| if (index == length) { | |
| // This is not an attempted language version override comment. | |
| return false; | |
| } | |
| // The separator between "@dart" and the version number. | |
| int dartVersionSeparatorStartIndex = index; | |
| // Move through any other consecutive punctuation, whitespace, | |
| while (index < length) { | |
| int possibleSeparatorCharacter = comment.codeUnitAt(index); | |
| if (isNumeric(possibleSeparatorCharacter) || | |
| isAlphabetical(possibleSeparatorCharacter) || | |
| isWhitespace(possibleSeparatorCharacter)) { | |
| break; | |
| } | |
| index++; | |
| } | |
| if (index == length) { | |
| // This is not an attempted language version override comment. | |
| return false; | |
| } | |
| int dartVersionSeparatorLength = index - dartVersionSeparatorStartIndex; | |
| skipWhitespaces(); | |
| if (index == length) { | |
| // This is not an attempted language version override comment. | |
| return false; | |
| } | |
| bool containsInvalidVersionNumberPrefix = false; | |
| if (isAlphabetical(comment.codeUnitAt(index))) { | |
| containsInvalidVersionNumberPrefix = true; | |
| index++; | |
| if (index == length) { | |
| // This is not an attempted language version override comment. | |
| return false; | |
| } | |
| } | |
| if (!isNumeric(comment.codeUnitAt(index))) { | |
| // This is not an attempted language version override comment. | |
| return false; | |
| } | |
| if (index + 1 < length && isAlphabetical(comment.codeUnitAt(index + 1))) { | |
| // This is not an attempted language version override comment. | |
| return false; | |
| } | |
| if (!atSignPresent && dartVersionSeparatorLength == 0) { | |
| // The comment is too different from a valid language version override | |
| // comment, like "/// dart2 is great". | |
| return false; | |
| } | |
| // At this point, the comment is considered an "attempted" language version | |
| // override comment. Check for all issues which would make it an invalid | |
| // language version override comment. | |
| if (slashCount > 2) { | |
| _diagnosticReporter.report( | |
| diag.invalidLanguageVersionOverrideTwoSlashes.atOffset( | |
| offset: offset, | |
| length: length, | |
| ), | |
| ); | |
| return false; | |
| } | |
| if (!atSignPresent) { | |
| _diagnosticReporter.report( | |
| diag.invalidLanguageVersionOverrideAtSign.atOffset( | |
| offset: offset, | |
| length: length, | |
| ), | |
| ); | |
| return false; | |
| } | |
| if (possibleDart != 'dart') { | |
| // The 4 characters after `@` are "dart", but in the wrong case. | |
| _diagnosticReporter.report( | |
| diag.invalidLanguageVersionOverrideLowerCase.atOffset( | |
| offset: offset, | |
| length: length, | |
| ), | |
| ); | |
| return false; | |
| } | |
| if (comment.codeUnitAt(dartVersionSeparatorStartIndex) != 0x3D) { | |
| // The first non-whitespace character after "@dart" is not "=". Examples | |
| // include: "// @dart: 2", "// @dart 2.0". | |
| _diagnosticReporter.report( | |
| diag.invalidLanguageVersionOverrideEquals.atOffset( | |
| offset: offset, | |
| length: length, | |
| ), | |
| ); | |
| return false; | |
| } | |
| if (dartVersionSeparatorLength != 1) { | |
| // The separator starts with "=", so what follows the "=" is what is not | |
| // a version number. Example: `// @dart="2.0"`. | |
| _diagnosticReporter.report( | |
| diag.invalidLanguageVersionOverrideNumber.atOffset( | |
| offset: offset, | |
| length: length, | |
| ), | |
| ); | |
| return false; | |
| } | |
| if (containsInvalidVersionNumberPrefix) { | |
| _diagnosticReporter.report( | |
| diag.invalidLanguageVersionOverridePrefix.atOffset( | |
| offset: offset, | |
| length: length, | |
| ), | |
| ); | |
| return false; | |
| } | |
| void reportInvalidNumber() { | |
| _diagnosticReporter.report( | |
| diag.invalidLanguageVersionOverrideNumber.atOffset( | |
| offset: offset, | |
| length: length, | |
| ), | |
| ); | |
| } | |
| // Nothing preceding the version number makes this comment invalid. Check | |
| // the format of the version number, and trailing characters. | |
| // Skip major version. | |
| while (index < length && isNumeric(comment.codeUnitAt(index))) { | |
| index++; | |
| } | |
| // Skip '.' separator. | |
| if (index == length || comment.codeUnitAt(index) != 0x2E) { | |
| reportInvalidNumber(); | |
| return false; | |
| } | |
| index++; | |
| // Skip minor version. | |
| while (index < length && isNumeric(comment.codeUnitAt(index))) { | |
| index++; | |
| } | |
| skipWhitespaces(); | |
| // OK, no trailing characters. | |
| if (index == length) { | |
| return true; | |
| } | |
| // This comment is a valid language version override, except for trailing | |
| // characters. | |
| _diagnosticReporter.report( | |
| diag.invalidLanguageVersionOverrideTrailingCharacters.atOffset( | |
| offset: offset, | |
| length: length, | |
| ), | |
| ); | |
| return false; | |
| } | |
| /// Verify that all language version overrides are before declarations. | |
| void _verifyMisplaced(CompilationUnit unit) { | |
| Token firstMeaningfulToken; | |
| if (unit.directives.isNotEmpty) { | |
| firstMeaningfulToken = unit.directives.first.beginToken; | |
| } else if (unit.declarations2.isNotEmpty) { | |
| firstMeaningfulToken = unit.declarations2.first.beginToken; | |
| } else { | |
| return; | |
| } | |
| var token = firstMeaningfulToken.next; | |
| while (token != null) { | |
| if (token.offset > firstMeaningfulToken.offset) { | |
| Token? commentToken = token.precedingComments; | |
| for (; commentToken != null; commentToken = commentToken.next) { | |
| var lexeme = commentToken.lexeme; | |
| // Optimization: The regex usage below is expensive. Checking if the | |
| // string contains an `@` first saves most calls to it. | |
| int offset = 0; | |
| int end = lexeme.length - 5; | |
| for (; offset < end; offset++) { | |
| if (lexeme.codeUnitAt(offset) == $AT) break; | |
| } | |
| if (offset >= end) continue; | |
| var match = _overrideCommentLine.firstMatch(lexeme); | |
| if (match != null) { | |
| var atDartStart = lexeme.indexOf('@dart'); | |
| _diagnosticReporter.report( | |
| diag.invalidLanguageVersionOverrideLocation.atOffset( | |
| offset: commentToken.offset + atDartStart, | |
| length: match.end - atDartStart, | |
| ), | |
| ); | |
| } | |
| } | |
| } | |
| if (token.next == token) { | |
| break; | |
| } else { | |
| token = token.next; | |
| } | |
| } | |
| } | |
| } |