| // Copyright 2013 The Flutter 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:io' as io; |
| |
| import 'package:file/file.dart'; |
| |
| import 'common/core.dart'; |
| import 'common/file_filters.dart'; |
| import 'common/flutter_command_utils.dart'; |
| import 'common/gradle.dart'; |
| import 'common/output_utils.dart'; |
| import 'common/package_looping_command.dart'; |
| import 'common/plugin_utils.dart'; |
| import 'common/repository_package.dart'; |
| import 'common/xcode.dart'; |
| |
| /// A command to run Dart analysis on packages. |
| class AnalyzeCommand extends PackageLoopingCommand { |
| /// Creates a analysis command instance. |
| AnalyzeCommand( |
| super.packagesDir, { |
| super.processRunner, |
| super.platform, |
| super.gitDir, |
| }) { |
| // Platform options. |
| // By default, only Dart analysis is run. |
| argParser.addFlag(_dartFlag, help: "Runs 'dart analyze'", defaultsTo: true); |
| argParser.addFlag(platformAndroid, |
| help: "Runs 'gradle lint' on Android code"); |
| argParser.addFlag(platformIOS, |
| help: "Runs 'xcodebuild analyze' on iOS code"); |
| argParser.addFlag(platformMacOS, |
| help: "Runs 'xcodebuild analyze' on macOS code"); |
| |
| // Dart options. |
| argParser.addMultiOption(_customAnalysisFlag, |
| help: |
| 'Directories (comma separated) that are allowed to have their own ' |
| 'analysis options.\n\n' |
| 'Alternately, a list of one or more YAML files that contain a list ' |
| 'of allowed directories.', |
| defaultsTo: <String>[]); |
| argParser.addOption(_analysisSdk, |
| valueHelp: 'dart-sdk', |
| help: 'An optional path to a Dart SDK; this is used to override the ' |
| 'SDK used to provide analysis.'); |
| argParser.addFlag(_downgradeFlag, |
| help: 'Runs "flutter pub downgrade" before analysis to verify that ' |
| 'the minimum constraints are sufficiently new for APIs used.'); |
| argParser.addFlag(_libOnlyFlag, |
| help: 'Only analyze the lib/ directory of the main package, not the ' |
| 'entire package.'); |
| argParser.addFlag(_skipIfResolvingFailsFlag, |
| help: 'If resolution fails, skip the package. This is only ' |
| 'intended to be used with pathified analysis, where a resolver ' |
| 'failure indicates that no out-of-band failure can result anyway.', |
| hide: true); |
| |
| // Xcode options. |
| argParser.addOption(_minIOSVersionArg, |
| help: 'Sets the minimum iOS deployment version to use when compiling, ' |
| 'overriding the default minimum version. This can be used to find ' |
| 'deprecation warnings that will affect the plugin in the future.'); |
| argParser.addOption(_minMacOSVersionArg, |
| help: |
| 'Sets the minimum macOS deployment version to use when compiling, ' |
| 'overriding the default minimum version. This can be used to find ' |
| 'deprecation warnings that will affect the plugin in the future.'); |
| } |
| |
| static const String _dartFlag = 'dart'; |
| static const String _customAnalysisFlag = 'custom-analysis'; |
| static const String _downgradeFlag = 'downgrade'; |
| static const String _libOnlyFlag = 'lib-only'; |
| static const String _analysisSdk = 'analysis-sdk'; |
| static const String _skipIfResolvingFailsFlag = 'skip-if-resolving-fails'; |
| static const String _minIOSVersionArg = 'ios-min-version'; |
| static const String _minMacOSVersionArg = 'macos-min-version'; |
| |
| late String _dartBinaryPath; |
| |
| Set<String> _allowedCustomAnalysisDirectories = const <String>{}; |
| |
| @override |
| final String name = 'analyze'; |
| |
| @override |
| final String description = 'Analyzes all packages using dart analyze.\n\n' |
| 'This command requires "dart" and "flutter" to be in your path.'; |
| |
| /// Checks that there are no unexpected analysis_options.yaml files. |
| bool _hasUnexpectedAnalysisOptions(RepositoryPackage package) { |
| final List<FileSystemEntity> files = |
| package.directory.listSync(recursive: true, followLinks: false); |
| for (final FileSystemEntity file in files) { |
| if (file.basename != 'analysis_options.yaml' && |
| file.basename != '.analysis_options') { |
| continue; |
| } |
| |
| final bool allowed = _allowedCustomAnalysisDirectories.any( |
| (String directory) => |
| directory.isNotEmpty && |
| path.isWithin( |
| packagesDir.childDirectory(directory).path, file.path)); |
| if (allowed) { |
| continue; |
| } |
| |
| printError( |
| 'Found an extra analysis_options.yaml at ${file.absolute.path}.'); |
| printError( |
| 'If this was deliberate, pass the package to the analyze command ' |
| 'with the --$_customAnalysisFlag flag and try again.'); |
| return true; |
| } |
| return false; |
| } |
| |
| @override |
| bool shouldIgnoreFile(String path) { |
| // Support files don't affect any analysis. |
| if (isRepoLevelNonCodeImpactingFile(path) || isPackageSupportFile(path)) { |
| return true; |
| } |
| |
| // For native code, it depends on the flags. |
| if (path.endsWith('.dart')) { |
| return !getBoolArg(_dartFlag); |
| } |
| if (path.endsWith('.java') || path.endsWith('.kt')) { |
| return !getBoolArg(platformAndroid); |
| } |
| if (path.endsWith('.c') || |
| path.endsWith('.cc') || |
| path.endsWith('.cpp') || |
| path.endsWith('.h')) { |
| // If C/C++ linting is added, Windows and Linux should be added here. |
| return !(getBoolArg(platformIOS) || getBoolArg(platformMacOS)); |
| } |
| if (path.endsWith('.m') || |
| path.endsWith('.mm') || |
| path.endsWith('.swift')) { |
| return !(getBoolArg(platformIOS) || getBoolArg(platformMacOS)); |
| } |
| |
| return false; |
| } |
| |
| @override |
| Future<void> initializeRun() async { |
| _allowedCustomAnalysisDirectories = getYamlListArg(_customAnalysisFlag); |
| |
| // Use the Dart SDK override if one was passed in. |
| final String? dartSdk = argResults![_analysisSdk] as String?; |
| _dartBinaryPath = |
| dartSdk == null ? 'dart' : path.join(dartSdk, 'bin', 'dart'); |
| } |
| |
| @override |
| Future<PackageResult> runForPackage(RepositoryPackage package) async { |
| final Map<String, PackageResult> subResults = <String, PackageResult>{}; |
| if (getBoolArg(_dartFlag)) { |
| _printSectionHeading('Running dart analyze.'); |
| subResults['Dart'] = await _runDartAnalysisForPackage(package); |
| } |
| if (getBoolArg(platformAndroid)) { |
| _printSectionHeading('Running gradle lint.'); |
| subResults['Android'] = await _runGradleLintForPackage(package); |
| } |
| if (getBoolArg(platformIOS)) { |
| _printSectionHeading('Running iOS xcodebuild analyze.'); |
| final String minIOSVersion = getStringArg(_minIOSVersionArg); |
| subResults['iOS'] = await _runXcodeAnalysisForPackage( |
| package, |
| FlutterPlatform.ios, |
| extraFlags: <String>[ |
| '-destination', |
| 'generic/platform=iOS Simulator', |
| if (minIOSVersion.isNotEmpty) |
| 'IPHONEOS_DEPLOYMENT_TARGET=$minIOSVersion', |
| ], |
| ); |
| } |
| if (getBoolArg(platformMacOS)) { |
| _printSectionHeading('Running macOS xcodebuild analyze.'); |
| final String minMacOSVersion = getStringArg(_minMacOSVersionArg); |
| subResults['macOS'] = await _runXcodeAnalysisForPackage( |
| package, |
| FlutterPlatform.macos, |
| extraFlags: <String>[ |
| if (minMacOSVersion.isNotEmpty) |
| 'MACOSX_DEPLOYMENT_TARGET=$minMacOSVersion', |
| ], |
| ); |
| } |
| |
| // Make sure at least one analysis option was requested. |
| if (subResults.isEmpty) { |
| printError('At least one analysis option flag must be provided.'); |
| throw ToolExit(exitInvalidArguments); |
| } |
| // If only one analysis was requested, just return its result. |
| if (subResults.length == 1) { |
| return subResults.values.first; |
| } |
| // Otherwise, aggregate the messages, with the least positive status. |
| final Map<String, PackageResult> failedResults = |
| Map<String, PackageResult>.of(subResults) |
| ..removeWhere((String key, PackageResult value) => |
| value.state != RunState.failed); |
| final Map<String, PackageResult> skippedResults = |
| Map<String, PackageResult>.of(subResults) |
| ..removeWhere((String key, PackageResult value) => |
| value.state != RunState.skipped); |
| // If anything failed, collect all the failure messages, prefixed by type. |
| if (failedResults.isNotEmpty) { |
| return PackageResult.fail(<String>[ |
| for (final MapEntry<String, PackageResult> entry |
| in failedResults.entries) |
| '${entry.key}${entry.value.details.isEmpty ? '' : ': ${entry.value.details.join(', ')}'}' |
| ]); |
| } |
| // If everything was skipped, mark as skipped with all of the explanations. |
| if (skippedResults.length == subResults.length) { |
| return PackageResult.skip(skippedResults.entries |
| .map((MapEntry<String, PackageResult> entry) => |
| '${entry.key}: ${entry.value.details.first}') |
| .join(', ')); |
| } |
| // For all succes, or a mix of success and skip, log any skips but mark as |
| // success. |
| for (final MapEntry<String, PackageResult> skip in skippedResults.entries) { |
| printSkip('Skipped ${skip.key}: ${skip.value.details.first}'); |
| } |
| return PackageResult.success(); |
| } |
| |
| void _printSectionHeading(String heading) { |
| print('\n$heading'); |
| print('--------------------'); |
| } |
| |
| /// Runs Dart analysis for the given package, and returns the result that |
| /// applies to that analysis. |
| Future<PackageResult> _runDartAnalysisForPackage( |
| RepositoryPackage package) async { |
| final bool libOnly = getBoolArg(_libOnlyFlag); |
| |
| if (libOnly && !package.libDirectory.existsSync()) { |
| return PackageResult.skip('No lib/ directory.'); |
| } |
| |
| if (getBoolArg(_downgradeFlag)) { |
| if (!await _runPubCommand(package, 'downgrade')) { |
| return PackageResult.fail(<String>['Unable to downgrade dependencies']); |
| } |
| } |
| |
| // Analysis runs over the package and all subpackages (unless only lib/ is |
| // being analyzed), so all of them need `flutter pub get` run before |
| // analyzing. `example` packages can be skipped since 'flutter packages get' |
| // automatically runs `pub get` in examples as part of handling the parent |
| // directory. |
| final List<RepositoryPackage> packagesToGet = <RepositoryPackage>[ |
| package, |
| if (!libOnly) ...await getSubpackages(package).toList(), |
| ]; |
| for (final RepositoryPackage packageToGet in packagesToGet) { |
| if (packageToGet.directory.basename != 'example' || |
| !RepositoryPackage(packageToGet.directory.parent) |
| .pubspecFile |
| .existsSync()) { |
| if (!await _runPubCommand(packageToGet, 'get')) { |
| if (getBoolArg(_skipIfResolvingFailsFlag)) { |
| // Re-run, capturing output, to see if the failure was a resolver |
| // failure. (This is slightly inefficient, but this should be a |
| // very rare case.) |
| const String resolverFailureMessage = 'version solving failed'; |
| final io.ProcessResult result = await processRunner.run( |
| flutterCommand, <String>['pub', 'get'], |
| workingDir: packageToGet.directory); |
| if ((result.stderr as String).contains(resolverFailureMessage) || |
| (result.stdout as String).contains(resolverFailureMessage)) { |
| logWarning('Skipping package due to pub resolution failure.'); |
| return PackageResult.skip('Resolution failed.'); |
| } |
| } |
| return PackageResult.fail(<String>['Unable to get dependencies']); |
| } |
| } |
| } |
| |
| if (_hasUnexpectedAnalysisOptions(package)) { |
| return PackageResult.fail(<String>['Unexpected local analysis options']); |
| } |
| final int exitCode = await processRunner.runAndStream(_dartBinaryPath, |
| <String>['analyze', '--fatal-infos', if (libOnly) 'lib'], |
| workingDir: package.directory); |
| if (exitCode != 0) { |
| return PackageResult.fail(); |
| } |
| return PackageResult.success(); |
| } |
| |
| Future<bool> _runPubCommand(RepositoryPackage package, String command) async { |
| final int exitCode = await processRunner.runAndStream( |
| flutterCommand, <String>['pub', command], |
| workingDir: package.directory); |
| return exitCode == 0; |
| } |
| |
| /// Runs Gradle lint analysis for the given package, and returns the result |
| /// that applies to that analysis. |
| Future<PackageResult> _runGradleLintForPackage( |
| RepositoryPackage package) async { |
| if (!pluginSupportsPlatform(platformAndroid, package, |
| requiredMode: PlatformSupport.inline)) { |
| return PackageResult.skip( |
| 'Package does not contain native Android plugin code'); |
| } |
| |
| for (final RepositoryPackage example in package.getExamples()) { |
| final GradleProject project = GradleProject(example, |
| processRunner: processRunner, platform: platform); |
| |
| if (!project.isConfigured()) { |
| final bool buildSuccess = await runConfigOnlyBuild( |
| example, processRunner, platform, FlutterPlatform.android); |
| if (!buildSuccess) { |
| printError('Unable to configure Gradle project.'); |
| return PackageResult.fail(<String>['Unable to configure Gradle.']); |
| } |
| } |
| |
| final String packageName = package.directory.basename; |
| |
| // Only lint one build mode to avoid extra work. |
| // Only lint the plugin project itself, to avoid failing due to errors in |
| // dependencies. |
| // |
| // TODO(stuartmorgan): Consider adding an XML parser to read and summarize |
| // all results. Currently, only the first three errors will be shown |
| // inline, and the rest have to be checked via the CI-uploaded artifact. |
| final int exitCode = await project.runCommand('$packageName:lintDebug'); |
| if (exitCode != 0) { |
| return PackageResult.fail(); |
| } |
| } |
| |
| return PackageResult.success(); |
| } |
| |
| /// Analyzes [plugin] for [targetPlatform]. |
| Future<PackageResult> _runXcodeAnalysisForPackage( |
| RepositoryPackage package, |
| FlutterPlatform targetPlatform, { |
| List<String> extraFlags = const <String>[], |
| }) async { |
| final String platformString = |
| targetPlatform == FlutterPlatform.ios ? 'iOS' : 'macOS'; |
| if (!pluginSupportsPlatform(targetPlatform.name, package, |
| requiredMode: PlatformSupport.inline)) { |
| return PackageResult.skip( |
| 'Package does not contain native $platformString plugin code'); |
| } |
| |
| final Xcode xcode = Xcode(processRunner: processRunner, log: true); |
| final List<String> errors = <String>[]; |
| for (final RepositoryPackage example in package.getExamples()) { |
| // See https://github.com/flutter/flutter/issues/172427 for discussion of |
| // why this is currently necessary. |
| print('Disabling Swift Package Manager...'); |
| setSwiftPackageManagerState(example, enabled: false); |
| |
| // Unconditionally re-run build with --debug --config-only, to ensure that |
| // the project is in a debug state even if it was previously configured, |
| // and that SwiftPM is disabled. |
| print('Running flutter build --config-only...'); |
| final bool buildSuccess = await runConfigOnlyBuild( |
| example, |
| processRunner, |
| platform, |
| targetPlatform, |
| buildDebug: true, |
| ); |
| if (!buildSuccess) { |
| printError('Unable to prepare native project files.'); |
| errors.add( |
| 'Unable to build ${getRelativePosixPath(example.directory, from: package.directory)}.'); |
| continue; |
| } |
| |
| // Running tests and static analyzer. |
| final String examplePath = getRelativePosixPath(example.directory, |
| from: package.directory.parent); |
| print('Running $platformString tests and analyzer for $examplePath...'); |
| final int exitCode = await xcode.runXcodeBuild( |
| example.directory, |
| platformString, |
| // Clean before analyzing to remove cached swiftmodules from previous |
| // runs, which can cause conflicts. |
| actions: <String>['clean', 'analyze'], |
| workspace: '${platformString.toLowerCase()}/Runner.xcworkspace', |
| scheme: 'Runner', |
| configuration: 'Debug', |
| hostPlatform: platform, |
| extraFlags: <String>[ |
| ...extraFlags, |
| 'GCC_TREAT_WARNINGS_AS_ERRORS=YES', |
| ], |
| ); |
| if (exitCode == 0) { |
| printSuccess('$examplePath ($platformString) passed analysis.'); |
| } else { |
| printError('$examplePath ($platformString) failed analysis.'); |
| errors.add( |
| '${getRelativePosixPath(example.directory, from: package.directory)} failed analysis.'); |
| } |
| |
| print('Removing Swift Package Manager override...'); |
| setSwiftPackageManagerState(example, enabled: null); |
| } |
| return errors.isEmpty |
| ? PackageResult.success() |
| : PackageResult.fail(errors); |
| } |
| } |