Introduce first presubmit check for analyzer team.
See the issue below, and the code comments, for the further plans.
I plan on adding whichever ones we find useful, as long as I can
keep them performant. This one is performant, and we can discuss
possible problematic checks on the issue.
Work towards https://github.com/dart-lang/sdk/issues/53578
Change-Id: Ie3980e6194e46574a01ad3e0bd8e36f7ac248917
Reviewed-on: https://dart-review.googlesource.com/c/sdk/+/329620
Reviewed-by: Brian Wilkerson <brianwilkerson@google.com>
Reviewed-by: Sigmund Cherem <sigmund@google.com>
Reviewed-by: Phil Quitslund <pquitslund@google.com>
Reviewed-by: Kallen Tu <kallentu@google.com>
Reviewed-by: Konstantin Shcheglov <scheglov@google.com>
Commit-Queue: Samuel Rawlins <srawlins@google.com>
Reviewed-by: Jonas Termansen <sortie@google.com>
diff --git a/PRESUBMIT.py b/PRESUBMIT.py
index 3956ec7..c7f17e4 100644
--- a/PRESUBMIT.py
+++ b/PRESUBMIT.py
@@ -283,6 +283,49 @@
]
+def _CheckAnalyzerFiles(input_api, output_api):
+ """Run analyzer checks on source files."""
+
+ # The first (and so far, only) check, is to verify the "error fix status"
+ # file.
+ relevant_files = [
+ "pkg/analyzer/lib/src/error/error_code_values.g.dart",
+ "pkg/linter/lib/src/rules.dart",
+ ]
+
+ if any(f.LocalPath() in relevant_files for f in input_api.AffectedFiles()):
+ args = [
+ "tools/sdks/dart-sdk/bin/dart",
+ "pkg/analysis_server/tool/presubmit/verify_error_fix_status.dart",
+ ]
+ stdout = input_api.subprocess.check_output(args).strip()
+ if not stdout:
+ return []
+
+ return [
+ output_api.PresubmitError(
+ "The verify_error_fix_status Analyzer tool revealed issues:",
+ long_text=stdout)
+ ]
+
+ # TODO(srawlins): Check more:
+ # * "verify_sorted" for individual modified (not deleted) files in
+ # Analyzer-team-owned directories.
+ # * "verify_tests" for individual modified (not deleted) test files in
+ # Analyzer-team-owned directories.
+ # * Verify that `messages/generate.dart` does not produce different
+ # content, when `pkg/analyzer/messages.yaml` is modified.
+ # * Verify that `diagnostics/generate.dart` does not produce different
+ # content, when `pkg/analyzer/messages.yaml` is modified.
+ # * Verify that `machine.json` is not outdated, when any
+ # `pkg/linter/lib/src/rules` file is modified.
+ # * Maybe "verify_no_solo" for individual modified (not deleted test files
+ # in Analyzer-team-owned directories.
+
+ # No files are relevant.
+ return []
+
+
def _CheckTestMatrixValid(input_api, output_api):
"""Run script to check that the test matrix has no errors."""
@@ -348,6 +391,7 @@
results.extend(
input_api.canned_checks.CheckPatchFormatted(input_api, output_api))
results.extend(_CheckCopyrightYear(input_api, output_api))
+ results.extend(_CheckAnalyzerFiles(input_api, output_api))
return results
diff --git a/pkg/analysis_server/test/verify_error_fix_status_test.dart b/pkg/analysis_server/test/verify_error_fix_status_test.dart
index 8b3cec0..01f8983 100644
--- a/pkg/analysis_server/test/verify_error_fix_status_test.dart
+++ b/pkg/analysis_server/test/verify_error_fix_status_test.dart
@@ -2,16 +2,10 @@
// 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:analysis_server/src/services/correction/dart/data_driven.dart';
-import 'package:analysis_server/src/services/correction/fix_internal.dart';
-import 'package:analyzer/error/error.dart';
-import 'package:analyzer/file_system/physical_file_system.dart';
-import 'package:analyzer/src/lint/registry.dart';
-import 'package:analyzer_utilities/package_root.dart' as package_root;
-import 'package:linter/src/rules.dart';
import 'package:test/test.dart';
import 'package:test_reflective_loader/test_reflective_loader.dart';
-import 'package:yaml/yaml.dart';
+
+import '../tool/presubmit/verify_error_fix_status.dart';
void main() {
defineReflectiveSuite(() {
@@ -21,192 +15,10 @@
@reflectiveTest
class VerifyErrorFixStatusTest {
- PhysicalResourceProvider resourceProvider = PhysicalResourceProvider.INSTANCE;
-
void test_statusFile() {
- var statusInfo = _statusInfo();
- var errorCodeNames = _errorCodeNames();
- var lintRuleNames = _lintRuleNames();
-
- var errorData = _ErrorData();
- for (var code in errorCodeValues) {
- var name = code.uniqueName;
- if (name.startsWith('TodoCode.')) {
- // To-do codes are ignored.
- } else {
- var info = statusInfo.nodes[name];
- if (info == null) {
- errorData.codesWithNoEntry.add(name);
- } else if (info is YamlMap) {
- var markedAsHavingFix = info['status'] == 'hasFix';
- var hasFix = _hasCodeFix(code);
- if (hasFix) {
- if (!markedAsHavingFix) {
- errorData.codesWithFixes.add(name);
- }
- } else {
- if (markedAsHavingFix) {
- errorData.codesWithoutFixes.add(name);
- }
- }
- }
- }
- }
- for (var name in lintRuleNames) {
- var info = statusInfo.nodes[name];
- if (info == null) {
- errorData.codesWithNoEntry.add(name);
- } else if (info is YamlMap) {
- var markedAsHavingFix = info['status'] == 'hasFix';
- var hasFix = _hasLintFix(name);
- if (hasFix) {
- if (!markedAsHavingFix) {
- errorData.codesWithFixes.add(name);
- }
- } else {
- if (markedAsHavingFix) {
- errorData.codesWithoutFixes.add(name);
- }
- }
- }
- }
-
- for (var key in statusInfo.keys) {
- if (key is String) {
- if (!errorCodeNames.contains(key) && !lintRuleNames.contains(key)) {
- errorData.entriesWithNoCode.add(key);
- }
- }
- }
-
- if (errorData.isNotEmpty) {
- fail(_failureMessage(errorData));
+ var errors = verifyErrorFixStatus();
+ if (errors != null) {
+ fail(errors);
}
}
-
- /// Return the unique names of the error codes.
- Set<String> _errorCodeNames() {
- var codes = errorCodeValues;
- var codeNames = <String>{};
- for (var code in codes) {
- codeNames.add(code.uniqueName);
- }
- return codeNames;
- }
-
- /// Return a failure message composed from the given lists.
- String _failureMessage(_ErrorData errorData) {
- var buffer = StringBuffer('In ${_statusFilePath()}:\n');
- var needsBlankLine = false;
- if (errorData.codesWithNoEntry.isNotEmpty) {
- buffer.writeln('Add the following entries:');
- buffer.writeln();
- for (var code in errorData.codesWithNoEntry) {
- buffer.writeln('$code:');
- buffer.writeln(' status: needsEvaluation');
- }
- needsBlankLine = true;
- }
- if (errorData.entriesWithNoCode.isNotEmpty) {
- if (needsBlankLine) {
- buffer.writeln();
- }
- buffer.writeln('Remove the following entries:');
- for (var code in errorData.entriesWithNoCode) {
- buffer.writeln('- $code');
- }
- needsBlankLine = true;
- }
- if (errorData.codesWithFixes.isNotEmpty) {
- if (needsBlankLine) {
- buffer.writeln();
- }
- buffer.writeln('Mark the following entries as having fixes:');
- for (var code in errorData.codesWithFixes) {
- buffer.writeln('- $code');
- }
- needsBlankLine = true;
- }
- if (errorData.codesWithoutFixes.isNotEmpty) {
- if (needsBlankLine) {
- buffer.writeln();
- }
- buffer.writeln('Mark the following entries as not having fixes:');
- for (var code in errorData.codesWithoutFixes) {
- buffer.writeln('- $code');
- }
- needsBlankLine = true;
- }
- return buffer.toString();
- }
-
- /// Return `true` if the given error [code] has a fix associated with it.
- bool _hasCodeFix(ErrorCode code) {
- var producers = FixProcessor.nonLintProducerMap[code];
- if (producers != null) {
- return true;
- }
- var multiProducers = FixProcessor.nonLintMultiProducerMap[code];
- if (multiProducers != null) {
- for (var producer in multiProducers) {
- if (producer is! DataDriven) {
- return true;
- }
- }
- }
- return false;
- }
-
- /// Return `true` if the lint with the given name has a fix associated with
- /// it.
- bool _hasLintFix(String codeName) {
- var name = codeName.substring('LintCode.'.length);
- var producers = FixProcessor.lintProducerMap[name];
- return producers != null;
- }
-
- /// Return the unique names of the lint rules.
- Set<String> _lintRuleNames() {
- registerLintRules();
- var ruleNames = <String>{};
- for (var rule in Registry.ruleRegistry.rules) {
- for (var code in rule.lintCodes) {
- ruleNames.add(code.uniqueName);
- }
- }
- return ruleNames;
- }
-
- /// Return the path to the file containing the status information.
- String _statusFilePath() {
- var pathContext = resourceProvider.pathContext;
- var packageRoot = pathContext.normalize(package_root.packageRoot);
- return pathContext.join(packageRoot, 'analysis_server', 'lib', 'src',
- 'services', 'correction', 'error_fix_status.yaml');
- }
-
- /// Return the content of the file containing the status information, parsed
- /// as a YAML map.
- YamlMap _statusInfo() {
- var statusFile = resourceProvider.getFile(_statusFilePath());
- var document = loadYamlDocument(statusFile.readAsStringSync());
- var statusInfo = document.contents;
- if (statusInfo is! YamlMap) {
- fail('Expected a YamlMap, found ${statusInfo.runtimeType}');
- }
- return statusInfo;
- }
-}
-
-class _ErrorData {
- final List<String> codesWithFixes = [];
- final List<String> codesWithNoEntry = [];
- final List<String> codesWithoutFixes = [];
- final List<String> entriesWithNoCode = [];
-
- bool get isNotEmpty =>
- codesWithFixes.isNotEmpty ||
- codesWithNoEntry.isNotEmpty ||
- codesWithoutFixes.isNotEmpty ||
- entriesWithNoCode.isNotEmpty;
}
diff --git a/pkg/analysis_server/tool/presubmit/verify_error_fix_status.dart b/pkg/analysis_server/tool/presubmit/verify_error_fix_status.dart
new file mode 100644
index 0000000..c281c60
--- /dev/null
+++ b/pkg/analysis_server/tool/presubmit/verify_error_fix_status.dart
@@ -0,0 +1,206 @@
+// Copyright (c) 2023, 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 'dart:io';
+
+import 'package:analysis_server/src/services/correction/dart/data_driven.dart';
+import 'package:analysis_server/src/services/correction/fix_internal.dart';
+import 'package:analyzer/error/error.dart';
+import 'package:analyzer/file_system/physical_file_system.dart';
+import 'package:analyzer/src/lint/registry.dart';
+import 'package:analyzer_utilities/package_root.dart' as package_root;
+import 'package:linter/src/rules.dart';
+import 'package:yaml/yaml.dart';
+
+void main() {
+ var errors = verifyErrorFixStatus();
+ if (errors != null) {
+ print(errors);
+ exitCode = 1;
+ }
+}
+
+PhysicalResourceProvider resourceProvider = PhysicalResourceProvider.INSTANCE;
+
+/// Returns whether the given error [code] has a fix associated with it.
+bool hasCodeFix(ErrorCode code) {
+ var producers = FixProcessor.nonLintProducerMap[code];
+ if (producers != null) {
+ return true;
+ }
+ var multiProducers = FixProcessor.nonLintMultiProducerMap[code];
+ if (multiProducers != null) {
+ for (var producer in multiProducers) {
+ if (producer is! DataDriven) {
+ return true;
+ }
+ }
+ }
+ return false;
+}
+
+/// Returns `true` if the lint rule with the given name has a fix associated
+/// with it.
+bool hasLintFix(String codeName) {
+ var name = codeName.substring('LintCode.'.length);
+ var producers = FixProcessor.lintProducerMap[name];
+ return producers != null;
+}
+
+/// Returns the path to the file containing the status information.
+String statusFilePath() {
+ var pathContext = resourceProvider.pathContext;
+ var packageRoot = pathContext.normalize(package_root.packageRoot);
+ return pathContext.join(packageRoot, 'analysis_server', 'lib', 'src',
+ 'services', 'correction', 'error_fix_status.yaml');
+}
+
+/// Verifies that the "error fix status" file is up-to-date and returns an error
+/// string describing how the file is out-of-date, or `null` if the file is
+/// up-to-date.
+String? verifyErrorFixStatus() {
+ var (error, statusInfo) = _statusInfo();
+ if (error != null) {
+ return error;
+ }
+ statusInfo!; // This is non-null when `error` is `null`.
+ registerLintRules();
+ var errorCodeNames = {
+ for (var code in errorCodeValues) code.uniqueName,
+ };
+ var lintRuleNames = {
+ for (var rule in Registry.ruleRegistry.rules)
+ ...rule.lintCodes.map((e) => e.uniqueName),
+ };
+
+ var errorData = ErrorData();
+ for (var code in errorCodeValues) {
+ var name = code.uniqueName;
+ if (name.startsWith('TodoCode.')) {
+ // To-do codes are ignored.
+ continue;
+ }
+
+ var info = statusInfo.nodes[name];
+ if (info == null) {
+ errorData.codesWithNoEntry.add(name);
+ } else if (info is YamlMap) {
+ var markedAsHavingFix = info['status'] == 'hasFix';
+ var hasFix = hasCodeFix(code);
+ if (hasFix) {
+ if (!markedAsHavingFix) {
+ errorData.codesWithFixes.add(name);
+ }
+ } else {
+ if (markedAsHavingFix) {
+ errorData.codesWithoutFixes.add(name);
+ }
+ }
+ }
+ }
+ for (var name in lintRuleNames) {
+ var info = statusInfo.nodes[name];
+ if (info == null) {
+ errorData.codesWithNoEntry.add(name);
+ } else if (info is YamlMap) {
+ var markedAsHavingFix = info['status'] == 'hasFix';
+ var hasFix = hasLintFix(name);
+ if (hasFix) {
+ if (!markedAsHavingFix) {
+ errorData.codesWithFixes.add(name);
+ }
+ } else {
+ if (markedAsHavingFix) {
+ errorData.codesWithoutFixes.add(name);
+ }
+ }
+ }
+ }
+
+ for (var key in statusInfo.keys) {
+ if (key is String) {
+ if (!errorCodeNames.contains(key) && !lintRuleNames.contains(key)) {
+ errorData.entriesWithNoCode.add(key);
+ }
+ }
+ }
+
+ if (errorData.isNotEmpty) {
+ return errorData.failureMessage;
+ }
+
+ // No errors.
+ return null;
+}
+
+/// Returns the content of the file containing the status information, parsed
+/// as a YAML map.
+(String? error, YamlMap? info) _statusInfo() {
+ var statusFile = resourceProvider.getFile(statusFilePath());
+ var document = loadYamlDocument(statusFile.readAsStringSync());
+ var statusInfo = document.contents;
+ if (statusInfo is! YamlMap) {
+ return ('Expected a YamlMap, found ${statusInfo.runtimeType}', null);
+ }
+ return (null, statusInfo);
+}
+
+class ErrorData {
+ final List<String> codesWithFixes = [];
+ final List<String> codesWithNoEntry = [];
+ final List<String> codesWithoutFixes = [];
+ final List<String> entriesWithNoCode = [];
+
+ /// A failure message composed from the various lists.
+ String get failureMessage {
+ var buffer = StringBuffer('In ${statusFilePath()}:\n');
+ var needsBlankLine = false;
+ if (codesWithNoEntry.isNotEmpty) {
+ buffer.writeln('Add the following entries:');
+ buffer.writeln();
+ for (var code in codesWithNoEntry) {
+ buffer.writeln('$code:');
+ buffer.writeln(' status: needsEvaluation');
+ }
+ needsBlankLine = true;
+ }
+ if (entriesWithNoCode.isNotEmpty) {
+ if (needsBlankLine) {
+ buffer.writeln();
+ }
+ buffer.writeln('Remove the following entries:');
+ for (var code in entriesWithNoCode) {
+ buffer.writeln('- $code');
+ }
+ needsBlankLine = true;
+ }
+ if (codesWithFixes.isNotEmpty) {
+ if (needsBlankLine) {
+ buffer.writeln();
+ }
+ buffer.writeln('Mark the following entries as having fixes:');
+ for (var code in codesWithFixes) {
+ buffer.writeln('- $code');
+ }
+ needsBlankLine = true;
+ }
+ if (codesWithoutFixes.isNotEmpty) {
+ if (needsBlankLine) {
+ buffer.writeln();
+ }
+ buffer.writeln('Mark the following entries as not having fixes:');
+ for (var code in codesWithoutFixes) {
+ buffer.writeln('- $code');
+ }
+ needsBlankLine = true;
+ }
+ return buffer.toString();
+ }
+
+ bool get isNotEmpty =>
+ codesWithFixes.isNotEmpty ||
+ codesWithNoEntry.isNotEmpty ||
+ codesWithoutFixes.isNotEmpty ||
+ entriesWithNoCode.isNotEmpty;
+}