blob: 5aa0cee5e9c6adbfad63f4d8595fd06c480d5ad0 [file] [log] [blame]
// Copyright (c) 2019, 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.
/// Regenerates the static error test markers inside static error tests based
/// on the actual errors reported by analyzer and CFE.
library;
import 'dart:io';
import 'package:analyzer/dart/analysis/analysis_context.dart';
import 'package:analyzer/dart/analysis/analysis_context_collection.dart';
import 'package:analyzer/dart/analysis/results.dart';
import 'package:analyzer/diagnostic/diagnostic.dart';
import 'package:analyzer/file_system/file_system.dart' show ResourceProvider;
import 'package:analyzer/file_system/overlay_file_system.dart';
import 'package:analyzer/file_system/physical_file_system.dart';
import 'package:args/args.dart';
import 'package:collection/collection.dart';
import 'package:glob/glob.dart';
import 'package:glob/list_local_fs.dart';
import 'package:path/path.dart' as p;
import 'package:test_runner/src/command_output.dart';
import 'package:test_runner/src/path.dart';
import 'package:test_runner/src/static_error.dart';
import 'package:test_runner/src/test_file.dart';
import 'package:test_runner/src/update_errors.dart';
const _usage =
"Usage: dart update_static_error_tests.dart [flags...] <path glob>";
final dartPath = _findBinary("dart", "exe");
Future<void> main(List<String> args) async {
var sources = ErrorSource.all.map((e) => e.marker).toList();
var parser = ArgParser();
parser.addFlag("help", abbr: "h");
parser.addFlag("dry-run",
abbr: "n",
help: "Print result but do not overwrite any files.",
negatable: false);
parser.addFlag("context",
abbr: "c", help: "Include context messages in output.");
parser.addFlag("only-static-error-tests",
abbr: "o",
help: "Skips files that don't already have static error expectations.",
negatable: false);
parser.addSeparator("What operations to perform:");
parser.addFlag("remove-all",
abbr: "r",
help: "Remove all existing error expectations.",
negatable: false);
parser.addMultiOption("remove",
help: "Remove error expectations for given front ends.",
allowed: sources);
parser.addFlag("insert-all",
abbr: "i",
help: "Insert error expectations for all front ends.",
negatable: false);
parser.addMultiOption("insert",
help: "Insert error expectations from given front ends.",
allowed: sources);
parser.addFlag("update-all",
abbr: "u",
help: "Replace error expectations for all front ends.",
negatable: false);
parser.addMultiOption("update",
help: "Update error expectations for given front ends.",
allowed: sources);
var results = parser.parse(args);
if (results["help"] as bool) {
print("Regenerates the test markers inside static error tests.");
print("");
print(_usage);
print("");
print(parser.usage);
exit(0);
}
var dryRun = results["dry-run"] as bool;
var includeContext = results["context"] as bool;
var onlyStaticErrorTests = results["only-static-error-tests"] as bool;
var removeSources = <ErrorSource>{};
var insertSources = <ErrorSource>{};
for (var source in results["remove"] as List<String>) {
removeSources.add(ErrorSource.find(source)!);
}
for (var source in results["insert"] as List<String>) {
insertSources.add(ErrorSource.find(source)!);
}
for (var source in results["update"] as List<String>) {
removeSources.add(ErrorSource.find(source)!);
insertSources.add(ErrorSource.find(source)!);
}
if (results["remove-all"] as bool) {
removeSources.addAll(ErrorSource.all);
}
if (results["insert-all"] as bool) {
insertSources.addAll(ErrorSource.all);
}
if (results["update-all"] as bool) {
removeSources.addAll(ErrorSource.all);
insertSources.addAll(ErrorSource.all);
}
if (removeSources.isEmpty && insertSources.isEmpty) {
_usageError(
parser, "Must provide at least one flag for an operation to perform.");
}
var testFiles =
_listFiles(results.rest, onlyStaticErrorTests: onlyStaticErrorTests);
var analyzerErrors = const <TestFile, List<StaticError>>{};
var cfeErrors = const <TestFile, List<StaticError>>{};
var webErrors = const <TestFile, List<StaticError>>{};
if (insertSources.contains(ErrorSource.analyzer)) {
analyzerErrors = await _runAnalyzer(testFiles);
}
// If we're inserting web errors, we also need to gather the CFE errors to
// tell which web errors are web-specific.
if (insertSources.contains(ErrorSource.cfe) ||
insertSources.contains(ErrorSource.web)) {
cfeErrors = await _runCfe(testFiles);
}
if (insertSources.contains(ErrorSource.web)) {
webErrors = await _runDart2js(testFiles, cfeErrors);
}
print('Updating test files...');
for (var testFile in testFiles) {
_updateErrors(
testFile,
[
if (insertSources.contains(ErrorSource.analyzer))
...?analyzerErrors[testFile],
if (insertSources.contains(ErrorSource.cfe)) ...?cfeErrors[testFile],
if (insertSources.contains(ErrorSource.web)) ...?webErrors[testFile],
],
remove: removeSources,
includeContext: includeContext,
dryRun: dryRun);
}
}
List<String> _testFileOptions(TestFile testFile, {bool cfe = false}) {
return [
...testFile.sharedOptions,
if (testFile.experiments.isNotEmpty)
"--enable-experiment=${testFile.experiments.join(',')}",
];
}
/// Find all of the files that match [pathGlobs], load corresponding [TestFile]s
/// and return all tests that should be updated.
///
/// Omits multitests since those don't support being used as static error tests.
/// Omits any [TestFile] that doesn't already contain a static error test
/// expectation if [onlyStaticErrorTests] is `true`.
List<TestFile> _listFiles(List<String> pathGlobs,
{required bool onlyStaticErrorTests}) {
print('Listing files...');
var testFiles = <TestFile>[];
for (var pathGlob in pathGlobs) {
// Allow tests to be specified without the extension for compatibility with
// the regular test runner syntax.
if (!pathGlob.endsWith(".dart")) pathGlob += ".dart";
// Allow tests to be specified either relative to the "tests" directory
// or relative to the current directory.
var root = pathGlob.startsWith("tests") ? "." : "tests";
// [Glob] doesn't handle Windows path delimiters, so we normalize it using
// [Path]. [Glob] doesn't handle absolute Windows paths, though, so this
// only supports the relative paths.
pathGlob = Path.normalize(pathGlob);
for (var file in Glob(pathGlob, recursive: true).listSync(root: root)) {
if (file is! File) continue;
if (!file.path.endsWith(".dart")) continue;
// Canonicalize the path in the same way StaticError does, so it matches.
var f = p.relative(file.path, from: Directory.current.path);
var testFile = TestFile.read(Path("."), f);
if (testFile.isMultitest) {
print("Skip ${testFile.path} since this tool can't update multitests.");
continue;
}
if (onlyStaticErrorTests && testFile.expectedErrors.isEmpty) {
print("Skip ${testFile.path} since it isn't a static error test.");
continue;
}
testFiles.add(testFile);
}
}
return testFiles;
}
void _usageError(ArgParser parser, String message) {
stderr.writeln(message);
stderr.writeln();
stderr.writeln(_usage);
stderr.writeln(parser.usage);
exit(64);
}
/// Run analyzer on [allTestFiles] and return the errors for each file.
Future<Map<TestFile, List<StaticError>>> _runAnalyzer(
List<TestFile> allTestFiles) async {
// For performance, we want to analyze multiple tests using the same analysis
// context collection. But a context collection only works with a single set
// of experiment flags, so group the tests by their experiments.
var testsByExperiments =
EqualityMap<List<String>, List<TestFile>>(const ListEquality());
for (var testFile in allTestFiles) {
var experiments = testFile.experiments.toList()..sort();
testsByExperiments.putIfAbsent(experiments, () => []).add(testFile);
}
var errors = <TestFile, List<StaticError>>{};
for (var experiments in testsByExperiments.keys) {
var testFiles = testsByExperiments[experiments]!;
ResourceProvider resourceProvider;
if (experiments.isEmpty) {
print('Running analyzer on ${_plural(testFiles, 'file')}...');
resourceProvider = PhysicalResourceProvider.INSTANCE;
} else {
print('Running analyzer on ${_plural(testFiles, 'file')} '
'with experiments "${experiments.join(', ')}"...');
// If there are experiments, then synthesize an analysis options file in the
// root directory that enables the experiments for all of the tests.
var options = StringBuffer();
options.writeln('analyzer:');
options.writeln(' enable-experiment:');
for (var experiment in experiments) {
options.writeln(' - $experiment');
}
resourceProvider =
OverlayResourceProvider(PhysicalResourceProvider.INSTANCE)
..setOverlay(
Path('tests/analysis_options.yaml').absolute.toNativePath(),
content: options.toString(),
modificationStamp: 0);
}
var paths = [
for (var testFile in testFiles)
p.normalize(testFile.path.absolute.toNativePath())
];
var contextCollection = AnalysisContextCollection(
includedPaths: paths, resourceProvider: resourceProvider);
for (var testFile in testFiles) {
errors[testFile] = await _runAnalyzerOnFile(contextCollection, testFile);
}
await contextCollection.dispose();
}
return errors;
}
/// Analyze [testFile] using [contextCollection] and return the list of reported
/// errors.
Future<List<StaticError>> _runAnalyzerOnFile(
AnalysisContextCollection contextCollection, TestFile testFile) async {
var absolutePath = testFile.path.absolute.toNativePath();
var context = contextCollection.contextFor(absolutePath);
var errorsResult = await context.currentSession.getErrors(absolutePath);
// Convert the analyzer errors to test_runner [StaticErrors].
var errors = <StaticError>[];
if (errorsResult is ErrorsResult) {
for (var diagnostic in errorsResult.diagnostics) {
switch (diagnostic.severity) {
case Severity.error:
case Severity.warning
when AnalyzerError.isValidatedWarning(
diagnostic.diagnosticCode.name):
errors.add(
_convertAnalysisError(context, errorsResult.path, diagnostic));
default:
// Ignore todos and other harmless warnings like unused variables
// which the tests are riddled with but we don't want to bother
// validating.
break;
}
}
}
return errors;
}
/// Convert an [Diagnostic] from the analyzer package to the test runner's
/// [StaticError] type.
StaticError _convertAnalysisError(
AnalysisContext analysisContext, String containingFile, Diagnostic error) {
var fileResult =
analysisContext.currentSession.getFile(containingFile) as FileResult;
var errorLocation = fileResult.lineInfo.getLocation(error.offset);
var staticError = StaticError(ErrorSource.analyzer,
'${error.diagnosticCode.type.name}.${error.diagnosticCode.name}',
path: containingFile,
line: errorLocation.lineNumber,
column: errorLocation.columnNumber,
length: error.length);
for (var context in error.contextMessages) {
var contextFileResult =
analysisContext.currentSession.getFile(context.filePath) as FileResult;
var contextLocation =
contextFileResult.lineInfo.getLocation(context.offset);
staticError.contextMessages.add(StaticError(
ErrorSource.context, context.messageText(includeUrl: true),
path: context.filePath,
line: contextLocation.lineNumber,
column: contextLocation.columnNumber,
length: context.length));
}
return staticError;
}
/// Invoke CFE on all [allTestFiles] and gather the static errors it reports.
Future<Map<TestFile, List<StaticError>>> _runCfe(
List<TestFile> allTestFiles) async {
// For performance, we want to run CFE on batches of files, but we can't use
// the same invocation for files with different options, so group by options
// first.
var testsByOptions =
EqualityMap<List<String>, List<TestFile>>(const ListEquality());
for (var testFile in allTestFiles) {
var options = _testFileOptions(testFile, cfe: true)..sort();
testsByOptions.putIfAbsent(options, () => []).add(testFile);
}
var errors = <TestFile, List<StaticError>>{};
for (var options in testsByOptions.keys) {
var testFiles = testsByOptions[options]!;
if (options.isEmpty) {
print('Running CFE on ${_plural(testFiles, 'file')}...');
} else {
print('Running CFE on ${_plural(testFiles, 'file')} '
'with options "${options.join(', ')}"...');
}
var absolutePaths = [
for (var testFile in testFiles)
File(testFile.path.toNativePath()).absolute.path,
];
var result = await Process.run(dartPath, [
'pkg/front_end/tool/compile.dart',
...options,
'--packages=.dart_tool/package_config.json',
'--verify',
'-o',
'dev:null', // Output is only created for file URIs.
...absolutePaths,
]);
// Running the above command may generate dill files next to the tests,
// which we don't want, so delete them if present.
for (var absolutePath in absolutePaths) {
var dill = File("$absolutePath.dill");
if (await dill.exists()) {
await dill.delete();
}
}
if (result.exitCode != 0) {
print("CFE run failed: ${result.stdout}\n${result.stderr}");
exit(1);
}
var parsedErrors = <StaticError>[];
FastaCommandOutput.parseErrors(
result.stdout as String, parsedErrors, parsedErrors);
for (var error in parsedErrors) {
var testFile =
testFiles.firstWhere((test) => test.path == Path(error.path));
errors.putIfAbsent(testFile, () => []).add(error);
}
}
return errors;
}
Future<Map<TestFile, List<StaticError>>> _runDart2js(List<TestFile> testFiles,
Map<TestFile, List<StaticError>> cfeErrors) async {
var errors = <TestFile, List<StaticError>>{};
for (var testFile in testFiles) {
print('Running dart2js on ${testFile.path}...');
errors[testFile] = await _runDart2jsOnFile(
testFile, cfeErrors[testFile] ?? const <StaticError>[]);
}
return errors;
}
/// Invoke dart2js on [testFile] and gather all static errors it reports.
Future<List<StaticError>> _runDart2jsOnFile(
TestFile testFile, List<StaticError> cfeErrors) async {
var result = await Process.run(dartPath, [
'compile',
'js',
..._testFileOptions(testFile),
"-o",
"dev:null", // Output is only created for file URIs.
testFile.path.absolute.toNativePath(),
]);
var errors = <StaticError>[];
Dart2jsCompilerCommandOutput.parseErrors(result.stdout as String, errors);
// We only want the web-specific errors from dart2js, so filter out any errors
// that are also reported by the CFE.
errors.removeWhere((dart2jsError) {
return cfeErrors.any((cfeError) {
return dart2jsError.line == cfeError.line &&
dart2jsError.column == cfeError.column &&
dart2jsError.length == cfeError.length &&
dart2jsError.message == cfeError.message;
});
});
return errors;
}
/// Update the static error expectations in [testFile].
///
/// Adds [errors] to the file and removes any existing errors that are in from
/// the sources in [remove].
///
/// If [includeContext] is `true`, then includes context messages in the
/// resulting test. If [dryRun] is `false`, then writes the result to disk.
/// Otherwise, prints the resulting test file.
void _updateErrors(TestFile testFile, List<StaticError> errors,
{required Set<ErrorSource> remove,
required bool includeContext,
required bool dryRun}) {
// Error expectations can be in imported libraries or part files. Iterate
// over the set of paths that is the main file path plus all paths mentioned
// in expectations, updating them.
var paths = {testFile.path, for (var error in errors) Path(error.path)};
for (var path in paths) {
var nativePath = path.toNativePath();
var file = File(nativePath);
var pathErrors = errors.where((e) => Path(e.path) == path).toList();
var result = updateErrorExpectations(
nativePath, file.readAsStringSync(), pathErrors,
remove: remove, includeContext: includeContext);
if (dryRun) {
print(result);
} else {
file.writeAsString(result);
print('- $nativePath (${_plural(pathErrors, 'error')})');
}
}
}
/// Find the most recently-built binary [name] in any of the build directories.
String _findBinary(String name, String windowsExtension) {
var binary = Platform.isWindows ? "$name.$windowsExtension" : name;
String? newestPath;
DateTime? newestTime;
var buildDirectory = Directory(Platform.isMacOS ? "xcodebuild" : "out");
if (buildDirectory.existsSync()) {
for (var config in buildDirectory.listSync()) {
var path = p.join(config.path, "dart-sdk", "bin", binary);
var file = File(path);
if (!file.existsSync()) continue;
var modified = file.lastModifiedSync();
if (newestTime == null || modified.isAfter(newestTime)) {
newestPath = path;
newestTime = modified;
}
}
}
if (newestPath == null) {
// Clear the current line since we're in the middle of a progress line.
print("");
print("Could not find a built SDK with a $binary to run.");
print("Make sure to build the Dart SDK before running this tool.");
exit(1);
}
return newestPath;
}
/// Returns a string with the number of [things] followed by [noun], pluralized
/// as needed.
String _plural(List<Object?> things, String noun) {
if (things.length == 1) return '1 $noun';
return '${things.length} ${noun}s';
}