blob: 5853f8ea1636441d3953e98684f156c332a36973 [file] [log] [blame]
// 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:analyzer/dart/ast/ast.dart';
import 'package:analyzer/dart/ast/token.dart';
import 'package:analyzer/error/listener.dart';
import 'package:analyzer/src/error/codes.dart';
/// Finds invalid, or misplaced language override comments.
class LanguageVersionOverrideVerifier {
static final _overrideCommentLine = RegExp(r'^\s*//\s*@dart\s*=\s*\d+\.\d+');
final ErrorReporter _errorReporter;
LanguageVersionOverrideVerifier(this._errorReporter);
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) {
_errorReporter.reportErrorForOffset(
HintCode.INVALID_LANGUAGE_VERSION_OVERRIDE_TWO_SLASHES,
offset,
length);
return false;
}
if (!atSignPresent) {
_errorReporter.reportErrorForOffset(
HintCode.INVALID_LANGUAGE_VERSION_OVERRIDE_AT_SIGN, offset, length);
return false;
}
if (possibleDart != 'dart') {
// The 4 characters after `@` are "dart", but in the wrong case.
_errorReporter.reportErrorForOffset(
HintCode.INVALID_LANGUAGE_VERSION_OVERRIDE_LOWER_CASE,
offset,
length);
return false;
}
if (dartVersionSeparatorLength != 1 ||
comment.codeUnitAt(dartVersionSeparatorStartIndex) != 0x3D) {
// The separator between "@dart" and the version number is either not
// present, or is not a single "=" character.
_errorReporter.reportErrorForOffset(
HintCode.INVALID_LANGUAGE_VERSION_OVERRIDE_EQUALS, offset, length);
return false;
}
if (containsInvalidVersionNumberPrefix) {
_errorReporter.reportErrorForOffset(
HintCode.INVALID_LANGUAGE_VERSION_OVERRIDE_PREFIX, offset, length);
return false;
}
void reportInvalidNumber() {
_errorReporter.reportErrorForOffset(
HintCode.INVALID_LANGUAGE_VERSION_OVERRIDE_NUMBER, offset, 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.
_errorReporter.reportErrorForOffset(
HintCode.INVALID_LANGUAGE_VERSION_OVERRIDE_TRAILING_CHARACTERS,
offset,
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.declarations.isNotEmpty) {
firstMeaningfulToken = unit.declarations.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;
var match = _overrideCommentLine.firstMatch(lexeme);
if (match != null) {
var atDartStart = lexeme.indexOf('@dart');
_errorReporter.reportErrorForOffset(
HintCode.INVALID_LANGUAGE_VERSION_OVERRIDE_LOCATION,
commentToken.offset + atDartStart,
match.end - atDartStart,
);
}
}
}
if (token.next == token) {
break;
} else {
token = token.next;
}
}
}
}