blob: d2f6b1386c89c134129b836bc9e350d876f220fe [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/error/error.dart';
import 'package:analyzer/src/dart/ast/token.dart';
/// The name and location of a diagnostic name in an ignore comment.
class DiagnosticName implements IgnoredElement {
/// The name of the diagnostic being ignored.
final String name;
final int offset;
/// Initialize a newly created diagnostic name to have the given [name] and
/// [offset].
DiagnosticName(this.name, this.offset);
/// Return `true` if this diagnostic name matches the given error code.
@override
bool matches(ErrorCode errorCode) {
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();
}
}
class DiagnosticType implements IgnoredElement {
final String type;
final int offset;
final int length;
DiagnosticType(String type, this.offset, this.length)
: type = type.toLowerCase();
@override
bool matches(ErrorCode errorCode) =>
type == errorCode.type.name.toLowerCase();
}
abstract class IgnoredElement {
bool matches(ErrorCode errorCode);
}
/// 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 ('error_code_1,error_code2').
static final RegExp IGNORE_MATCHER = RegExp(r'//+[ ]*ignore:');
/// A regular expression for matching 'ignore_for_file' comments.
///
/// Resulting codes may be in a list ('error_code_1,error_code2').
static final RegExp IGNORE_FOR_FILE_MATCHER =
RegExp(r'//[ ]*ignore_for_file:');
/// 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 = [];
/// Initialize a newly created instance of this class to represent the ignore
/// comments in the given compilation [unit].
IgnoreInfo.forDart(CompilationUnit unit, String content) {
var 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);
}
}
}
/// Return `true` if there are any ignore comments in the file.
bool get hasIgnores =>
_ignoredOnLine.isNotEmpty || _ignoredForFile.isNotEmpty;
/// Return a list containing all of the diagnostics that are ignored for the
/// whole file.
List<IgnoredElement> get ignoredForFile => _ignoredForFile.toList();
/// Return 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;
}
/// Return `true` if the [errorCode] is ignored at the given [line].
bool ignoredAt(ErrorCode errorCode, int line) {
var ignoredDiagnostics = _ignoredOnLine[line];
if (ignoredForFile.isEmpty && ignoredDiagnostics == null) {
return false;
}
if (ignoredForFile.any((name) => name.matches(errorCode))) {
return true;
}
if (ignoredDiagnostics == null) {
return false;
}
return ignoredDiagnostics.any((name) => name.matches(errorCode));
}
}
extension on CompilationUnit {
/// Return all of the ignore comments in this compilation unit.
Iterable<CommentToken> get ignoreComments sync* {
Iterable<CommentToken> processPrecedingComments(Token currentToken) sync* {
var comment = currentToken.precedingComments;
while (comment != null) {
var lexeme = comment.lexeme;
if (lexeme.startsWith(IgnoreInfo.IGNORE_MATCHER)) {
yield comment;
} else if (lexeme.startsWith(IgnoreInfo.IGNORE_FOR_FILE_MATCHER)) {
yield comment;
}
comment = comment.next as CommentToken?;
}
}
var currentToken = beginToken;
while (currentToken != currentToken.next) {
yield* processPrecedingComments(currentToken);
currentToken = currentToken.next!;
}
yield* processPrecedingComments(currentToken);
}
}
extension on CommentToken {
/// The error codes currently do not contain dollar signs, so we can be a bit
/// more restrictive in this test.
static final _errorCodeNameRegExp = RegExp(r'^[a-zA-Z][_a-z0-9A-Z]*$');
static final _errorTypeRegExp =
RegExp(r'^type[ ]*=[ ]*lint', caseSensitive: false);
/// Return the diagnostic names contained in this comment, assuming that it is
/// a correctly formatted ignore comment.
Iterable<IgnoredElement> get ignoredElements sync* {
int offset = lexeme.indexOf(':') + 1;
var names = lexeme.substring(offset).split(',');
offset += this.offset;
for (var name in names) {
var trimmedName = name.trim();
if (trimmedName.isNotEmpty) {
if (trimmedName.contains(_errorCodeNameRegExp)) {
var innerOffset = name.indexOf(trimmedName);
yield DiagnosticName(trimmedName.toLowerCase(), offset + innerOffset);
} else {
var match = _errorTypeRegExp.matchAsPrefix(trimmedName);
if (match != null) {
var innerOffset = name.indexOf(trimmedName);
yield DiagnosticType('lint', offset + innerOffset, name.length);
}
}
}
offset += name.length + 1;
}
}
}