// Copyright 2015 The Chromium Authors. 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:async'; import 'dart:collection'; import 'package:args/args.dart'; import 'package:yaml/yaml.dart' as yaml; import '../base/common.dart'; import '../base/file_system.dart'; import '../base/process.dart'; import '../cache.dart'; import '../dart/analysis.dart'; import '../globals.dart'; import 'analyze.dart'; import 'analyze_base.dart'; bool isDartFile(FileSystemEntity entry) => entry is File && entry.path.endsWith('.dart'); typedef bool FileFilter(FileSystemEntity entity); /// An aspect of the [AnalyzeCommand] to perform once time analysis. class AnalyzeOnce extends AnalyzeBase { AnalyzeOnce(ArgResults argResults, this.repoPackages, { this.workingDirectory }) : super(argResults); final List<Directory> repoPackages; /// The working directory for testing analysis using dartanalyzer final Directory workingDirectory; @override Future<Null> analyze() async { final Stopwatch stopwatch = new Stopwatch()..start(); final Set<Directory> pubSpecDirectories = new HashSet<Directory>(); final List<File> dartFiles = <File>[]; for (String file in argResults.rest.toList()) { file = fs.path.normalize(fs.path.absolute(file)); final String root = fs.path.rootPrefix(file); dartFiles.add(fs.file(file)); while (file != root) { file = fs.path.dirname(file); if (fs.isFileSync(fs.path.join(file, 'pubspec.yaml'))) { pubSpecDirectories.add(fs.directory(file)); break; } } } final bool currentPackage = argResults['current-package'] && (argResults.wasParsed('current-package') || dartFiles.isEmpty); final bool flutterRepo = argResults['flutter-repo'] || (workingDirectory == null && inRepo(argResults.rest)); // Use dartanalyzer directly except when analyzing the Flutter repository. // Analyzing the repository requires a more complex report than dartanalyzer // currently supports (e.g. missing member dartdoc summary). // TODO(danrubel): enhance dartanalyzer to provide this type of summary if (!flutterRepo) { final List<String> arguments = <String>[]; arguments.addAll(dartFiles.map((FileSystemEntity f) => f.path)); if (arguments.isEmpty || currentPackage) { // workingDirectory is non-null only when testing flutter analyze final Directory currentDirectory = workingDirectory ?? fs.currentDirectory.absolute; final Directory projectDirectory = await projectDirectoryContaining(currentDirectory); if (projectDirectory != null) { arguments.add(projectDirectory.path); } else if (arguments.isEmpty) { arguments.add(currentDirectory.path); } } // If the files being analyzed are outside of the current directory hierarchy // then dartanalyzer does not yet know how to find the ".packages" file. // TODO(danrubel): fix dartanalyzer to find the .packages file final File packagesFile = await packagesFileFor(arguments); if (packagesFile != null) { arguments.insert(0, '--packages'); arguments.insert(1, packagesFile.path); } final String dartanalyzer = fs.path.join(Cache.flutterRoot, 'bin', 'cache', 'dart-sdk', 'bin', 'dartanalyzer'); arguments.insert(0, dartanalyzer); bool noErrors = false; int exitCode = await runCommandAndStreamOutput( arguments, workingDirectory: workingDirectory?.path, mapFunction: (String line) { // Workaround for the fact that dartanalyzer does not exit with a non-zero exit code // when errors are found. // TODO(danrubel): Fix dartanalyzer to return non-zero exit code if (line == 'No issues found!') noErrors = true; return line; }, ); stopwatch.stop(); final String elapsed = (stopwatch.elapsedMilliseconds / 1000.0).toStringAsFixed(1); // Workaround for the fact that dartanalyzer does not exit with a non-zero exit code // when errors are found. // TODO(danrubel): Fix dartanalyzer to return non-zero exit code if (exitCode == 0 && !noErrors) exitCode = 1; if (exitCode != 0) throwToolExit('(Ran in ${elapsed}s)', exitCode: exitCode); printStatus('Ran in ${elapsed}s'); return; } //TODO (pq): revisit package and directory defaults for (Directory dir in repoPackages) { _collectDartFiles(dir, dartFiles); pubSpecDirectories.add(dir); } // determine what all the various .packages files depend on final PackageDependencyTracker dependencies = new PackageDependencyTracker(); for (Directory directory in pubSpecDirectories) { final String pubSpecYamlPath = fs.path.join(directory.path, 'pubspec.yaml'); final File pubSpecYamlFile = fs.file(pubSpecYamlPath); if (pubSpecYamlFile.existsSync()) { // we are analyzing the actual canonical source for this package; // make sure we remember that, in case all the packages are actually // pointing elsewhere somehow. final yaml.YamlMap pubSpecYaml = yaml.loadYaml(fs.file(pubSpecYamlPath).readAsStringSync()); final String packageName = pubSpecYaml['name']; final String packagePath = fs.path.normalize(fs.path.absolute(fs.path.join(directory.path, 'lib'))); dependencies.addCanonicalCase(packageName, packagePath, pubSpecYamlPath); } final String dotPackagesPath = fs.path.join(directory.path, '.packages'); final File dotPackages = fs.file(dotPackagesPath); if (dotPackages.existsSync()) { // this directory has opinions about what we should be using dotPackages .readAsStringSync() .split('\n') .where((String line) => !line.startsWith(new RegExp(r'^ *#'))) .forEach((String line) { final int colon = line.indexOf(':'); if (colon > 0) { final String packageName = line.substring(0, colon); final String packagePath = fs.path.fromUri(line.substring(colon+1)); // Ensure that we only add the `analyzer` package defined in the vended SDK (and referred to with a local fs.path. directive). // Analyzer package versions reached via transitive dependencies (e.g., via `test`) are ignored since they would produce // spurious conflicts. if (packageName != 'analyzer' || packagePath.startsWith('..')) dependencies.add(packageName, fs.path.normalize(fs.path.absolute(directory.path, packagePath)), dotPackagesPath); } }); } } // prepare a union of all the .packages files if (dependencies.hasConflicts) { final StringBuffer message = new StringBuffer(); message.writeln(dependencies.generateConflictReport()); message.writeln('Make sure you have run "pub upgrade" in all the directories mentioned above.'); if (dependencies.hasConflictsAffectingFlutterRepo) { message.writeln( 'For packages in the flutter repository, try using ' '"flutter update-packages --upgrade" to do all of them at once.'); } message.write( 'If this does not help, to track down the conflict you can use ' '"pub deps --style=list" and "pub upgrade --verbosity=solver" in the affected directories.'); throwToolExit(message.toString()); } final Map<String, String> packages = dependencies.asPackageMap(); Cache.releaseLockEarly(); if (argResults['preamble']) { if (dartFiles.length == 1) { logger.printStatus('Analyzing ${fs.path.relative(dartFiles.first.path)}...'); } else { logger.printStatus('Analyzing ${dartFiles.length} files...'); } } final DriverOptions options = new DriverOptions(); options.dartSdkPath = argResults['dart-sdk']; options.packageMap = packages; options.analysisOptionsFile = fs.path.join(Cache.flutterRoot, '.analysis_options_repo'); final AnalysisDriver analyzer = new AnalysisDriver(options); // TODO(pq): consider error handling final List<AnalysisErrorDescription> errors = analyzer.analyze(dartFiles); int errorCount = 0; int membersMissingDocumentation = 0; for (AnalysisErrorDescription error in errors) { bool shouldIgnore = false; if (error.errorCode.name == 'public_member_api_docs') { // https://github.com/dart-lang/linter/issues/208 if (isFlutterLibrary(error.source.fullName)) { if (!argResults['dartdocs']) { membersMissingDocumentation += 1; shouldIgnore = true; } } else { shouldIgnore = true; } } if (shouldIgnore) continue; printError(error.asString()); errorCount += 1; } dumpErrors(errors.map<String>((AnalysisErrorDescription error) => error.asString())); stopwatch.stop(); final String elapsed = (stopwatch.elapsedMilliseconds / 1000.0).toStringAsFixed(1); if (isBenchmarking) writeBenchmark(stopwatch, errorCount, membersMissingDocumentation); if (errorCount > 0) { // we consider any level of error to be an error exit (we don't report different levels) if (membersMissingDocumentation > 0) throwToolExit('[lint] $membersMissingDocumentation public ${ membersMissingDocumentation == 1 ? "member lacks" : "members lack" } documentation (ran in ${elapsed}s)'); else throwToolExit('(Ran in ${elapsed}s)'); } if (argResults['congratulate']) { if (membersMissingDocumentation > 0) { printStatus('No analyzer warnings! (ran in ${elapsed}s; $membersMissingDocumentation public ${ membersMissingDocumentation == 1 ? "member lacks" : "members lack" } documentation)'); } else { printStatus('No analyzer warnings! (ran in ${elapsed}s)'); } } } /// Return a path to the ".packages" file for use by dartanalyzer when analyzing the specified files. /// Report an error if there are file paths that belong to different projects. Future<File> packagesFileFor(List<String> filePaths) async { String projectPath = await projectPathContaining(filePaths.first); if (projectPath != null) { if (projectPath.endsWith(fs.path.separator)) projectPath = projectPath.substring(0, projectPath.length - 1); final String projectPrefix = projectPath + fs.path.separator; // Assert that all file paths are contained in the same project directory for (String filePath in filePaths) { if (!filePath.startsWith(projectPrefix) && filePath != projectPath) throwToolExit('Files in different projects cannot be analyzed at the same time.\n' ' Project: $projectPath\n File outside project: $filePath'); } } else { // Assert that all file paths are not contained in any project for (String filePath in filePaths) { final String otherProjectPath = await projectPathContaining(filePath); if (otherProjectPath != null) throwToolExit('Files inside a project cannot be analyzed at the same time as files not in any project.\n' ' File inside a project: $filePath'); } } if (projectPath == null) return null; final File packagesFile = fs.file(fs.path.join(projectPath, '.packages')); return await packagesFile.exists() ? packagesFile : null; } Future<String> projectPathContaining(String targetPath) async { final FileSystemEntity target = await fs.isDirectory(targetPath) ? fs.directory(targetPath) : fs.file(targetPath); final Directory projectDirectory = await projectDirectoryContaining(target); return projectDirectory?.path; } Future<Directory> projectDirectoryContaining(FileSystemEntity entity) async { Directory dir = entity is Directory ? entity : entity.parent; dir = dir.absolute; while (!await dir.childFile('pubspec.yaml').exists()) { final Directory parent = dir.parent; if (parent == null || parent.path == dir.path) return null; dir = parent; } return dir; } List<String> flutterRootComponents; bool isFlutterLibrary(String filename) { flutterRootComponents ??= fs.path.normalize(fs.path.absolute(Cache.flutterRoot)).split(fs.path.separator); final List<String> filenameComponents = fs.path.normalize(fs.path.absolute(filename)).split(fs.path.separator); if (filenameComponents.length < flutterRootComponents.length + 4) // the 4: 'packages', package_name, 'lib', file_name return false; for (int index = 0; index < flutterRootComponents.length; index += 1) { if (flutterRootComponents[index] != filenameComponents[index]) return false; } if (filenameComponents[flutterRootComponents.length] != 'packages') return false; if (filenameComponents[flutterRootComponents.length + 1] == 'flutter_tools') return false; if (filenameComponents[flutterRootComponents.length + 2] != 'lib') return false; return true; } List<File> _collectDartFiles(Directory dir, List<File> collected) { // Bail out in case of a .dartignore. if (fs.isFileSync(fs.path.join(dir.path, '.dartignore'))) return collected; for (FileSystemEntity entity in dir.listSync(recursive: false, followLinks: false)) { if (isDartFile(entity)) collected.add(entity); if (entity is Directory) { final String name = fs.path.basename(entity.path); if (!name.startsWith('.') && name != 'packages') _collectDartFiles(entity, collected); } } return collected; } } class PackageDependency { // This is a map from dependency targets (lib directories) to a list // of places that ask for that target (.packages or pubspec.yaml files) Map<String, List<String>> values = <String, List<String>>{}; String canonicalSource; void addCanonicalCase(String packagePath, String pubSpecYamlPath) { assert(canonicalSource == null); add(packagePath, pubSpecYamlPath); canonicalSource = pubSpecYamlPath; } void add(String packagePath, String sourcePath) { values.putIfAbsent(packagePath, () => <String>[]).add(sourcePath); } bool get hasConflict => values.length > 1; bool get hasConflictAffectingFlutterRepo { assert(fs.path.isAbsolute(Cache.flutterRoot)); for (List<String> targetSources in values.values) { for (String source in targetSources) { assert(fs.path.isAbsolute(source)); if (fs.path.isWithin(Cache.flutterRoot, source)) return true; } } return false; } void describeConflict(StringBuffer result) { assert(hasConflict); final List<String> targets = values.keys.toList(); targets.sort((String a, String b) => values[b].length.compareTo(values[a].length)); for (String target in targets) { final int count = values[target].length; result.writeln(' $count ${count == 1 ? 'source wants' : 'sources want'} "$target":'); bool canonical = false; for (String source in values[target]) { result.writeln(' $source'); if (source == canonicalSource) canonical = true; } if (canonical) { result.writeln(' (This is the actual package definition, so it is considered the canonical "right answer".)'); } } } String get target => values.keys.single; } class PackageDependencyTracker { // This is a map from package names to objects that track the paths // involved (sources and targets). Map<String, PackageDependency> packages = <String, PackageDependency>{}; PackageDependency getPackageDependency(String packageName) { return packages.putIfAbsent(packageName, () => new PackageDependency()); } void addCanonicalCase(String packageName, String packagePath, String pubSpecYamlPath) { getPackageDependency(packageName).addCanonicalCase(packagePath, pubSpecYamlPath); } void add(String packageName, String packagePath, String dotPackagesPath) { getPackageDependency(packageName).add(packagePath, dotPackagesPath); } bool get hasConflicts { return packages.values.any((PackageDependency dependency) => dependency.hasConflict); } bool get hasConflictsAffectingFlutterRepo { return packages.values.any((PackageDependency dependency) => dependency.hasConflictAffectingFlutterRepo); } String generateConflictReport() { assert(hasConflicts); final StringBuffer result = new StringBuffer(); for (String package in packages.keys.where((String package) => packages[package].hasConflict)) { result.writeln('Package "$package" has conflicts:'); packages[package].describeConflict(result); } return result.toString(); } Map<String, String> asPackageMap() { final Map<String, String> result = <String, String>{}; for (String package in packages.keys) result[package] = packages[package].target; return result; } }