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