| // Copyright (c) 2016, 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.md file. | |
| library testing.analyze; | |
| import 'dart:async' show Stream, Future; | |
| import 'dart:convert' show LineSplitter, utf8; | |
| import 'dart:io' | |
| show Directory, File, FileSystemEntity, Platform, Process, ProcessResult; | |
| import '../testing.dart' show dartArguments, startDart; | |
| import 'log.dart' show isVerbose, splitLines; | |
| import 'suite.dart' show Suite; | |
| class Analyze extends Suite { | |
| final Uri analysisOptions; | |
| final List<Uri> uris; | |
| final List<RegExp> exclude; | |
| final List<String> gitGrepPathspecs; | |
| final List<String> gitGrepPatterns; | |
| Analyze(this.analysisOptions, this.uris, this.exclude, this.gitGrepPathspecs, | |
| this.gitGrepPatterns) | |
| : super("analyze", "analyze", null); | |
| Future<Null> run(Uri packages, List<Uri> extraUris) { | |
| List<Uri> allUris = new List<Uri>.from(uris); | |
| if (extraUris != null) { | |
| allUris.addAll(extraUris); | |
| } | |
| return analyzeUris(analysisOptions, packages, allUris, exclude, | |
| gitGrepPathspecs, gitGrepPatterns); | |
| } | |
| static Future<Analyze> fromJsonMap( | |
| Uri base, Map json, List<Suite> suites) async { | |
| String optionsPath = json["options"]; | |
| Uri optionsUri = optionsPath == null ? null : base.resolve(optionsPath); | |
| List<Uri> uris = json["uris"].map<Uri>((relative) { | |
| String r = relative; | |
| return base.resolve(r); | |
| }).toList(); | |
| List<RegExp> exclude = | |
| json["exclude"].map<RegExp>((p) => new RegExp(p)).toList(); | |
| Map gitGrep = json["git grep"]; | |
| List<String> gitGrepPathspecs; | |
| List<String> gitGrepPatterns; | |
| if (gitGrep != null) { | |
| gitGrepPathspecs = gitGrep["pathspecs"] == null | |
| ? const <String>["."] | |
| : new List<String>.from(gitGrep["pathspecs"]); | |
| if (gitGrep["patterns"] != null) | |
| gitGrepPatterns = new List<String>.from(gitGrep["patterns"]); | |
| } | |
| return new Analyze( | |
| optionsUri, uris, exclude, gitGrepPathspecs, gitGrepPatterns); | |
| } | |
| String toString() => "Analyze($uris, $exclude)"; | |
| } | |
| class AnalyzerDiagnostic { | |
| final String kind; | |
| final String detailedKind; | |
| final String code; | |
| final Uri uri; | |
| final int line; | |
| final int startColumn; | |
| final int endColumn; | |
| final String message; | |
| static final Pattern potentialSplitPattern = new RegExp(r"\\|\|"); | |
| static final Pattern unescapePattern = new RegExp(r"\\(.)"); | |
| AnalyzerDiagnostic(this.kind, this.detailedKind, this.code, this.uri, | |
| this.line, this.startColumn, this.endColumn, this.message); | |
| AnalyzerDiagnostic.malformed(String line) | |
| : this(null, null, null, null, -1, -1, -1, line); | |
| factory AnalyzerDiagnostic.fromLine(String line) { | |
| List<String> parts = <String>[]; | |
| int start = 0; | |
| int index = line.indexOf(potentialSplitPattern); | |
| addPart() { | |
| parts.add(line | |
| .substring(start, index == -1 ? null : index) | |
| .replaceAllMapped(unescapePattern, (Match m) => m[1])); | |
| } | |
| while (index != -1) { | |
| if (line[index] == "\\") { | |
| index = line.indexOf(potentialSplitPattern, index + 2); | |
| } else { | |
| addPart(); | |
| start = index + 1; | |
| index = line.indexOf(potentialSplitPattern, start); | |
| } | |
| } | |
| addPart(); | |
| if (parts.length != 8) { | |
| return new AnalyzerDiagnostic.malformed(line); | |
| } | |
| return new AnalyzerDiagnostic( | |
| parts[0], | |
| parts[1], | |
| parts[2], | |
| Uri.base.resolveUri(new Uri.file(parts[3])), | |
| int.parse(parts[4]), | |
| int.parse(parts[5]), | |
| int.parse(parts[6]), | |
| parts[7]); | |
| } | |
| String toString() { | |
| return kind == null | |
| ? "Malformed output from dartanalyzer:\n$message" | |
| : "${uri.toFilePath()}:$line:$startColumn: " | |
| "${kind == 'INFO' ? 'warning: hint' : kind.toLowerCase()}:\n" | |
| "[$code] $message"; | |
| } | |
| } | |
| Stream<AnalyzerDiagnostic> parseAnalyzerOutput( | |
| Stream<List<int>> stream) async* { | |
| Stream<String> lines = | |
| stream.transform(utf8.decoder).transform(new LineSplitter()); | |
| await for (String line in lines) { | |
| if (line.startsWith(">>> ")) continue; | |
| yield new AnalyzerDiagnostic.fromLine(line); | |
| } | |
| } | |
| /// Run dartanalyzer on all tests in [uris]. | |
| Future<Null> analyzeUris( | |
| Uri analysisOptions, | |
| Uri packages, | |
| List<Uri> uris, | |
| List<RegExp> exclude, | |
| List<String> gitGrepPathspecs, | |
| List<String> gitGrepPatterns) async { | |
| if (uris.isEmpty) return; | |
| String topLevel; | |
| try { | |
| topLevel = new Uri.directory( | |
| (await git("rev-parse", <String>["--show-toplevel"])).trimRight()) | |
| .toFilePath(windows: false); | |
| } catch (e) { | |
| topLevel = Uri.base.toFilePath(windows: false); | |
| } | |
| String toFilePath(Uri uri) { | |
| String path = uri.toFilePath(windows: false); | |
| return path.startsWith(topLevel) ? path.substring(topLevel.length) : path; | |
| } | |
| Set<String> filesToAnalyze = new Set<String>(); | |
| for (Uri uri in uris) { | |
| if (await new Directory.fromUri(uri).exists()) { | |
| await for (FileSystemEntity entity in new Directory.fromUri(uri) | |
| .list(recursive: true, followLinks: false)) { | |
| if (entity is File && entity.path.endsWith(".dart")) { | |
| filesToAnalyze.add(toFilePath(entity.uri)); | |
| } | |
| } | |
| } else if (await new File.fromUri(uri).exists()) { | |
| filesToAnalyze.add(toFilePath(uri)); | |
| } else { | |
| throw "File not found: ${uri}"; | |
| } | |
| } | |
| if (gitGrepPatterns != null) { | |
| List<String> arguments = <String>["-l"]; | |
| arguments.addAll( | |
| gitGrepPatterns.expand((String pattern) => <String>["-e", pattern])); | |
| arguments.add("--"); | |
| arguments.addAll(gitGrepPathspecs); | |
| filesToAnalyze.addAll(splitLines(await git("grep", arguments)) | |
| .map((String line) => line.trimRight())); | |
| } | |
| const String analyzerPath = "pkg/analyzer_cli/bin/analyzer.dart"; | |
| Uri analyzer = Uri.base.resolve(analyzerPath); | |
| if (!await new File.fromUri(analyzer).exists()) { | |
| throw "Couldn't find '$analyzerPath' in '${toFilePath(Uri.base)}'"; | |
| } | |
| List<String> arguments = <String>[ | |
| "--packages=${toFilePath(packages)}", | |
| "--format=machine", | |
| "--dart-sdk=${Uri.base.resolve('sdk/').toFilePath()}", | |
| ]; | |
| if (analysisOptions != null) { | |
| arguments.add("--options=${toFilePath(analysisOptions)}"); | |
| } | |
| filesToAnalyze = filesToAnalyze | |
| .where((String path) => !exclude.any((RegExp r) => path.contains(r))) | |
| .toSet(); | |
| arguments.addAll(filesToAnalyze); | |
| if (isVerbose) { | |
| print("Running:\n ${toFilePath(analyzer)} ${arguments.join(' ')}"); | |
| } else { | |
| print("Running dartanalyzer."); | |
| } | |
| Stopwatch sw = new Stopwatch()..start(); | |
| Process process = await startDart( | |
| analyzer, | |
| const <String>["--batch"], | |
| dartArguments | |
| ..remove("-c") | |
| ..add("-DuseFastaScanner=true")); | |
| process.stdin.writeln(arguments.join(" ")); | |
| process.stdin.close(); | |
| bool hasOutput = false; | |
| Set<String> seen = new Set<String>(); | |
| processAnalyzerOutput(Stream<AnalyzerDiagnostic> diagnostics) async { | |
| await for (AnalyzerDiagnostic diagnostic in diagnostics) { | |
| if (diagnostic.uri != null) { | |
| String path = toFilePath(diagnostic.uri); | |
| if (!filesToAnalyze.contains(path)) continue; | |
| } | |
| String message = "$diagnostic"; | |
| if (seen.add(message)) { | |
| hasOutput = true; | |
| print(message); | |
| } | |
| } | |
| } | |
| Future stderrFuture = | |
| processAnalyzerOutput(parseAnalyzerOutput(process.stderr)); | |
| Future stdoutFuture = | |
| processAnalyzerOutput(parseAnalyzerOutput(process.stdout)); | |
| await process.exitCode; | |
| await stdoutFuture; | |
| await stderrFuture; | |
| sw.stop(); | |
| print("Running analyzer took: ${sw.elapsed}."); | |
| if (hasOutput) { | |
| throw "Non-empty output from analyzer."; | |
| } | |
| } | |
| Future<String> git(String command, Iterable<String> arguments, | |
| {String workingDirectory}) async { | |
| ProcessResult result = await Process.run( | |
| Platform.isWindows ? "git.bat" : "git", | |
| <String>[command]..addAll(arguments), | |
| workingDirectory: workingDirectory); | |
| if (result.exitCode != 0) { | |
| throw "Non-zero exit code from git $command (${result.exitCode})\n" | |
| "${result.stdout}\n${result.stderr}"; | |
| } | |
| return result.stdout; | |
| } |