blob: 5bbc61ed95820e3ed737a0bbced677d8f725ff55 [file] [log] [blame]
// Copyright (c) 2015, 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/diagnostic/diagnostic.dart';
import 'package:analyzer/error/error.dart';
import 'package:analyzer/source/line_info.dart';
import 'package:analyzer/src/utilities/extensions/string.dart';
/// The text and location of trailing unstructured comment text in an ignore
/// comment.
class IgnoredDiagnosticComment implements IgnoredElement {
final String text;
final int offset;
IgnoredDiagnosticComment(this.text, this.offset);
@override
bool _matches(ErrorCode errorCode, {String? pluginName}) => false;
}
/// The name and location of a diagnostic name in an ignore comment.
class IgnoredDiagnosticName implements IgnoredElement {
/// The name of the diagnostic being ignored.
final String name;
final String? pluginName;
final int offset;
IgnoredDiagnosticName(String name, this.offset, {this.pluginName})
: name = name.toLowerCase();
@override
bool _matches(ErrorCode errorCode, {String? pluginName}) {
if (this.pluginName != pluginName) {
return false;
}
if (name == errorCode.name.toLowerCase()) {
return true;
}
var uniqueName = errorCode.uniqueName;
var period = uniqueName.indexOf('.');
if (period >= 0) {
uniqueName = uniqueName.substring(period + 1);
}
return name == uniqueName.toLowerCase();
}
}
/// The name and location of a diagnostic type in an ignore comment.
class IgnoredDiagnosticType implements IgnoredElement {
final String type;
final int offset;
final int length;
IgnoredDiagnosticType(String type, this.offset, this.length)
: type = type.toLowerCase();
@override
bool _matches(ErrorCode errorCode, {String? pluginName}) {
// Ignore 'pluginName'; it is irrelevant in an IgnoredDiagnosticType.
return switch (errorCode.type) {
DiagnosticType.HINT => type == 'hint',
DiagnosticType.LINT => type == 'lint',
DiagnosticType.STATIC_WARNING => type == 'warning',
// Only errors with one of the above types can be ignored via the type.
_ => false,
};
}
}
sealed class IgnoredElement {
/// Returns whether this matches the given [errorCode].
bool _matches(ErrorCode errorCode, {String? pluginName});
}
/// Information about analysis `//ignore:` and `//ignore_for_file:` comments
/// within a source file.
class IgnoreInfo {
/// A regular expression for matching 'ignore' comments.
///
/// Resulting codes may be in a list (e.g. 'error_code_1,error_code2').
static final RegExp ignoreMatcher = RegExp(r'//+[ ]*ignore:');
/// A regular expression for matching 'ignore_for_file' comments.
///
/// Resulting codes may be in a list (e.g. 'error_code_1,error_code2').
static final RegExp ignoreForFileMatcher = RegExp(r'//[ ]*ignore_for_file:');
/// A regular expression for matching 'ignore' comments in a .yaml file.
///
/// Resulting codes may be in a list (e.g. 'error_code_1,error_code2').
static final RegExp _yamlIgnoreMatcher = RegExp(
r'^(?<before>.*)#+[ ]*ignore:(?<ignored>.*)',
multiLine: true,
);
/// A regular expression for matching 'ignore_for_file' comments.
///
/// Resulting codes may be in a list (e.g. 'error_code_1,error_code2').
static final RegExp _yamlIgnoreForFileMatcher = RegExp(
r'#[ ]*ignore_for_file:(?<ignored>.*)',
);
static final _trimmedCommaSeparatedMatcher = RegExp(r'[^\s,]([^,]*[^\s,])?');
/// A table mapping line numbers to the elements (diagnostics and diagnostic
/// types) that are ignored on that line.
final Map<int, List<IgnoredElement>> _ignoredOnLine = {};
/// A list containing all of the elements (diagnostics and diagnostic types)
/// that are ignored for the whole file.
final List<IgnoredElement> _ignoredForFile = [];
final LineInfo _lineInfo;
IgnoreInfo.empty() : _lineInfo = LineInfo([]);
/// Initializes a newly created instance of this class to represent the ignore
/// comments in the given compilation [unit].
IgnoreInfo.forDart(CompilationUnit unit, String content)
: _lineInfo = unit.lineInfo {
for (var comment in unit.ignoreComments) {
var lexeme = comment.lexeme;
if (lexeme.contains('ignore:')) {
var location = _lineInfo.getLocation(comment.offset);
var lineNumber = location.lineNumber;
var offsetOfLine = _lineInfo.getOffsetOfLine(lineNumber - 1);
var beforeMatch = content.substring(
offsetOfLine,
offsetOfLine + location.columnNumber - 1,
);
if (beforeMatch.trim().isEmpty) {
// The comment is on its own line, so it refers to the next line.
lineNumber++;
}
_ignoredOnLine
.putIfAbsent(lineNumber, () => [])
.addAll(comment.ignoredElements);
} else if (lexeme.contains('ignore_for_file:')) {
_ignoredForFile.addAll(comment.ignoredElements);
}
}
}
/// Initializes a newly created instance of this class to represent the ignore
/// comments in the given YAML file.
IgnoreInfo.forYaml(String content, this._lineInfo) {
Iterable<IgnoredDiagnosticName> diagnosticNamesInMatch(RegExpMatch match) {
var ignored = match.namedGroup('ignored')!;
var offset = match.start;
return _trimmedCommaSeparatedMatcher
.allMatches(ignored)
.map((m) => IgnoredDiagnosticName(m[0]!, offset + m.start));
}
for (var match in _yamlIgnoreForFileMatcher.allMatches(content)) {
_ignoredForFile.addAll(diagnosticNamesInMatch(match));
}
for (var match in _yamlIgnoreMatcher.allMatches(content)) {
var lineNumber = _lineInfo.getLocation(match.start).lineNumber;
var beforeComment = match.namedGroup('before')!;
var nextLine = beforeComment.trim().isEmpty;
_ignoredOnLine
.putIfAbsent(nextLine ? lineNumber + 1 : lineNumber, () => [])
.addAll(diagnosticNamesInMatch(match));
}
}
/// Whether there are any ignore comments in the file.
bool get hasIgnores =>
_ignoredOnLine.isNotEmpty || _ignoredForFile.isNotEmpty;
/// A list containing all of the diagnostics that are ignored for the whole
/// file.
List<IgnoredElement> get ignoredForFile => _ignoredForFile.toList();
/// A table mapping line numbers to the diagnostics that are ignored on that
/// line.
Map<int, List<IgnoredElement>> get ignoredOnLine {
Map<int, List<IgnoredElement>> ignoredOnLine = {};
for (var entry in _ignoredOnLine.entries) {
ignoredOnLine[entry.key] = entry.value.toList();
}
return ignoredOnLine;
}
/// Whether [diagnostic] is ignored via an inline "ignore" comment.
bool ignored(Diagnostic diagnostic, {String? pluginName}) {
var line = _lineInfo.getLocation(diagnostic.offset).lineNumber;
return _ignoredAt(diagnostic.errorCode, line, pluginName: pluginName);
}
/// Returns whether the [errorCode] is ignored at the given [line].
bool _ignoredAt(ErrorCode errorCode, int line, {String? pluginName}) {
var ignoredDiagnostics = _ignoredOnLine[line];
if (ignoredForFile.isEmpty && ignoredDiagnostics == null) {
return false;
}
if (ignoredForFile.any(
(name) => name._matches(errorCode, pluginName: pluginName),
)) {
return true;
}
if (ignoredDiagnostics == null) {
return false;
}
return ignoredDiagnostics.any(
(name) => name._matches(errorCode, pluginName: pluginName),
);
}
}
extension CommentTokenExtension on CommentToken {
/// The elements ([IgnoredDiagnosticName]s and [IgnoredDiagnosticType]s) cited
/// by this comment, if it is a correctly formatted ignore comment.
// Use of `sync*` should not be non-performant; the vast majority of ignore
// comments cite a single diagnostic name. Ignore comments that cite multiple
// diagnostic names typically cite only a handful.
Iterable<IgnoredElement> get ignoredElements sync* {
var offset = lexeme.indexOf(':') + 1;
void skipPastWhitespace() {
while (offset < lexeme.length) {
if (!lexeme.codeUnitAt(offset).isWhitespace) {
return;
}
offset++;
}
}
void readWord() {
if (!lexeme.codeUnitAt(offset).isLetter) {
// Must start with a letter.
return;
}
offset++;
while (offset < lexeme.length) {
if (!lexeme.codeUnitAt(offset).isLetterOrDigitOrUnderscore) {
return;
}
offset++;
}
}
// We only want to add an `IgnoredDiagnosticComment` if it is preceded by
// one or more `IgnoredDiagnosticName`s or `IgnoredDiagnosticType`s.
var hasIgnoredElements = false;
while (true) {
skipPastWhitespace();
if (offset == lexeme.length) {
// Reached the end without finding any ignored elements.
return;
}
var wordOffset = offset;
// Parse each comma-separated diagnostic code, and diagnostic type.
readWord();
if (wordOffset == offset) {
// There is a non-word (other characters) at `offset`.
if (hasIgnoredElements) {
yield IgnoredDiagnosticComment(
lexeme.substring(offset),
this.offset + wordOffset,
);
}
return;
}
var word = lexeme.substring(wordOffset, offset);
if (word.toLowerCase() == 'type') {
// Parse diagnostic type.
skipPastWhitespace();
if (offset == lexeme.length) return;
var nextChar = lexeme.codeUnitAt(offset);
if (!nextChar.isEqual) return;
offset++;
skipPastWhitespace();
if (offset == lexeme.length) return;
var typeOffset = offset;
readWord();
if (typeOffset == offset) {
// There is a non-word (other characters) at `offset`.
if (hasIgnoredElements) {
yield IgnoredDiagnosticComment(
lexeme.substring(offset),
this.offset + wordOffset,
);
}
return;
}
if (offset < lexeme.length) {
var nextChar = lexeme.codeUnitAt(offset);
if (!nextChar.isSpace && !nextChar.isComma) {
// There are non-identifier characters at the end of this word,
// like `ignore: http://google.com`. This is not a diagnostic name.
if (hasIgnoredElements) {
yield IgnoredDiagnosticComment(
lexeme.substring(wordOffset),
this.offset + wordOffset,
);
}
return;
}
}
var type = lexeme.substring(typeOffset, offset);
hasIgnoredElements = true;
yield IgnoredDiagnosticType(
type,
this.offset + wordOffset,
offset - wordOffset,
);
} else {
String? pluginName;
if (offset < lexeme.length) {
var nextChar = lexeme.codeUnitAt(offset);
if (nextChar.isSlash) {
// We may be looking at a plugin-name-prefixed code, like
// 'plugin_one/foo'.
pluginName = word;
offset++;
if (offset == lexeme.length) return;
var nameOffset = offset;
readWord();
word = lexeme.substring(nameOffset, offset);
if (nameOffset == offset) {
// There is a non-word (other characters) at `offset`.
if (hasIgnoredElements) {
yield IgnoredDiagnosticComment(
lexeme.substring(offset),
this.offset + nameOffset,
);
}
return;
}
}
}
if (offset < lexeme.length) {
var nextChar = lexeme.codeUnitAt(offset);
if (!nextChar.isSpace && !nextChar.isComma) {
// There are non-identifier characters at the end of this word,
// like `ignore: http://google.com`. This is not a diagnostic name.
if (hasIgnoredElements) {
yield IgnoredDiagnosticComment(
lexeme.substring(wordOffset),
this.offset + wordOffset,
);
}
return;
}
}
hasIgnoredElements = true;
yield IgnoredDiagnosticName(
word,
this.offset + wordOffset,
pluginName: pluginName,
);
}
if (offset == lexeme.length) return;
skipPastWhitespace();
if (offset == lexeme.length) return;
var nextChar = lexeme.codeUnitAt(offset);
if (!nextChar.isComma) {
// We've reached the end of the comma-separated codes and types. What
// follows is unstructured comment text.
if (hasIgnoredElements) {
yield IgnoredDiagnosticComment(
lexeme.substring(offset),
this.offset + wordOffset,
);
}
return;
}
offset++;
if (offset == lexeme.length) return;
}
}
}
extension CompilationUnitExtension on CompilationUnit {
/// Returns all of the ignore comments in this compilation unit.
List<CommentToken> get ignoreComments {
var result = <CommentToken>[];
void processPrecedingComments(Token currentToken) {
var comment = currentToken.precedingComments;
while (comment != null) {
var lexeme = comment.lexeme;
if (lexeme.startsWith(IgnoreInfo.ignoreMatcher)) {
result.add(comment);
} else if (lexeme.startsWith(IgnoreInfo.ignoreForFileMatcher)) {
result.add(comment);
}
comment = comment.next as CommentToken?;
}
}
var currentToken = beginToken;
while (currentToken != currentToken.next) {
processPrecedingComments(currentToken);
currentToken = currentToken.next!;
}
processPrecedingComments(currentToken);
return result;
}
}