| // 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; |
| } |
| } |