// 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 '../base/common.dart'; import '../base/file_system.dart'; import '../base/process.dart'; import '../base/utils.dart'; import '../cache.dart'; import '../dart/analysis.dart'; import '../dart/sdk.dart' as sdk; 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, this.previewDart2: false, }) : super(argResults); final List<Directory> repoPackages; /// The working directory for testing analysis using dartanalyzer final Directory workingDirectory; final bool previewDart2; @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) { if (argResults['dartdocs']) throwToolExit('The --dartdocs option is currently only supported with --flutter-repo.'); 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); } if (previewDart2) { arguments.add('--preview-dart-2'); } else { arguments.add('--no-preview-dart-2'); } final String sdkPath = argResults['dart-sdk'] ?? sdk.dartSdkPath; final String dartanalyzer = fs.path.join(sdkPath, 'bin', 'dartanalyzer'); arguments.insert(0, dartanalyzer); bool noErrors = false; final Set<String> issues = new Set<String>(); int exitCode = await runCommandAndStreamOutput( arguments, workingDirectory: workingDirectory?.path, mapFunction: (String line) { // De-duplicate the dartanalyzer command output (https://github.com/dart-lang/sdk/issues/25697). if (line.startsWith(' ')) { if (!issues.add(line.trim())) return null; } // 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; // Remove text about the issue count ('2 hints found.'); with the duplicates // above, the printed count would be incorrect. if (line.endsWith(' found.')) return null; return line; }, ); stopwatch.stop(); if (issues.isNotEmpty) printStatus('${issues.length} ${pluralize('issue', issues.length)} found.'); 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; } 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(); dependencies.checkForConflictingDependencies(pubSpecDirectories, dependencies); 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.yaml'); 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; } }