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