blob: 8f7b0b3c2b073ec12cfd66fe694e9b4f3ace3fea [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/source/line_info.dart';
import 'package:analyzer/src/dart/ast/token.dart';
/// The name and location of a diagnostic name in an ignore comment.
class DiagnosticName {
/// The name of the diagnostic being ignored.
final String name;
/// The offset of the diagnostic in the source file.
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.
bool matches(String errorCode) => name == errorCode;
}
/// Information about analysis `//ignore:` and `//ignore_for_file` comments
/// within a source file.
class IgnoreInfo {
/// Instance shared by all cases without matches.
// ignore: deprecated_member_use_from_same_package
static final IgnoreInfo _EMPTY_INFO = IgnoreInfo();
/// A regular expression for matching 'ignore' comments. Produces matches
/// containing 2 groups. For example:
///
/// * ['//ignore: error_code', 'error_code']
///
/// Resulting codes may be in a list ('error_code_1,error_code2').
static final RegExp IGNORE_MATCHER =
RegExp(r'//+[ ]*ignore:(.*)$', multiLine: true);
/// A regular expression for matching 'ignore_for_file' comments. Produces
/// matches containing 2 groups. For example:
///
/// * ['//ignore_for_file: error_code', 'error_code']
///
/// Resulting codes may be in a list ('error_code_1,error_code2').
static final RegExp IGNORE_FOR_FILE_MATCHER =
RegExp(r'//[ ]*ignore_for_file:(.*)$', multiLine: true);
/// A table mapping line numbers to the diagnostics that are ignored on that
/// line.
final Map<int, List<DiagnosticName>> _ignoredOnLine = {};
/// A list containing all of the diagnostics that are ignored for the whole
/// file.
final List<DiagnosticName> _ignoredForFile = [];
@Deprecated('Use the constructor IgnoreInfo.forDart')
IgnoreInfo();
/// 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;
String beforeMatch = content.substring(
lineInfo.getOffsetOfLine(lineNumber - 1),
lineInfo.getOffsetOfLine(lineNumber - 1) +
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.diagnosticNames);
} else if (lexeme.contains('ignore_for_file:')) {
_ignoredForFile.addAll(comment.diagnosticNames);
}
}
}
/// 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<DiagnosticName> get ignoredForFile => _ignoredForFile.toList();
/// Return a table mapping line numbers to the diagnostics that are ignored on
/// that line.
Map<int, List<DiagnosticName>> get ignoredOnLine {
Map<int, List<DiagnosticName>> 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(String errorCode, int line) {
for (var name in _ignoredForFile) {
if (name.matches(errorCode)) {
return true;
}
}
var ignoredOnLine = _ignoredOnLine[line];
if (ignoredOnLine != null) {
for (var name in ignoredOnLine) {
if (name.matches(errorCode)) {
return true;
}
}
}
return false;
}
/// Ignore these [errorCodes] at [line].
void _addAll(int line, Iterable<DiagnosticName> errorCodes) {
_ignoredOnLine.putIfAbsent(line, () => []).addAll(errorCodes);
}
/// Ignore these [errorCodes] in the whole file.
void _addAllForFile(Iterable<DiagnosticName> errorCodes) {
_ignoredForFile.addAll(errorCodes);
}
/// Calculate ignores for the given [content] with line [info].
@Deprecated('Use the constructor IgnoreInfo.forDart')
static IgnoreInfo calculateIgnores(String content, LineInfo info) {
Iterable<Match> matches = IGNORE_MATCHER.allMatches(content);
Iterable<Match> fileMatches = IGNORE_FOR_FILE_MATCHER.allMatches(content);
if (matches.isEmpty && fileMatches.isEmpty) {
return _EMPTY_INFO;
}
IgnoreInfo ignoreInfo = IgnoreInfo();
for (Match match in matches) {
// See _IGNORE_MATCHER for format --- note the possibility of error lists.
// Note that the offsets are not being computed here. This shouldn't
// affect older clients of this class because none of the previous APIs
// depended on having offsets.
Iterable<DiagnosticName> codes = match
.group(1)!
.split(',')
.map((String code) => DiagnosticName(code.trim().toLowerCase(), -1));
var location = info.getLocation(match.start);
int lineNumber = location.lineNumber;
String beforeMatch = content.substring(
info.getOffsetOfLine(lineNumber - 1),
info.getOffsetOfLine(lineNumber - 1) + location.columnNumber - 1);
if (beforeMatch.trim().isEmpty) {
// The comment is on its own line, so it refers to the next line.
ignoreInfo._addAll(lineNumber + 1, codes);
} else {
// The comment sits next to code, so it refers to its own line.
ignoreInfo._addAll(lineNumber, codes);
}
}
// Note that the offsets are not being computed here. This shouldn't affect
// older clients of this class because none of the previous APIs depended on
// having offsets.
for (Match match in fileMatches) {
Iterable<DiagnosticName> codes = match
.group(1)!
.split(',')
.map((String code) => DiagnosticName(code.trim().toLowerCase(), -1));
ignoreInfo._addAllForFile(codes);
}
return ignoreInfo;
}
}
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;
var match = IgnoreInfo.IGNORE_MATCHER.matchAsPrefix(lexeme);
if (match != null) {
yield comment;
} else {
match = IgnoreInfo.IGNORE_FOR_FILE_MATCHER.matchAsPrefix(lexeme);
if (match != null) {
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]*$');
/// Return the diagnostic names contained in this comment, assuming that it is
/// a correctly formatted ignore comment.
Iterable<DiagnosticName> get diagnosticNames sync* {
bool isValidErrorCodeName(String text) {
return text.contains(_errorCodeNameRegExp);
}
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 && isValidErrorCodeName(trimmedName)) {
var innerOffset = name.indexOf(trimmedName);
yield DiagnosticName(trimmedName.toLowerCase(), offset + innerOffset);
}
offset += name.length + 1;
}
}
}