| // Copyright (c) 2020, 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/diagnostic/diagnostic.dart'; | |
| import 'package:analyzer/error/error.dart'; | |
| import 'package:analyzer/source/line_info.dart'; | |
| import 'package:analyzer/src/diagnostic/diagnostic.dart' as diag; | |
| import 'package:analyzer/src/error/codes.dart'; | |
| import 'package:analyzer/src/error/listener.dart'; | |
| import 'package:analyzer/src/ignore_comments/ignore_info.dart'; | |
| import 'package:analyzer/src/lint/registry.dart'; | |
| typedef UnnecessaryIgnoreDiagnosticCode = | |
| DiagnosticWithArguments< | |
| LocatableDiagnostic Function({required String name}) | |
| >; | |
| /// Used to validate the ignore comments in a single file. | |
| class IgnoreValidator { | |
| /// A list of known diagnostic codes used to ensure we don't over-report | |
| /// `unnecessary_ignore`s on error codes that may be contributed by a plugin. | |
| static final Set<String> _validDiagnosticCodeNames = diagnosticCodeValues | |
| .map((d) => d.lowerCaseName) | |
| .toSet(); | |
| /// Diagnostic codes used to report `unnecessary_ignore`s. | |
| /// | |
| /// These codes are set when the `UnnecessaryIgnore` lint rule is instantiated and | |
| /// registered by the linter. | |
| static late UnnecessaryIgnoreDiagnosticCode unnecessaryIgnoreLocationLintCode; | |
| static late UnnecessaryIgnoreDiagnosticCode unnecessaryIgnoreFileLintCode; | |
| static late UnnecessaryIgnoreDiagnosticCode | |
| unnecessaryIgnoreNameLocationLintCode; | |
| static late UnnecessaryIgnoreDiagnosticCode unnecessaryIgnoreNameFileLintCode; | |
| /// The diagnostic reporter to which diagnostics are to be reported. | |
| final DiagnosticReporter _diagnosticReporter; | |
| /// The diagnostics that are reported in the file being analyzed. | |
| final List<Diagnostic> _reportedDiagnostics; | |
| /// The information about the ignore comments in the file being analyzed. | |
| final IgnoreInfo _ignoreInfo; | |
| /// The line info for the file being analyzed. | |
| final LineInfo _lineInfo; | |
| /// A list of the names and unique names of all known error codes that can't | |
| /// be ignored. Note that this list is incomplete. Plugins might well define | |
| /// diagnostics with a severity of `ERROR`, but we won't be able to flag their | |
| /// use because we have no visibility of them here. | |
| final Set<String> _unignorableNames; | |
| /// Whether to validate unnecessary ignores (enabled by the `unnecessary_ignore` lint). | |
| final bool _validateUnnecessaryIgnores; | |
| /// Initialize a newly created validator to report any issues with ignore | |
| /// comments in the file being analyzed. The diagnostics will be reported to | |
| /// the [_diagnosticReporter]. | |
| IgnoreValidator( | |
| this._diagnosticReporter, | |
| this._reportedDiagnostics, | |
| this._ignoreInfo, | |
| this._lineInfo, | |
| this._unignorableNames, | |
| this._validateUnnecessaryIgnores, | |
| ) { | |
| assert(_unignorableNames.every((n) => n == n.toLowerCase())); | |
| } | |
| /// Report any issues with ignore comments in the file being analyzed. | |
| void reportErrors() { | |
| if (!_ignoreInfo.hasIgnores) { | |
| return; | |
| } | |
| var ignoredOnLineMap = _ignoreInfo.ignoredOnLine; | |
| var ignoredForFile = _ignoreInfo.ignoredForFile; | |
| // | |
| // Report and remove any un-ignorable or duplicated names. | |
| // | |
| var namesIgnoredForFile = <String>{}; | |
| var typesIgnoredForFile = <String>{}; | |
| var unignorable = <IgnoredDiagnosticName>[]; | |
| var duplicated = <IgnoredElement>[]; | |
| for (var ignoredElement in ignoredForFile) { | |
| if (ignoredElement is IgnoredDiagnosticName) { | |
| var name = ignoredElement.name; | |
| if (_unignorableNames.contains(name)) { | |
| unignorable.add(ignoredElement); | |
| } else if (!namesIgnoredForFile.add(name)) { | |
| duplicated.add(ignoredElement); | |
| } | |
| } else if (ignoredElement is IgnoredDiagnosticType) { | |
| if (!typesIgnoredForFile.add(ignoredElement.type)) { | |
| duplicated.add(ignoredElement); | |
| } | |
| } | |
| } | |
| _reportUnignorableAndDuplicateIgnores( | |
| unignorable, | |
| duplicated, | |
| ignoredForFile, | |
| ); | |
| for (var ignoredOnLine in ignoredOnLineMap.values) { | |
| var namedIgnoredOnLine = <String>{}; | |
| var typesIgnoredOnLine = <String>{}; | |
| var unignorable = <IgnoredElement>[]; | |
| var duplicated = <IgnoredElement>[]; | |
| for (var ignoredElement in ignoredOnLine) { | |
| if (ignoredElement is IgnoredDiagnosticName) { | |
| var name = ignoredElement.name; | |
| if (_unignorableNames.contains(name)) { | |
| unignorable.add(ignoredElement); | |
| } else if (namesIgnoredForFile.contains(name) || | |
| !namedIgnoredOnLine.add(name)) { | |
| duplicated.add(ignoredElement); | |
| } | |
| } else if (ignoredElement is IgnoredDiagnosticType) { | |
| var type = ignoredElement.type; | |
| if (typesIgnoredForFile.contains(type) || | |
| !typesIgnoredOnLine.add(type)) { | |
| duplicated.add(ignoredElement); | |
| } | |
| } | |
| } | |
| _reportUnignorableAndDuplicateIgnores( | |
| unignorable, | |
| duplicated, | |
| ignoredOnLine, | |
| ); | |
| } | |
| // | |
| // Remove all of the errors that are actually being ignored. | |
| // | |
| for (var error in _reportedDiagnostics) { | |
| var lineNumber = _lineInfo.getLocation(error.offset).lineNumber; | |
| var ignoredOnLine = ignoredOnLineMap[lineNumber]; | |
| ignoredForFile.removeByName(error.ignoreName); | |
| ignoredOnLine?.removeByName(error.ignoreName); | |
| } | |
| // | |
| // Report any remaining ignored names as being unnecessary. | |
| // | |
| _reportUnnecessaryOrRemovedOrDeprecatedIgnores( | |
| ignoredForFile, | |
| forFile: true, | |
| ); | |
| for (var ignoredOnLine in ignoredOnLineMap.values) { | |
| _reportUnnecessaryOrRemovedOrDeprecatedIgnores(ignoredOnLine); | |
| } | |
| } | |
| /// Report the names that are [unignorable] or [duplicated] and remove them | |
| /// from the [list] of names from which they were extracted. | |
| void _reportUnignorableAndDuplicateIgnores( | |
| List<IgnoredElement> unignorable, | |
| List<IgnoredElement> duplicated, | |
| List<IgnoredElement> list, | |
| ) { | |
| for (var unignorableName in unignorable) { | |
| if (unignorableName is IgnoredDiagnosticName) { | |
| var name = unignorableName.name; | |
| _diagnosticReporter.report( | |
| diag.unignorableIgnore | |
| .withArguments(diagnosticName: name) | |
| .atOffset(offset: unignorableName.offset, length: name.length), | |
| ); | |
| list.remove(unignorableName); | |
| } | |
| } | |
| for (var ignoredElement in duplicated) { | |
| if (ignoredElement is IgnoredDiagnosticName) { | |
| var name = ignoredElement.name; | |
| _diagnosticReporter.report( | |
| diag.duplicateIgnore | |
| .withArguments(name: name) | |
| .atOffset(offset: ignoredElement.offset, length: name.length), | |
| ); | |
| list.remove(ignoredElement); | |
| } else if (ignoredElement is IgnoredDiagnosticType) { | |
| _diagnosticReporter.report( | |
| diag.duplicateIgnore | |
| .withArguments(name: ignoredElement.type) | |
| .atOffset( | |
| offset: ignoredElement.offset, | |
| length: ignoredElement.length, | |
| ), | |
| ); | |
| list.remove(ignoredElement); | |
| } | |
| } | |
| } | |
| /// Report the [ignoredNames] as being unnecessary. | |
| void _reportUnnecessaryOrRemovedOrDeprecatedIgnores( | |
| List<IgnoredElement> ignoredNames, { | |
| bool forFile = false, | |
| }) { | |
| if (!_validateUnnecessaryIgnores) return; | |
| for (var ignoredName in ignoredNames) { | |
| if (ignoredName is IgnoredDiagnosticName) { | |
| var name = ignoredName.name; | |
| var rule = Registry.ruleRegistry.getRule(name); | |
| if (rule == null) { | |
| // If a code is not a lint or a recognized error, | |
| // don't report. (It could come from a plugin.) | |
| // TODO(pq): consider another diagnostic that reports undefined codes | |
| if (!_validDiagnosticCodeNames.contains(name.toLowerCase())) continue; | |
| } else { | |
| var state = rule.state; | |
| var since = state.since.toString(); | |
| if (state.isDeprecated) { | |
| // TODO(pq): implement. | |
| } else if (state.isRemoved) { | |
| var replacedBy = state.replacedBy; | |
| if (replacedBy != null) { | |
| _diagnosticReporter.report( | |
| diag.replacedLintUse | |
| .withArguments( | |
| ruleName: name, | |
| since: since, | |
| replacement: replacedBy, | |
| ) | |
| .atOffset(offset: ignoredName.offset, length: name.length), | |
| ); | |
| continue; | |
| } else { | |
| _diagnosticReporter.report( | |
| diag.removedLintUse | |
| .withArguments(ruleName: name, since: since) | |
| .atOffset(offset: ignoredName.offset, length: name.length), | |
| ); | |
| continue; | |
| } | |
| } | |
| } | |
| // We need to calculate if there are multiple diagnostic names in this | |
| // ignore comment. | |
| // | |
| // (This will determine what kind of fix to propose.) | |
| var currentLine = _lineInfo.getLocation(ignoredName.offset).lineNumber; | |
| // First we need to collect the (possibly) relevant ignores. | |
| late Iterable<IgnoredElement> ignoredElements; | |
| if (forFile) { | |
| ignoredElements = _ignoreInfo.ignoredForFile; | |
| } else { | |
| // To account for preceding and same-line ignore comments, we look at | |
| // the current line and its next. | |
| ignoredElements = { | |
| ...?_ignoreInfo.ignoredOnLine[currentLine], | |
| ...?_ignoreInfo.ignoredOnLine[currentLine + 1], | |
| }; | |
| } | |
| // Then we further narrow them down to ignored diagnostics that correspond | |
| // to the [currentLine]. | |
| var diagnosticsOnLine = 0; | |
| for (var ignore in ignoredElements) { | |
| if (ignore is IgnoredDiagnosticName) { | |
| var ignoreLine = _lineInfo.getLocation(ignore.offset).lineNumber; | |
| if (ignoreLine == currentLine) diagnosticsOnLine++; | |
| } | |
| } | |
| late UnnecessaryIgnoreDiagnosticCode lintCode; | |
| if (forFile) { | |
| lintCode = diagnosticsOnLine > 1 | |
| ? unnecessaryIgnoreNameFileLintCode | |
| : unnecessaryIgnoreFileLintCode; | |
| } else { | |
| lintCode = diagnosticsOnLine > 1 | |
| ? unnecessaryIgnoreNameLocationLintCode | |
| : unnecessaryIgnoreLocationLintCode; | |
| } | |
| _diagnosticReporter.report( | |
| lintCode | |
| .withArguments(name: name) | |
| .atOffset(offset: ignoredName.offset, length: name.length), | |
| ); | |
| } | |
| } | |
| } | |
| } | |
| extension on Diagnostic { | |
| String get ignoreName => diagnosticCode.lowerCaseName; | |
| } | |
| extension on List<IgnoredElement> { | |
| void removeByName(String name) { | |
| removeWhere( | |
| (ignoredElement) => | |
| ignoredElement is IgnoredDiagnosticName && | |
| ignoredElement.name == name, | |
| ); | |
| } | |
| } |