forked from flutter/packages
-
Notifications
You must be signed in to change notification settings - Fork 6
Expand file tree
/
Copy pathvalidate_command.dart
More file actions
381 lines (336 loc) · 12.5 KB
/
validate_command.dart
File metadata and controls
381 lines (336 loc) · 12.5 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
// 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 'package:file/file.dart';
import 'package:meta/meta.dart';
import 'package:path/path.dart' as p;
import 'package:pub_semver/pub_semver.dart';
import 'common/core.dart';
import 'common/git_version_finder.dart';
import 'common/output_utils.dart';
import 'common/package_looping_command.dart';
import 'common/repository_package.dart';
import 'common/tool_config.dart';
import 'validators/dependabot_validator.dart';
import 'validators/gradle_validator.dart';
import 'validators/pubspec_validator.dart';
import 'validators/readme_validator.dart';
import 'validators/repo_info_validator.dart';
import 'validators/version_and_changelog_validator.dart';
const int _missingMinSdkVersionExitCode = 3;
const int _unknownVersionMappingExitCode = 4;
/// The set of possible validators.
///
/// Exposed for testing so that unit tests can target a single validator's
/// behavior via the command without having to set everything required for
/// every other validator to pass.
///
/// This is done instead of testing validators directly to ensure that testing
/// includes things like command line parsing and run initialization.
@visibleForTesting
// ignore: public_member_api_docs
enum Validator { dependabot, gradle, pubspec, readme, repoInfo, version }
/// A command to validate that packages follow various team conventions,
/// guidelines, and best practices.
///
/// This includes:
/// - repository-level metadata about packages, such as repo README and
/// auto-label entries
/// - pubspec format and contents
/// - dependabot configuration coverage
/// - gradle configurations
class ValidateCommand extends PackageLoopingCommand {
/// Creates Dependabot check command instance.
ValidateCommand(
super.packagesDir, {
this.targetedValidators,
super.processRunner,
super.platform,
super.gitDir,
}) {
argParser.addOption(
_prLabelsArg,
help:
'A comma-separated list of labels associated with this PR, '
'if applicable.\n\n'
'If supplied, labels may override or disable some checks.',
hide: true,
);
argParser.addFlag(
_checkForMissingChanges,
help:
'Validates that changes to packages include CHANGELOG and '
'version changes unless they meet an established exemption.\n\n'
'If used with --$_prLabelsArg, this should only be used in '
'pre-submit CI checks, to prevent post-submit breakage '
'when labels are no longer applicable.',
);
argParser.addFlag(
_ignorePlatformInterfaceBreaks,
help:
'Bypasses the check that platform interfaces do not contain '
'breaking changes.\n\n'
'This is only intended for use in post-submit CI checks, to '
'prevent post-submit breakage when overriding the check with '
'labels. Pre-submit checks should always use '
'--$_prLabelsArg instead.',
hide: true,
);
}
static const String _prLabelsArg = 'pr-labels';
static const String _checkForMissingChanges = 'check-for-missing-changes';
static const String _ignorePlatformInterfaceBreaks =
'ignore-platform-interface-breaks';
/// The validators to run.
///
/// If null, all validators are run.
final Set<Validator>? targetedValidators;
late Directory _repoRoot;
late final GitVersionFinder _gitVersionFinder;
late final Set<String> _prLabels = _getPRLabels();
/// Data from the root README.md table of packages.
final Map<String, List<String>> _readmeTableEntries =
<String, List<String>>{};
/// Packages with entries in labeler.yml.
final Set<String> _autoLabeledPackages = <String>{};
/// The set of directories covered by the repo's Dependabot configuration.
late DependabotCoverage _dependabotCoverage;
/// The set of packages that are allowed as dependencies.
final AllowPackageLists _allowedPackages = (
local: <String>{},
pinned: <String>{},
unpinned: <String>{},
);
/// The minimum version of Flutter that is allowed for any package.
Version? _minMinFlutterVersion;
/// The minimum version of Dart that is allowed for any package.
Version? _minMinDartVersion;
@override
final String name = 'validate';
@override
final String description = 'Checks that packages follow team guidelines.';
@override
final PackageLoopingType packageLoopingType =
PackageLoopingType.includeAllSubpackages;
@override
final bool hasLongOutput = false;
@override
Future<void> initializeRun() async {
_gitVersionFinder = await retrieveVersionFinder();
_repoRoot = packagesDir.fileSystem.directory((await gitDir).path);
if (_shouldRun(Validator.repoInfo)) {
// Extract all of the repo-level README.md table entries.
_readmeTableEntries.addAll(
RepoInfoValidator.loadReadmeTableEntries(
repoRoot: _repoRoot,
packagesDir: packagesDir,
thirdPartyPackagesDir: thirdPartyPackagesDir,
),
);
// Extract all of the labeler.yml package entries.
_autoLabeledPackages.addAll(
RepoInfoValidator.loadAutoLabeledPackages(repoRoot: _repoRoot),
);
}
if (_shouldRun(Validator.pubspec)) {
await _loadAllowedDependencies();
final (flutter: Version? minFlutter, dart: Version? minDart) =
_loadMinMinSdkVersions();
_minMinFlutterVersion = minFlutter;
_minMinDartVersion =
minDart ??
(minFlutter == null ? null : getDartSdkForFlutterSdk(minFlutter));
if (_minMinDartVersion == null) {
printError(
'Dart SDK version for Flutter SDK version $_minMinFlutterVersion is unknown. '
'Please update the map for getDartSdkForFlutterSdk with the '
'corresponding Dart version.',
);
throw ToolExit(_unknownVersionMappingExitCode);
}
}
if (_shouldRun(Validator.dependabot)) {
_dependabotCoverage = DependabotValidator.loadConfig(repoRoot: _repoRoot);
}
}
@override
Future<PackageResult> runForPackage(RepositoryPackage package) async {
final List<String> errors = [
if (_shouldRun(Validator.repoInfo)) ...await _validateRepoInfo(package),
if (_shouldRun(Validator.pubspec)) ...await _validatePubspec(package),
if (_shouldRun(Validator.readme)) ...await _validateReadme(package),
if (_shouldRun(Validator.dependabot))
...await _validateDependabot(package),
if (_shouldRun(Validator.gradle)) ...await _validateGradle(package),
if (_shouldRun(Validator.version))
...await _validateVersionAndChangelog(package),
];
return errors.isEmpty
? PackageResult.success()
: PackageResult.fail(errors);
}
bool _shouldRun(Validator validator) =>
targetedValidators?.contains(validator) ?? true;
/// Runs repo-level checks.
Future<List<String>> _validateRepoInfo(RepositoryPackage package) async {
// Repo-level checks only apply to top-level packages.
if (!package.isTopLevel) {
return <String>[];
}
final validator = RepoInfoValidator(
readmeTableEntries: _readmeTableEntries,
autoLabeledPackages: _autoLabeledPackages,
gitDir: await gitDir,
repoRoot: _repoRoot,
indentation: indentation,
);
return validator.validatePackage(package);
}
Future<List<String>> _validateDependabot(RepositoryPackage package) async {
final validator = DependabotValidator(
coverage: _dependabotCoverage,
path: path,
repoRoot: _repoRoot,
indentation: indentation,
);
return validator.validateDependabotCoverage(package);
}
Future<List<String>> _validateGradle(RepositoryPackage package) async {
if (!package.platformDirectory(FlutterPlatform.android).existsSync()) {
return [];
}
final validator = GradleValidator(path: path, indentation: indentation);
return validator.validateGradle(package);
}
Future<List<String>> _validatePubspec(RepositoryPackage package) async {
final validator = PubspecValidator(
path: path,
indentation: indentation,
warningLogger: printWarning,
allowedPackages: _allowedPackages,
repoRoot: rootDir,
minMinFlutterVersion: _minMinFlutterVersion,
minMinDartVersion: _minMinDartVersion,
);
return validator.validatePubspec(package);
}
Future<List<String>> _validateReadme(RepositoryPackage package) async {
// TODO(stuartmorgan): Consider restructuring this to just check the
// current package's README for all packages, now that this is part of an
// includeAllSubpackages command. The current logic is from when it was
// its own top-level-only command.
if (!package.isTopLevel) {
return [];
}
final validator = ReadmeValidator(
path: path,
indentation: indentation,
warningLogger: printWarning,
);
final List<String> errors = validator.validateReadme(
package.readmeFile,
mainPackage: package,
isExample: false,
);
for (final RepositoryPackage packageToCheck in package.getExamples()) {
errors.addAll(
validator.validateReadme(
packageToCheck.readmeFile,
mainPackage: package,
isExample: true,
),
);
}
// If there's an example/README.md for a multi-example package, validate
// that as well, as it will be shown on pub.dev.
final Directory exampleDir = package.directory.childDirectory('example');
final File exampleDirReadme = exampleDir.childFile('README.md');
if (exampleDir.existsSync() && !isPackage(exampleDir)) {
errors.addAll(
validator.validateReadme(
exampleDirReadme,
mainPackage: package,
isExample: true,
),
);
}
return errors;
}
Future<List<String>> _validateVersionAndChangelog(
RepositoryPackage package,
) async {
if (!package.isTopLevel) {
return [];
}
final Directory repoRoot = packagesDir.fileSystem.directory(
(await gitDir).path,
);
final validator = VersionAndChangelogValidator(
path: path,
indentation: indentation,
warningLogger: logWarning,
gitVersionFinder: _gitVersionFinder,
repoRoot: repoRoot,
changedFiles: changedFiles,
prLabels: _prLabels,
);
return validator.validateChangelogAndVersion(
package,
checkForMissingChanges: getBoolArg(_checkForMissingChanges),
ignorePlatformInterfaceBreaks: getBoolArg(_ignorePlatformInterfaceBreaks),
);
}
Stream<String> _findAllPublishedPackages() async* {
for (final File pubspecFile
in (await _repoRoot.list(recursive: true, followLinks: false).toList())
.whereType<File>()
.where(
(File entity) => p.basename(entity.path) == 'pubspec.yaml',
)) {
final Pubspec? pubspec = _tryParsePubspec(pubspecFile.readAsStringSync());
if (pubspec != null && pubspec.publishTo != 'none') {
yield pubspec.name;
}
}
}
Future<void> _loadAllowedDependencies() async {
// Find all local, published packages.
_allowedPackages.local.addAll(await _findAllPublishedPackages().toList());
final ({List<String> pinned, List<String> unpinned}) allowedDeps =
getAllowedDependencies(_repoRoot);
_allowedPackages.unpinned.addAll(allowedDeps.unpinned);
_allowedPackages.pinned.addAll(allowedDeps.pinned);
}
({Version? flutter, Version? dart}) _loadMinMinSdkVersions() {
final String? minFlutter = getMinFlutterVersion(_repoRoot);
final String? minDart = getMinDartVersion(_repoRoot);
if (minFlutter == null && minDart == null) {
printError(
'Either min_flutter or min_dart must be provided '
'in the repo tool configuration.',
);
throw ToolExit(_missingMinSdkVersionExitCode);
}
return (
flutter: minFlutter == null ? null : Version.parse(minFlutter),
dart: minDart == null ? null : Version.parse(minDart),
);
}
Pubspec? _tryParsePubspec(String pubspecContents) {
try {
return Pubspec.parse(pubspecContents);
} on Exception catch (exception) {
print(' Cannot parse pubspec.yaml: $exception');
}
return null;
}
/// Returns the labels associated with this PR, if any, or an empty set
/// if that flag is not provided.
Set<String> _getPRLabels() {
final String labels = getStringArg(_prLabelsArg);
if (labels.isEmpty) {
return <String>{};
}
return labels.split(',').map((String label) => label.trim()).toSet();
}
}