linter: inline LintDriver code into TestLinter

Change-Id: I1eb821f2e0365204bd415b630fb6a7e5a7ba47a7
Reviewed-on: https://dart-review.googlesource.com/c/sdk/+/444926
Commit-Queue: Brian Wilkerson <brianwilkerson@google.com>
Auto-Submit: Samuel Rawlins <srawlins@google.com>
Reviewed-by: Brian Wilkerson <brianwilkerson@google.com>
This commit is contained in:
Sam Rawlins
2025-08-13 10:28:40 -07:00
committed by Commit Queue
parent e1619544a2
commit 23a63f2542
4 changed files with 127 additions and 178 deletions
+40 -40
View File
@@ -20,7 +20,6 @@ import 'package:path/path.dart' as path;
import 'package:yaml/yaml.dart';
import 'lint_sets.dart';
import 'linter_options.dart';
import 'test_linter.dart';
/// Benchmarks lint rules.
@@ -62,16 +61,20 @@ Iterable<File> collectFiles(String entityPath) {
return files;
}
Future<void> lintFiles(TestLinter linter, List<File> filesToLint) async {
Future<void> lintFiles(
List<File> filesToLint, {
required List<AbstractAnalysisRule> rules,
required String? dartSdkPath,
}) async {
// Setup an error watcher to track whether an error was logged to stderr so
// we can set the exit code accordingly.
var errorWatcher = _ErrorWatchingSink(errorSink);
errorSink = errorWatcher;
var errors = await linter.lintFiles(filesToLint);
var diagnostics = await TestLinter(rules, dartSdkPath).lintFiles(filesToLint);
if (errorWatcher.encounteredError) {
exitCode = loggedAnalyzerErrorExitCode;
} else if (errors.isNotEmpty) {
exitCode = _maxSeverity(errors);
} else if (diagnostics.isNotEmpty) {
exitCode = _maxSeverity(diagnostics);
}
}
@@ -116,7 +119,7 @@ Future<void> runLinter(List<String> args) async {
return;
}
if (options['help'] as bool) {
if (options.flag('help')) {
printUsage(parser, outSink);
return;
}
@@ -132,25 +135,21 @@ Future<void> runLinter(List<String> args) async {
return;
}
var configFile = options['config'];
var ruleNames = options['rules'];
var customSdk = options.option('dart-sdk');
var configFile = options.option('config');
var ruleNames = options.multiOption('rules');
var dartSdkPath = options.option('dart-sdk');
LinterOptions linterOptions;
if (configFile is String) {
var optionsContent = readFile(configFile);
List<AbstractAnalysisRule> rules;
if (configFile != null) {
var optionsContent = File(configFile).readAsStringSync();
var options = loadYamlNode(optionsContent) as YamlMap;
var ruleConfigs = parseLinterSection(options)!.values;
var enabledRules = Registry.ruleRegistry.where(
(rule) => !ruleConfigs.any((rc) => rc.disables(rule.name)),
);
linterOptions = LinterOptions(
enabledRules: enabledRules,
dartSdkPath: customSdk,
);
} else if (ruleNames is Iterable<String> && ruleNames.isNotEmpty) {
var rules = <AbstractAnalysisRule>[];
rules =
Registry.ruleRegistry
.where((rule) => !ruleConfigs.any((rc) => rc.disables(rule.name)))
.toList();
} else if (ruleNames.isNotEmpty) {
rules = <AbstractAnalysisRule>[];
for (var ruleName in ruleNames) {
var rule = Registry.ruleRegistry[ruleName];
if (rule == null) {
@@ -159,9 +158,8 @@ Future<void> runLinter(List<String> args) async {
}
rules.add(rule);
}
linterOptions = LinterOptions(enabledRules: rules, dartSdkPath: customSdk);
} else {
linterOptions = LinterOptions(dartSdkPath: customSdk);
rules = Registry.ruleRegistry.toList();
}
var filesToLint = [
@@ -171,17 +169,23 @@ Future<void> runLinter(List<String> args) async {
).map((file) => file.path.toAbsoluteNormalizedPath()).map(File.new),
];
await writeBenchmarks(outSink, filesToLint, linterOptions);
await writeBenchmarks(
outSink,
filesToLint,
rules: rules,
dartSdkPath: dartSdkPath,
);
}
Future<void> writeBenchmarks(
StringSink out,
List<File> filesToLint,
LinterOptions linterOptions,
) async {
List<File> filesToLint, {
required List<AbstractAnalysisRule> rules,
required String? dartSdkPath,
}) async {
var timings = <String, int>{};
for (var i = 0; i < benchmarkRuns; ++i) {
await lintFiles(TestLinter(linterOptions), filesToLint);
await lintFiles(filesToLint, rules: rules, dartSdkPath: dartSdkPath);
analysisRuleTimers.timers.forEach((n, t) {
var timing = t.elapsedMilliseconds;
var previous = timings[n];
@@ -195,18 +199,13 @@ Future<void> writeBenchmarks(
var stats =
timings.keys.map((t) {
var sets = <String>[];
if (coreRuleset.contains(t)) {
sets.add('core');
}
if (recommendedRuleset.contains(t)) {
sets.add('recommended');
}
if (flutterRuleset.contains(t)) {
sets.add('flutter');
}
var rulesets = [
if (coreRuleset.contains(t)) 'core',
if (recommendedRuleset.contains(t)) 'recommended',
if (flutterRuleset.contains(t)) 'flutter',
];
var details = sets.isEmpty ? '' : " [${sets.join(', ')}]";
var details = rulesets.isEmpty ? '' : " [${rulesets.join(', ')}]";
return Stat('$t$details', timings[t] ?? 0);
}).toList();
out.writeTimings(stats, 0);
@@ -260,6 +259,7 @@ extension on String {
path.split(this).any((part) => part.startsWith('.'));
/// Whether this path is a Dart file or a Pubspec file.
// TODO(srawlins): This should include analysis options files as well.
bool get isLintable =>
endsWith('.dart') || path.basename(this) == file_paths.pubspecYaml;
}
-95
View File
@@ -1,95 +0,0 @@
// Copyright (c) 2024, 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' as io;
import 'package:analyzer/dart/analysis/results.dart';
import 'package:analyzer/diagnostic/diagnostic.dart';
import 'package:analyzer/file_system/file_system.dart';
import 'package:analyzer/file_system/physical_file_system.dart' as file_system;
import 'package:analyzer/instrumentation/instrumentation.dart';
import 'package:analyzer/src/dart/analysis/analysis_context_collection.dart';
import 'package:analyzer/src/generated/engine.dart' show AnalysisEngine;
import 'package:analyzer/src/lint/io.dart';
import 'linter_options.dart';
class LintDriver {
/// The files which have been analyzed so far. This is used to compute the
/// total number of files analyzed for statistics.
final Set<String> _filesAnalyzed = {};
final LinterOptions _options;
LintDriver(this._options);
ResourceProvider get _resourceProvider =>
file_system.PhysicalResourceProvider.INSTANCE;
Future<List<Diagnostic>> analyze(Iterable<io.File> files) async {
AnalysisEngine.instance.instrumentationService = _StdInstrumentation();
var filesPaths =
files.map((file) => _absoluteNormalizedPath(file.path)).toList();
var contextCollection = AnalysisContextCollectionImpl(
resourceProvider: _resourceProvider,
sdkPath: _options.dartSdkPath,
includedPaths: filesPaths,
updateAnalysisOptions3: ({required analysisOptions, required sdk}) {
analysisOptions.lint = true;
analysisOptions.warning = false;
analysisOptions.lintRules = _options.enabledRules.toList(
growable: false,
);
},
enableLintRuleTiming: true,
);
_filesAnalyzed.addAll(filesPaths);
var result = <Diagnostic>[];
for (var path in _filesAnalyzed) {
var analysisSession = contextCollection.contextFor(path).currentSession;
var errorsResult = await analysisSession.getErrors(path);
if (errorsResult is ErrorsResult) {
result.addAll(errorsResult.diagnostics);
}
}
return result;
}
String _absoluteNormalizedPath(String path) => _resourceProvider.pathContext
.normalize(_resourceProvider.pathContext.absolute(path));
}
/// Prints logging information comments to the [outSink] and error messages to
/// [errorSink].
class _StdInstrumentation extends NoopInstrumentationService {
@override
void logError(String message, [Object? exception]) {
errorSink.writeln(message);
if (exception != null) {
errorSink.writeln(exception);
}
}
@override
void logException(
exception, [
StackTrace? stackTrace,
List<InstrumentationServiceAttachment>? attachments,
]) {
errorSink.writeln(exception);
errorSink.writeln(stackTrace);
}
@override
void logInfo(String message, [Object? exception]) {
outSink.writeln(message);
if (exception != null) {
outSink.writeln(exception);
}
}
}
-18
View File
@@ -1,18 +0,0 @@
// Copyright (c) 2024, 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/analysis_rule/analysis_rule.dart';
import 'package:analyzer/src/lint/registry.dart';
class LinterOptions {
final Iterable<AbstractAnalysisRule> enabledRules;
/// The path to the Dart SDK.
final String? dartSdkPath;
LinterOptions({
Iterable<AbstractAnalysisRule>? enabledRules,
this.dartSdkPath,
}) : enabledRules = enabledRules ?? Registry.ruleRegistry;
}
+87 -25
View File
@@ -2,44 +2,41 @@
// 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 'dart:io' as io;
import 'package:analyzer/analysis_rule/analysis_rule.dart';
import 'package:analyzer/dart/analysis/results.dart';
import 'package:analyzer/diagnostic/diagnostic.dart';
import 'package:analyzer/error/listener.dart';
import 'package:analyzer/file_system/file_system.dart';
import 'package:analyzer/file_system/physical_file_system.dart' as file_system;
import 'package:analyzer/instrumentation/instrumentation.dart';
import 'package:analyzer/source/file_source.dart';
import 'package:analyzer/source/source.dart';
import 'package:analyzer/src/dart/analysis/analysis_context_collection.dart';
import 'package:analyzer/src/generated/engine.dart' show AnalysisEngine;
import 'package:analyzer/src/lint/io.dart';
import 'package:analyzer/src/lint/pub.dart';
import 'package:analyzer/src/util/file_paths.dart' as file_paths;
import 'package:path/path.dart' as path;
import 'lint_driver.dart';
import 'linter_options.dart';
Source _createSource(Uri uri) {
var filePath = uri.toFilePath();
var file = file_system.PhysicalResourceProvider.INSTANCE.getFile(filePath);
return FileSource(file, uri);
}
class TestLinter implements DiagnosticListener {
final errors = <Diagnostic>[];
final LinterOptions options;
TestLinter(this.options);
final List<AbstractAnalysisRule> _rules;
path.Context get _pathContext =>
file_system.PhysicalResourceProvider.INSTANCE.pathContext;
final String? _dartSdkPath;
Future<List<Diagnostic>> lintFiles(List<File> files) async {
var lintDriver = LintDriver(options);
var errors = await lintDriver.analyze(
files.where((f) => f.path.endsWith('.dart')),
);
TestLinter(this._rules, this._dartSdkPath);
ResourceProvider get _resourceProvider =>
file_system.PhysicalResourceProvider.INSTANCE;
Future<List<Diagnostic>> lintFiles(List<io.File> files) async {
var errors = await _analyze(files.where((f) => f.path.endsWith('.dart')));
for (var file in files.where(_isPubspecFile)) {
_lintPubspecSource(
contents: file.readAsStringSync(),
sourcePath: _pathContext.normalize(file.absolute.path),
sourcePath: _resourceProvider.pathContext.normalize(file.absolute.path),
);
}
return errors;
@@ -48,22 +45,57 @@ class TestLinter implements DiagnosticListener {
@override
void onDiagnostic(Diagnostic error) => errors.add(error);
/// Returns whether this [entry] is a pubspec file.
bool _isPubspecFile(FileSystemEntity entry) =>
String _absoluteNormalizedPath(String path) => _resourceProvider.pathContext
.normalize(_resourceProvider.pathContext.absolute(path));
Future<List<Diagnostic>> _analyze(Iterable<io.File> files) async {
AnalysisEngine.instance.instrumentationService = _StdInstrumentation();
var filePaths =
files.map((file) => _absoluteNormalizedPath(file.path)).toList();
var contextCollection = AnalysisContextCollectionImpl(
resourceProvider: _resourceProvider,
sdkPath: _dartSdkPath,
includedPaths: filePaths,
updateAnalysisOptions3: ({required analysisOptions, required sdk}) {
analysisOptions.lint = true;
analysisOptions.warning = false;
analysisOptions.lintRules = _rules;
},
enableLintRuleTiming: true,
);
var result = <Diagnostic>[];
for (var path in filePaths) {
var analysisSession = contextCollection.contextFor(path).currentSession;
var errorsResult = await analysisSession.getErrors(path);
if (errorsResult is ErrorsResult) {
result.addAll(errorsResult.diagnostics);
}
}
return result;
}
/// Whether this [entry] is a pubspec file.
bool _isPubspecFile(io.FileSystemEntity entry) =>
path.basename(entry.path) == file_paths.pubspecYaml;
void _lintPubspecSource({required String contents, String? sourcePath}) {
var sourceUrl = sourcePath == null ? null : path.toUri(sourcePath);
var spec = Pubspec.parse(contents, sourceUrl: sourceUrl);
for (var rule in options.enabledRules) {
for (var rule in _rules) {
var visitor = rule.pubspecVisitor;
if (visitor != null) {
// Analyzer sets reporters; if this file is not being analyzed,
// we need to set one ourselves. (Needless to say, when pubspec
// processing gets pushed down, this hack can go away.)
if (sourceUrl != null) {
var source = _createSource(sourceUrl);
var source = FileSource(
_resourceProvider.getFile(sourceUrl.toFilePath()),
sourceUrl,
);
rule.reporter = DiagnosticReporter(this, source);
}
try {
@@ -75,3 +107,33 @@ class TestLinter implements DiagnosticListener {
}
}
}
/// Prints logging information comments to the [outSink] and error messages to
/// [errorSink].
class _StdInstrumentation extends NoopInstrumentationService {
@override
void logError(String message, [Object? exception]) {
errorSink.writeln(message);
if (exception != null) {
errorSink.writeln(exception);
}
}
@override
void logException(
exception, [
StackTrace? stackTrace,
List<InstrumentationServiceAttachment>? attachments,
]) {
errorSink.writeln(exception);
errorSink.writeln(stackTrace);
}
@override
void logInfo(String message, [Object? exception]) {
outSink.writeln(message);
if (exception != null) {
outSink.writeln(exception);
}
}
}