blob: 25d1be67e6d7185b157d4f3efce6bd5eaa23669a [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.
import 'dart:io';
import 'package:path/path.dart' as p;
import 'io.dart';
import 'log.dart';
Future<bool> analyzeTests(String nnbdTestDir) async {
var files = <String, _FileInfo>{};
for (var entry in Directory(nnbdTestDir).listSync(recursive: true)) {
if (entry is File && entry.path.endsWith(".dart")) {
// Skip multitests since they aren't valid Dart files.
var isTest = entry.path.endsWith("_test.dart") &&
!readFile(entry.path).contains("//#");
files[entry.path] = _FileInfo(entry.path, isTest: isTest);
}
}
// Pre-existing multi-line errors will modify the character length in the
// errors reported by the analyzer. Strip all errors first before updating.
for (var file in files.values) {
if (!dryRun) _removeErrors(file.path);
}
// Analyze the directory both in legacy and NNBD modes.
var legacyErrorsFuture = _runAnalyzer(nnbdTestDir, nnbd: false);
var nnbdErrorsFuture = _runAnalyzer(nnbdTestDir, nnbd: true);
var legacyErrors = await legacyErrorsFuture;
var nnbdErrors = await nnbdErrorsFuture;
legacyErrors.forEach((path, errors) {
// Sometimes the analysis reaches out to things like pkg/expect.
if (!files.containsKey(path)) {
files[path] = _FileInfo(path, isTest: false);
}
files[path].legacyErrors.addAll(errors);
});
nnbdErrors.forEach((path, errors) {
// Sometimes the analysis reaches out to things like pkg/expect.
if (!files.containsKey(path)) {
files[path] = _FileInfo(path, isTest: false);
}
files[path].nnbdErrors.addAll(errors);
});
var fileCount = 0;
var errorFileCount = 0;
var errorCount = 0;
plural(int count, String name) => "$count $name${count == 1 ? '' : 's'}";
var fileNames = files.keys.toList()..sort();
for (var fileName in fileNames) {
var file = files[fileName];
if (!file.isTest) continue;
// Only insert errors that are not already present when the file is
// analyzed as a legacy library.
file.calculateDifferences();
fileCount++;
errorCount += file.addedErrors.length;
if (file.addedErrors.length > 0) {
var count = file.addedErrors.length;
print("${p.relative(file.path, from: testRoot)}: " +
plural(count, 'error'));
errorFileCount++;
}
if (!dryRun) _insertErrors(file.path, file.addedErrors);
}
if (errorCount == 0) {
print(green("All ${plural(fileCount, 'file')} are static error free!"));
} else {
print(red("Analyzed ${plural(fileCount, 'file')} and found "
"${plural(errorCount, 'error')} "
"in ${plural(errorFileCount, 'file')}."));
}
return errorCount == 0;
}
Future<Map<String, List<_StaticError>>> _runAnalyzer(String inputDir,
{bool nnbd}) async {
print("Analyzing ${p.relative(inputDir, from: testRoot)}"
"${nnbd ? ' with NNBD' : ''}...");
var result = await Process.run("dartanalyzer", [
"--packages=${p.join(sdkRoot, '.packages')}",
if (nnbd) "--enable-experiment=non-nullable",
"--format=machine",
inputDir,
]);
// TODO(rnystrom): How do we pass in options from the test files?
var errors = _StaticError.parse(result.stderr as String);
var errorsByFile = <String, List<_StaticError>>{};
for (var error in errors) {
if (error.code.startsWith("HINT.")) continue;
errorsByFile.putIfAbsent(error.file, () => []).add(error);
}
for (var errors in errorsByFile.values) {
errors.sort();
}
return errorsByFile;
}
/// Removes pre-existing errors in the file at [path].
void _removeErrors(String path) {
// Sanity check.
if (!p.isWithin(testRoot, path)) {
throw ArgumentError("$path is outside of test directory.");
}
var lines = readFileLines(path);
var result = StringBuffer();
var changed = false;
for (var i = 0; i < lines.length; i++) {
// Strip out previous inserted comments.
if (!lines[i].startsWith("//|")) {
result.writeln(lines[i]);
} else {
changed = true;
}
}
if (changed) {
writeFile(path, result.toString());
}
}
/// Inserts any new errors in [errors] into the file at [path].
void _insertErrors(String path, List<_StaticError> errors) {
// Sanity check.
if (!p.isWithin(testRoot, path)) {
throw ArgumentError("$path is outside of test directory.");
}
var lines = readFileLines(path);
var result = StringBuffer();
var changed = false;
for (var i = 0; i < lines.length; i++) {
result.writeln(lines[i]);
// TODO(rnystrom): Inefficient.
for (var error in errors) {
if (error.line == i + 1) {
result.write("//|");
result.write(" " * (error.column - 3));
result.write("^" * error.length);
result.writeln(" ${error.code}");
result.writeln("//| ${error.message}");
changed = true;
}
}
}
if (changed) {
writeFile(path, result.toString());
}
}
class _FileInfo {
final String path;
final bool isTest;
final Set<_StaticError> legacyErrors = {};
final Set<_StaticError> nnbdErrors = {};
final List<_StaticError> removedErrors = [];
final List<_StaticError> addedErrors = [];
_FileInfo(this.path, {this.isTest});
void calculateDifferences() {
removedErrors.addAll(legacyErrors.toSet().difference(nnbdErrors.toSet()));
addedErrors.addAll(nnbdErrors.toSet().difference(legacyErrors.toSet()));
}
}
class _StaticError implements Comparable<_StaticError> {
static List<_StaticError> parse(String stderr) {
List<String> splitMachineError(String line) {
var field = StringBuffer();
var result = <String>[];
var escaped = false;
for (var i = 0; i < line.length; i++) {
var c = line[i];
if (!escaped && c == '\\') {
escaped = true;
continue;
}
escaped = false;
if (c == '|') {
result.add(field.toString());
field = StringBuffer();
continue;
}
field.write(c);
}
result.add(field.toString());
return result;
}
var errors = <_StaticError>[];
for (var line in stderr.split("\n")) {
if (line.isEmpty) continue;
var fields = splitMachineError(line);
// Lines without enough fields are other output we don't care about.
if (fields.length >= 8) {
var error = _StaticError(fields[3],
line: int.parse(fields[4]),
column: int.parse(fields[5]),
length: int.parse(fields[6]),
code: "${fields[1]}.${fields[2]}",
message: fields[7]);
errors.add(error);
} else {
print(line);
}
}
return errors;
}
final String file;
/// The one-based line number of the beginning of the error's location.
final int line;
/// The one-based column number of the beginning of the error's location.
final int column;
/// The number of characters in the error location.
///
/// This is optional. The CFE only reports error location, but not length.
final int length;
/// The expected analyzer error code for the error or `null` if this error
/// isn't expected to be reported by analyzer.
final String code;
/// The expected CFE error message or `null` if this error isn't expected to
/// be reported by the CFE.
final String message;
/// Creates a new StaticError at the given location with the given expected
/// error code and message.
///
/// In order to make it easier to incrementally add error tests before a
/// feature is fully implemented or specified, an error expectation can be in
/// an "unspecified" state for either or both platforms by having the error
/// code or message be the special string "unspecified". When an unspecified
/// error is tested, a front end is expected to report *some* error on that
/// error's line, but it can be any location, error code, or message.
_StaticError(this.file,
{this.line, this.column, this.length, this.code, this.message}) {
// Must have a location.
assert(line != null);
assert(column != null);
// Must have at least one piece of description.
assert(code != null || message != null);
}
/// A textual description of this error's location.
String get location {
var result = "line $line, column $column";
if (length != null) result += ", length $length";
return result;
}
String toString() => "Error $code in $file at $location: $message";
String toStringWithoutPath() => "[$location] $code: $message";
/// Orders errors primarily by location, then by other fields if needed.
@override
int compareTo(_StaticError other) {
if (file != other.file) return file.compareTo(other.file);
if (line != other.line) return line.compareTo(other.line);
if (column != other.column) return column.compareTo(other.column);
// Sort no length after all other lengths.
if (length == null && other.length != null) return 1;
if (length != null && other.length == null) return -1;
if (length != other.length) return length.compareTo(other.length);
var thisCode = code ?? "";
var otherCode = other.code ?? "";
if (thisCode != otherCode) return thisCode.compareTo(otherCode);
var thisMessage = message ?? "";
var otherMessage = other.message ?? "";
return thisMessage.compareTo(otherMessage);
}
bool operator ==(dynamic other) =>
other is _StaticError &&
file == other.file &&
line == other.line &&
column == other.column &&
length == other.length &&
normalizeCode(code) == normalizeCode(other.code);
int get hashCode =>
line.hashCode ^
column.hashCode ^
length.hashCode ^
normalizeCode(code).hashCode;
String normalizeCode(String code) {
// Pre-NNBD has a limited form of implicit downcast checking for
// constructors.
if (code == "COMPILE_TIME_ERROR.INVALID_CAST_NEW_EXPR") {
return "STATIC_WARNING.ARGUMENT_TYPE_NOT_ASSIGNABLE";
}
return code;
}
}