forked from flutter/packages
-
Notifications
You must be signed in to change notification settings - Fork 6
Expand file tree
/
Copy pathanalyze_command.dart
More file actions
501 lines (461 loc) · 16.9 KB
/
analyze_command.dart
File metadata and controls
501 lines (461 loc) · 16.9 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
// Copyright 2013 The Flutter Authors
// 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 file in files) {
if (file.basename != 'analysis_options.yaml' &&
file.basename != '.analysis_options') {
continue;
}
// Skip anything checked out inside of .dart_tool/.
if (file.path.contains('/.dart_tool/')) {
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 dartSdk = argResults![_analysisSdk] as String?;
_dartBinaryPath = dartSdk == null
? 'dart'
: path.join(dartSdk, 'bin', 'dart');
}
@override
Future<PackageResult> runForPackage(RepositoryPackage package) async {
final 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 failedResults = Map<String, PackageResult>.of(subResults)
..removeWhere(
(String key, PackageResult value) => value.state != RunState.failed,
);
final 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 resolve downgraded 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 pub get'
// automatically runs `pub get` in examples as part of handling the parent
// directory.
final packagesToGet = <RepositoryPackage>[
package,
if (!libOnly) ...package.getSubpackages(),
];
for (final 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 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 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 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(processRunner: processRunner, log: true);
final 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);
}
}