blob: dfcab21e96c6951d405eeaf40c9a9d476a5a62cd [file] [log] [blame]
// 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/fix/analysis_options/fix_generator.dart';
import 'package:analysis_server/src/services/correction/fix/pubspec/fix_generator.dart';
import 'package:analysis_server/src/services/correction/fix_internal.dart';
import 'package:analysis_server_plugin/src/correction/fix_generators.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 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();
registerBuiltInProducers();
var lintRuleCodes = {
for (var rule in Registry.ruleRegistry.rules) ...rule.lintCodes,
};
var lintRuleNames = {
for (var lintCode in lintRuleCodes) lintCode.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';
if (code.hasFix) {
if (!markedAsHavingFix) {
errorData.codesWithFixes.add(name);
}
} else {
if (markedAsHavingFix) {
errorData.codesWithoutFixes.add(name);
}
}
}
}
for (var lintCode in lintRuleCodes) {
var name = lintCode.uniqueName;
var info = statusInfo.nodes[name];
if (info == null) {
errorData.codesWithNoEntry.add(name);
} else if (info is YamlMap) {
var markedAsHavingFix = info['status'] == 'hasFix';
if (lintCode.hasFix) {
if (!markedAsHavingFix) {
errorData.codesWithFixes.add(name);
}
} else {
if (markedAsHavingFix) {
errorData.codesWithoutFixes.add(name);
}
}
}
}
var errorCodeNames = {
for (var code in errorCodeValues) code.uniqueName,
};
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;
}
extension on ErrorCode {
/// Whether this [ErrorCode] is likely to have a fix associated with
/// it.
bool get hasFix {
var self = this;
if (self is LintCode) {
return registeredFixGenerators.lintProducers.containsKey(self) ||
registeredFixGenerators.lintMultiProducers.containsKey(self);
}
return registeredFixGenerators.nonLintProducers.containsKey(self) ||
registeredFixGenerators.nonLintMultiProducers.containsKey(self) ||
AnalysisOptionsFixGenerator.codesWithFixes.contains(self) ||
PubspecFixGenerator.codesWithFixes.contains(self);
}
}