DAS plugins: Support plugin-namespaced inline ignore comments
Fixes https://github.com/dart-lang/sdk/issues/59647
Change-Id: I303c83c3a486d6eb495e890007127215357e2864
Reviewed-on: https://dart-review.googlesource.com/c/sdk/+/406686
Reviewed-by: Brian Wilkerson <brianwilkerson@google.com>
Commit-Queue: Samuel Rawlins <srawlins@google.com>
diff --git a/pkg/analysis_server_plugin/lib/src/plugin_server.dart b/pkg/analysis_server_plugin/lib/src/plugin_server.dart
index dc4032b..778f25e 100644
--- a/pkg/analysis_server_plugin/lib/src/plugin_server.dart
+++ b/pkg/analysis_server_plugin/lib/src/plugin_server.dart
@@ -26,6 +26,7 @@
import 'package:analyzer/src/dart/analysis/file_content_cache.dart';
import 'package:analyzer/src/dart/analysis/session.dart';
import 'package:analyzer/src/dart/element/type_system.dart';
+import 'package:analyzer/src/ignore_comments/ignore_info.dart';
import 'package:analyzer/src/lint/linter.dart';
import 'package:analyzer/src/lint/linter_visitor.dart';
import 'package:analyzer/src/lint/registry.dart';
@@ -217,7 +218,7 @@
}) async {
var file = _resourceProvider.getFile(path);
var analysisOptions = analysisContext.getAnalysisOptionsForFile(file);
- var diagnostics = await _computeLints(
+ var diagnostics = await _computeDiagnostics(
analysisContext,
path,
analysisOptions: analysisOptions as AnalysisOptionsImpl,
@@ -244,7 +245,7 @@
}
}
- Future<List<protocol.AnalysisError>> _computeLints(
+ Future<List<protocol.AnalysisError>> _computeDiagnostics(
AnalysisContext analysisContext,
String path, {
required AnalysisOptionsImpl analysisOptions,
@@ -252,11 +253,11 @@
var libraryResult =
await analysisContext.currentSession.getResolvedLibrary(path);
if (libraryResult is! ResolvedLibraryResult) {
- return [];
+ return const [];
}
var unitResult = await analysisContext.currentSession.getResolvedUnit(path);
if (unitResult is! ResolvedUnitResult) {
- return [];
+ return const [];
}
var listener = RecordingErrorListener();
var errorReporter = ErrorReporter(
@@ -293,6 +294,9 @@
null,
);
+ // A mapping from each lint and warning code to its corresponding plugin.
+ var pluginCodeMapping = <String, String>{};
+
for (var configuration in analysisOptions.pluginConfigurations) {
if (!configuration.isEnabled) continue;
// TODO(srawlins): Namespace rules by their plugin, to avoid collisions.
@@ -304,15 +308,33 @@
// `benchhmark.dart` script does.
rule.registerNodeProcessors(nodeRegistry, context);
}
+ for (var code in rules.expand((r) => r.lintCodes)) {
+ var existingPlugin = pluginCodeMapping[code.name];
+ if (existingPlugin == null) {
+ pluginCodeMapping[code.name] = configuration.name;
+ }
+ }
}
context.currentUnit = currentUnit;
currentUnit.unit.accept(
AnalysisRuleVisitor(nodeRegistry, shouldPropagateExceptions: true));
+
+ var ignoreInfo = IgnoreInfo.forDart(unitResult.unit, unitResult.content);
+ var errors = listener.errors.where((e) {
+ var pluginName = pluginCodeMapping[e.errorCode.name];
+ if (pluginName == null) {
+ // If [e] is somehow not mapped, something is wrong; but don't mark it
+ // as ignored.
+ return true;
+ }
+ return !ignoreInfo.ignored(e, pluginName: pluginName);
+ });
+
// The list of the `AnalysisError`s and their associated
// `protocol.AnalysisError`s.
var errorsAndProtocolErrors = [
- for (var e in listener.errors)
+ for (var e in errors)
(
error: e,
protocolError: protocol.AnalysisError(
diff --git a/pkg/analysis_server_plugin/pubspec.yaml b/pkg/analysis_server_plugin/pubspec.yaml
index b871245..6871bdd 100644
--- a/pkg/analysis_server_plugin/pubspec.yaml
+++ b/pkg/analysis_server_plugin/pubspec.yaml
@@ -7,7 +7,7 @@
sdk: '>=3.3.0 <4.0.0'
dependencies:
- analyzer: ^7.2.0
+ analyzer: ^7.3.0
analyzer_plugin: ^0.13.0
meta: ^1.16.0
diff --git a/pkg/analysis_server_plugin/test/src/plugin_server_test.dart b/pkg/analysis_server_plugin/test/src/plugin_server_test.dart
index 61a7f7d..9c92c9b 100644
--- a/pkg/analysis_server_plugin/test/src/plugin_server_test.dart
+++ b/pkg/analysis_server_plugin/test/src/plugin_server_test.dart
@@ -48,6 +48,33 @@
await startPlugin();
}
+ Future<void> test_diagnosticsCanBeIgnored() async {
+ writeAnalysisOptionsWithPlugin();
+ newFile(filePath, '''
+// ignore: no_literals/no_bools
+bool b = false;
+''');
+ await channel
+ .sendRequest(protocol.AnalysisSetContextRootsParams([contextRoot]));
+ var paramsQueue = _analysisErrorsParams;
+ var params = await paramsQueue.next;
+ expect(params.errors, isEmpty);
+ }
+
+ Future<void> test_diagnosticsCanBeIgnored_forFile() async {
+ writeAnalysisOptionsWithPlugin();
+ newFile(filePath, '''
+bool b = false;
+
+// ignore_for_file: no_literals/no_bools
+''');
+ await channel
+ .sendRequest(protocol.AnalysisSetContextRootsParams([contextRoot]));
+ var paramsQueue = _analysisErrorsParams;
+ var params = await paramsQueue.next;
+ expect(params.errors, isEmpty);
+ }
+
Future<void> test_handleAnalysisSetContextRoots() async {
writeAnalysisOptionsWithPlugin();
newFile(filePath, 'bool b = false;');
diff --git a/pkg/analyzer/lib/src/ignore_comments/ignore_info.dart b/pkg/analyzer/lib/src/ignore_comments/ignore_info.dart
index 279a448..1e713f3 100644
--- a/pkg/analyzer/lib/src/ignore_comments/ignore_info.dart
+++ b/pkg/analyzer/lib/src/ignore_comments/ignore_info.dart
@@ -18,7 +18,7 @@
IgnoredDiagnosticComment(this.text, this.offset);
@override
- bool matches(ErrorCode errorCode) => false;
+ bool _matches(ErrorCode errorCode, {String? pluginName}) => false;
}
/// The name and location of a diagnostic name in an ignore comment.
@@ -26,12 +26,18 @@
/// The name of the diagnostic being ignored.
final String name;
+ final String? pluginName;
+
final int offset;
- IgnoredDiagnosticName(String name, this.offset) : name = name.toLowerCase();
+ IgnoredDiagnosticName(String name, this.offset, {this.pluginName})
+ : name = name.toLowerCase();
@override
- bool matches(ErrorCode errorCode) {
+ bool _matches(ErrorCode errorCode, {String? pluginName}) {
+ if (this.pluginName != pluginName) {
+ return false;
+ }
if (name == errorCode.name.toLowerCase()) {
return true;
}
@@ -56,7 +62,8 @@
: type = type.toLowerCase();
@override
- bool matches(ErrorCode errorCode) {
+ bool _matches(ErrorCode errorCode, {String? pluginName}) {
+ // Ignore 'pluginName'; it is irrelevant in an IgnoredDiagnosticType.
return switch (errorCode.type) {
ErrorType.HINT => type == 'hint',
ErrorType.LINT => type == 'lint',
@@ -69,7 +76,7 @@
sealed class IgnoredElement {
/// Returns whether this matches the given [errorCode].
- bool matches(ErrorCode errorCode);
+ bool _matches(ErrorCode errorCode, {String? pluginName});
}
/// Information about analysis `//ignore:` and `//ignore_for_file:` comments
@@ -178,24 +185,27 @@
return ignoredOnLine;
}
- bool ignored(AnalysisError error) {
+ /// Whether [error] is ignored via an inline "ignore" comment.
+ bool ignored(AnalysisError error, {String? pluginName}) {
var line = _lineInfo.getLocation(error.offset).lineNumber;
- return ignoredAt(error.errorCode, line);
+ return _ignoredAt(error.errorCode, line, pluginName: pluginName);
}
/// Returns whether the [errorCode] is ignored at the given [line].
- bool ignoredAt(ErrorCode errorCode, int 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))) {
+ if (ignoredForFile
+ .any((name) => name._matches(errorCode, pluginName: pluginName))) {
return true;
}
if (ignoredDiagnostics == null) {
return false;
}
- return ignoredDiagnostics.any((name) => name.matches(errorCode));
+ return ignoredDiagnostics
+ .any((name) => name._matches(errorCode, pluginName: pluginName));
}
}
@@ -259,8 +269,8 @@
// Parse diagnostic type.
skipPastWhitespace();
if (offset == lexeme.length) return;
- var equalSign = lexeme.codeUnitAt(offset);
- if (equalSign != 0x3D) return;
+ var nextChar = lexeme.codeUnitAt(offset);
+ if (!nextChar.isEqual) return;
offset++;
skipPastWhitespace();
if (offset == lexeme.length) return;
@@ -293,6 +303,30 @@
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) {
@@ -306,7 +340,8 @@
}
}
hasIgnoredElements = true;
- yield IgnoredDiagnosticName(word, this.offset + wordOffset);
+ yield IgnoredDiagnosticName(word, this.offset + wordOffset,
+ pluginName: pluginName);
}
if (offset == lexeme.length) return;
diff --git a/pkg/analyzer/lib/src/utilities/extensions/string.dart b/pkg/analyzer/lib/src/utilities/extensions/string.dart
index 93cb8c6..4d8bb29 100644
--- a/pkg/analyzer/lib/src/utilities/extensions/string.dart
+++ b/pkg/analyzer/lib/src/utilities/extensions/string.dart
@@ -11,6 +11,8 @@
/// character.
bool get isEOL => this == 0x0D || this == 0x0A;
+ bool get isEqual => this == 0x3D;
+
bool get isLetter =>
(this >= 0x41 && this <= 0x5A) || (this >= 0x61 && this <= 0x7A);
@@ -18,6 +20,8 @@
bool get isLetterOrDigitOrUnderscore => isLetter || isDigit || this == 0x5F;
+ bool get isSlash => this == 0x2F;
+
/// Whether this, as an ASCII character, is a space or tab character.
bool get isSpace => this == 0x20 || this == 0x09;
diff --git a/pkg/analyzer/test/src/ignore_comments/ignore_info_test.dart b/pkg/analyzer/test/src/ignore_comments/ignore_info_test.dart
index 49a24d8..ec8d71d 100644
--- a/pkg/analyzer/test/src/ignore_comments/ignore_info_test.dart
+++ b/pkg/analyzer/test/src/ignore_comments/ignore_info_test.dart
@@ -35,6 +35,11 @@
expect(ignoredElements, isEmpty);
}
+ test_noIgnores_trailingSlash() async {
+ var ignoredElements = await _parseIgnoredElements('// ignore: /');
+ expect(ignoredElements, isEmpty);
+ }
+
test_noIgnores_trailingWhitespace() async {
var ignoredElements = await _parseIgnoredElements('// ignore: ');
expect(ignoredElements, isEmpty);
@@ -59,6 +64,24 @@
_expectIgnoredName(ignoredElements[0], name: 'foo', offset: 10);
}
+ test_pluginName() async {
+ var ignoredElements =
+ await _parseIgnoredElements('// ignore: plugin_one/foo');
+ expect(ignoredElements, hasLength(1));
+ _expectIgnoredName(ignoredElements[0],
+ name: 'foo', offset: 11, pluginName: 'plugin_one');
+ }
+
+ test_pluginName_multiple() async {
+ var ignoredElements = await _parseIgnoredElements(
+ '// ignore: plugin_one/foo, plugin_two/bar');
+ expect(ignoredElements, hasLength(2));
+ _expectIgnoredName(ignoredElements[0],
+ name: 'foo', offset: 11, pluginName: 'plugin_one');
+ _expectIgnoredName(ignoredElements[1],
+ name: 'bar', offset: 27, pluginName: 'plugin_two');
+ }
+
test_trailingComma() async {
var ignoredElements = await _parseIgnoredElements('// ignore: foo,');
expect(ignoredElements, hasLength(1));
@@ -78,6 +101,16 @@
_expectIgnoredName(ignoredElements[0], name: 'foo', offset: 11);
}
+ test_trailingSlash() async {
+ var ignoredElements = await _parseIgnoredElements('// ignore: foo/');
+ expect(ignoredElements, isEmpty);
+ }
+
+ test_trailingSlashAndSpace() async {
+ var ignoredElements = await _parseIgnoredElements('// ignore: foo/ ');
+ expect(ignoredElements, isEmpty);
+ }
+
test_trailingSpace() async {
var ignoredElements = await _parseIgnoredElements('// ignore: foo ');
expect(ignoredElements, hasLength(1));
@@ -135,12 +168,14 @@
IgnoredElement element, {
required String name,
required int offset,
+ String? pluginName,
}) =>
expect(
element,
isA<IgnoredDiagnosticName>()
.having((e) => e.name, 'name', name)
- .having((e) => e.offset, 'offset', offset),
+ .having((e) => e.offset, 'offset', offset)
+ .having((e) => e.pluginName, 'pluginName', pluginName),
);
void _expectIgnoredType(