1// Copyright 2014 The Flutter Authors. All rights reserved.
2// Use of this source code is governed by a BSD-style license that can be
3// found in the LICENSE file.
4
5import 'dart:convert';
6import 'dart:core' hide print;
7import 'dart:io' hide exit;
8import 'dart:typed_data';
9
10import 'package:analyzer/dart/analysis/features.dart';
11import 'package:analyzer/dart/analysis/results.dart';
12import 'package:analyzer/dart/analysis/utilities.dart';
13import 'package:analyzer/dart/ast/ast.dart';
14import 'package:analyzer/dart/ast/visitor.dart';
15import 'package:collection/equality.dart';
16import 'package:crypto/crypto.dart';
17import 'package:meta/meta.dart';
18import 'package:path/path.dart' as path;
19
20import 'allowlist.dart';
21import 'custom_rules/analyze.dart';
22import 'custom_rules/avoid_future_catcherror.dart';
23import 'custom_rules/no_double_clamp.dart';
24import 'custom_rules/no_stop_watches.dart';
25import 'custom_rules/protect_public_state_subtypes.dart';
26import 'custom_rules/render_box_intrinsics.dart';
27import 'run_command.dart';
28import 'utils.dart';
29
30final String flutterPackages = path.join(flutterRoot, 'packages');
31final String flutterExamples = path.join(flutterRoot, 'examples');
32
33/// The path to the `dart` executable; set at the top of `main`
34late final String dart;
35
36/// The path to the `pub` executable; set at the top of `main`
37late final String pub;
38
39/// When you call this, you can pass additional arguments to pass custom
40/// arguments to flutter analyze. For example, you might want to call this
41/// script with the parameter --dart-sdk to use custom dart sdk.
42///
43/// For example:
44/// bin/cache/dart-sdk/bin/dart dev/bots/analyze.dart --dart-sdk=/tmp/dart-sdk
45Future<void> main(List<String> arguments) async {
46 final String dartSdk = path.join(
47 Directory.current.absolute.path,
48 _getDartSdkFromArguments(arguments) ?? path.join(flutterRoot, 'bin', 'cache', 'dart-sdk'),
49 );
50 dart = path.join(dartSdk, 'bin', Platform.isWindows ? 'dart.exe' : 'dart');
51 pub = path.join(dartSdk, 'bin', Platform.isWindows ? 'pub.bat' : 'pub');
52 printProgress('STARTING ANALYSIS');
53 await run(arguments);
54 if (hasError) {
55 reportErrorsAndExit('${bold}Analysis failed.$reset');
56 }
57 reportSuccessAndExit('${bold}Analysis successful.$reset');
58}
59
60/// Scans [arguments] for an argument of the form `--dart-sdk` or
61/// `--dart-sdk=...` and returns the configured SDK, if any.
62String? _getDartSdkFromArguments(List<String> arguments) {
63 String? result;
64 for (int i = 0; i < arguments.length; i += 1) {
65 if (arguments[i] == '--dart-sdk') {
66 if (result != null) {
67 foundError(<String>['The --dart-sdk argument must not be used more than once.']);
68 return null;
69 }
70 if (i + 1 < arguments.length) {
71 result = arguments[i + 1];
72 } else {
73 foundError(<String>['--dart-sdk must be followed by a path.']);
74 return null;
75 }
76 }
77 if (arguments[i].startsWith('--dart-sdk=')) {
78 if (result != null) {
79 foundError(<String>['The --dart-sdk argument must not be used more than once.']);
80 return null;
81 }
82 result = arguments[i].substring('--dart-sdk='.length);
83 }
84 }
85 return result;
86}
87
88Future<void> run(List<String> arguments) async {
89 bool assertsEnabled = false;
90 assert(() {
91 assertsEnabled = true;
92 return true;
93 }());
94 if (!assertsEnabled) {
95 foundError(<String>['The analyze.dart script must be run with --enable-asserts.']);
96 }
97
98 printProgress('Release branch validation');
99 await verifyReleaseBranchState(flutterRoot);
100
101 printProgress('TargetPlatform tool/framework consistency');
102 await verifyTargetPlatform(flutterRoot);
103
104 printProgress('All tool test files end in _test.dart...');
105 await verifyToolTestsEndInTestDart(flutterRoot);
106
107 printProgress('No sync*/async*');
108 await verifyNoSyncAsyncStar(flutterPackages);
109 await verifyNoSyncAsyncStar(flutterExamples, minimumMatches: 200);
110
111 printProgress('No runtimeType in toString...');
112 await verifyNoRuntimeTypeInToString(flutterRoot);
113
114 printProgress('Debug mode instead of checked mode...');
115 await verifyNoCheckedMode(flutterRoot);
116
117 printProgress('Links for creating GitHub issues...');
118 await verifyIssueLinks(flutterRoot);
119
120 printProgress('Links to repositories...');
121 await verifyRepositoryLinks(flutterRoot);
122
123 printProgress('Unexpected binaries...');
124 await verifyNoBinaries(flutterRoot);
125
126 printProgress('Trailing spaces...');
127 await verifyNoTrailingSpaces(
128 flutterRoot,
129 ); // assumes no unexpected binaries, so should be after verifyNoBinaries
130
131 printProgress('Spaces after flow control statements...');
132 await verifySpacesAfterFlowControlStatements(flutterRoot);
133
134 printProgress('Deprecations...');
135 await verifyDeprecations(flutterRoot);
136
137 printProgress('Goldens...');
138 await verifyGoldenTags(flutterPackages);
139
140 printProgress('Skip test comments...');
141 await verifySkipTestComments(flutterRoot);
142
143 printProgress('Licenses...');
144 await verifyNoMissingLicense(flutterRoot);
145
146 printProgress('Test imports...');
147 await verifyNoTestImports(flutterRoot);
148
149 printProgress('Bad imports (framework)...');
150 await verifyNoBadImportsInFlutter(flutterRoot);
151
152 printProgress('Bad imports (tools)...');
153 await verifyNoBadImportsInFlutterTools(flutterRoot);
154
155 printProgress('Internationalization...');
156 await verifyInternationalizations(flutterRoot, dart);
157
158 printProgress('Localization files of stocks app...');
159 await verifyStockAppLocalizations(flutterRoot);
160
161 printProgress('Integration test timeouts...');
162 await verifyIntegrationTestTimeouts(flutterRoot);
163
164 printProgress('null initialized debug fields...');
165 await verifyNullInitializedDebugExpensiveFields(flutterRoot);
166
167 printProgress('Taboo words...');
168 await verifyTabooDocumentation(flutterRoot);
169
170 printProgress('Lint Kotlin files...');
171 await lintKotlinFiles(flutterRoot);
172
173 printProgress('Lint generated Kotlin files from templates...');
174 await lintKotlinTemplatedFiles(flutterRoot);
175
176 // Ensure that all package dependencies are in sync.
177 printProgress('Package dependencies...');
178 await runCommand(flutter, <String>['update-packages'], workingDirectory: flutterRoot);
179
180 /// Ensure that no new dependencies have been accidentally
181 /// added to core packages.
182 printProgress('Package Allowlist...');
183 await _checkConsumerDependencies();
184
185 // Analyze all the Dart code in the repo.
186 printProgress('Dart analysis...');
187 final CommandResult dartAnalyzeResult = await _runFlutterAnalyze(
188 flutterRoot,
189 options: <String>['--flutter-repo', ...arguments],
190 );
191
192 printProgress('Check formatting of Dart files...');
193 await runCommand(dart, <String>[
194 '--enable-asserts',
195 path.join(flutterRoot, 'dev', 'tools', 'bin', 'format.dart'),
196 ], workingDirectory: flutterRoot);
197
198 if (dartAnalyzeResult.exitCode == 0) {
199 // Only run the private lints when the code is free of type errors. The
200 // lints are easier to write when they can assume, for example, there is no
201 // inheritance cycles.
202 final List<AnalyzeRule> rules = <AnalyzeRule>[
203 noDoubleClamp,
204 noStopwatches,
205 renderBoxIntrinsicCalculation,
206 protectPublicStateSubtypes,
207 ];
208 final String ruleNames = rules.map((AnalyzeRule rule) => '\n * $rule').join();
209 printProgress('Analyzing code in the framework with the following rules:$ruleNames');
210 await analyzeWithRules(
211 flutterRoot,
212 rules,
213 includePaths: const <String>['packages/flutter/lib'],
214 excludePaths: const <String>['packages/flutter/lib/fix_data'],
215 );
216 final List<AnalyzeRule> testRules = <AnalyzeRule>[noStopwatches];
217 final String testRuleNames = testRules.map((AnalyzeRule rule) => '\n * $rule').join();
218 printProgress('Analyzing code in the test folder with the following rules:$testRuleNames');
219 await analyzeWithRules(flutterRoot, testRules, includePaths: <String>['packages/flutter/test']);
220 final List<AnalyzeRule> toolRules = <AnalyzeRule>[AvoidFutureCatchError()];
221 final String toolRuleNames = toolRules.map((AnalyzeRule rule) => '\n * $rule').join();
222 printProgress('Analyzing code in the tool with the following rules:$toolRuleNames');
223 await analyzeWithRules(
224 flutterRoot,
225 toolRules,
226 includePaths: const <String>['packages/flutter_tools/lib', 'packages/flutter_tools/test'],
227 );
228 } else {
229 printProgress(
230 'Skipped performing further analysis in the framework because "flutter analyze" finished with a non-zero exit code.',
231 );
232 }
233
234 printProgress('Executable allowlist...');
235 await _checkForNewExecutables();
236
237 // Try with the --watch analyzer, to make sure it returns success also.
238 // The --benchmark argument exits after one run.
239 // We specify a failureMessage so that the actual output is muted in the case where _runFlutterAnalyze above already failed.
240 printProgress('Dart analysis (with --watch)...');
241 await _runFlutterAnalyze(
242 flutterRoot,
243 failureMessage: 'Dart analyzer failed when --watch was used.',
244 options: <String>['--flutter-repo', '--watch', '--benchmark', ...arguments],
245 );
246
247 // Analyze the code in `{@tool snippet}` sections in the repo.
248 printProgress('Snippet code...');
249 await runCommand(dart, <String>[
250 '--enable-asserts',
251 path.join(flutterRoot, 'dev', 'bots', 'analyze_snippet_code.dart'),
252 '--verbose',
253 ], workingDirectory: flutterRoot);
254
255 // Make sure that all of the existing samples are linked from at least one API doc comment.
256 printProgress('Code sample link validation...');
257 await runCommand(dart, <String>[
258 '--enable-asserts',
259 path.join(flutterRoot, 'dev', 'bots', 'check_code_samples.dart'),
260 ], workingDirectory: flutterRoot);
261
262 // Try analysis against a big version of the gallery; generate into a temporary directory.
263 printProgress('Dart analysis (mega gallery)...');
264 final Directory outDir = Directory.systemTemp.createTempSync('flutter_mega_gallery.');
265 try {
266 await runCommand(dart, <String>[
267 path.join(flutterRoot, 'dev', 'tools', 'mega_gallery.dart'),
268 '--out',
269 outDir.path,
270 ], workingDirectory: flutterRoot);
271 await _runFlutterAnalyze(
272 outDir.path,
273 failureMessage: 'Dart analyzer failed on mega_gallery benchmark.',
274 options: <String>['--watch', '--benchmark', ...arguments],
275 );
276 } finally {
277 outDir.deleteSync(recursive: true);
278 }
279
280 // Ensure gen_default links the correct files
281 printProgress('Correct file names in gen_defaults.dart...');
282 await verifyTokenTemplatesUpdateCorrectFiles(flutterRoot);
283
284 // Ensure material library files are up-to-date with the token template files.
285 printProgress('Material library files are up-to-date with token template files...');
286 await verifyMaterialFilesAreUpToDateWithTemplateFiles(flutterRoot, dart);
287
288 // Ensure integration test files are up-to-date with the app template.
289 printProgress('Up to date integration test template files...');
290 await verifyIntegrationTestTemplateFiles(flutterRoot);
291}
292
293// TESTS
294
295FeatureSet _parsingFeatureSet() => FeatureSet.latestLanguageVersion();
296
297_Line _getLine(ParseStringResult parseResult, int offset) {
298 final int lineNumber = parseResult.lineInfo.getLocation(offset).lineNumber;
299 final String content = parseResult.content.substring(
300 parseResult.lineInfo.getOffsetOfLine(lineNumber - 1),
301 parseResult.lineInfo.getOffsetOfLine(lineNumber) - 1,
302 );
303 return _Line(lineNumber, content);
304}
305
306Future<void> verifyReleaseBranchState(String workringDirerctory) async {
307 final ProcessResult result = await Process.run(dart, <String>[
308 'bin/check_engine_version.dart',
309 ], workingDirectory: path.join(workringDirerctory, 'dev', 'tools'));
310 if (result.exitCode != 0) {
311 foundError(<String>['${result.stderr}']);
312 }
313}
314
315Future<void> verifyTargetPlatform(String workingDirectory) async {
316 final File framework = File(
317 '$workingDirectory/packages/flutter/lib/src/foundation/platform.dart',
318 );
319 final Set<String> frameworkPlatforms = <String>{};
320 List<String> lines = framework.readAsLinesSync();
321 int index = 0;
322 while (true) {
323 if (index >= lines.length) {
324 foundError(<String>['${framework.path}: Can no longer find TargetPlatform enum.']);
325 return;
326 }
327 if (lines[index].startsWith('enum TargetPlatform {')) {
328 index += 1;
329 break;
330 }
331 index += 1;
332 }
333 while (true) {
334 if (index >= lines.length) {
335 foundError(<String>['${framework.path}: Could not find end of TargetPlatform enum.']);
336 return;
337 }
338 String line = lines[index].trim();
339 final int comment = line.indexOf('//');
340 if (comment >= 0) {
341 line = line.substring(0, comment);
342 }
343 if (line == '}') {
344 break;
345 }
346 if (line.isNotEmpty) {
347 if (line.endsWith(',')) {
348 frameworkPlatforms.add(line.substring(0, line.length - 1));
349 } else {
350 foundError(<String>[
351 '${framework.path}:$index: unparseable line when looking for TargetPlatform values',
352 ]);
353 }
354 }
355 index += 1;
356 }
357 final File tool = File('$workingDirectory/packages/flutter_tools/lib/src/resident_runner.dart');
358 final Set<String> toolPlatforms = <String>{};
359 lines = tool.readAsLinesSync();
360 index = 0;
361 while (true) {
362 if (index >= lines.length) {
363 foundError(<String>['${tool.path}: Can no longer find nextPlatform logic.']);
364 return;
365 }
366 if (lines[index].trim().startsWith('const platforms = <String>[')) {
367 index += 1;
368 break;
369 }
370 index += 1;
371 }
372 while (true) {
373 if (index >= lines.length) {
374 foundError(<String>['${tool.path}: Could not find end of nextPlatform logic.']);
375 return;
376 }
377 final String line = lines[index].trim();
378 if (line.startsWith("'") && line.endsWith("',")) {
379 toolPlatforms.add(line.substring(1, line.length - 2));
380 } else if (line == '];') {
381 break;
382 } else {
383 foundError(<String>[
384 '${tool.path}:$index: unparseable line when looking for nextPlatform values',
385 ]);
386 }
387 index += 1;
388 }
389 final Set<String> frameworkExtra = frameworkPlatforms.difference(toolPlatforms);
390 if (frameworkExtra.isNotEmpty) {
391 foundError(<String>[
392 'TargetPlatform has some extra values not found in the tool: ${frameworkExtra.join(", ")}',
393 ]);
394 }
395 final Set<String> toolExtra = toolPlatforms.difference(frameworkPlatforms);
396 if (toolExtra.isNotEmpty) {
397 foundError(<String>[
398 'The nextPlatform logic in the tool has some extra values not found in TargetPlatform: ${toolExtra.join(", ")}',
399 ]);
400 }
401}
402
403/// Verify Token Templates are mapped to correct file names while generating
404/// M3 defaults in /dev/tools/gen_defaults/bin/gen_defaults.dart.
405Future<void> verifyTokenTemplatesUpdateCorrectFiles(String workingDirectory) async {
406 final List<String> errors = <String>[];
407
408 String getMaterialDirPath(List<String> lines) {
409 final String line = lines.firstWhere((String line) => line.contains('String materialLib'));
410 final String relativePath = line.substring(line.indexOf("'") + 1, line.lastIndexOf("'"));
411 return path.join(workingDirectory, relativePath);
412 }
413
414 String getFileName(String line) {
415 const String materialLibString = r"'$materialLib/";
416 final String leftClamp = line.substring(
417 line.indexOf(materialLibString) + materialLibString.length,
418 );
419 return leftClamp.substring(0, leftClamp.indexOf("'"));
420 }
421
422 final String genDefaultsBinDir = '$workingDirectory/dev/tools/gen_defaults/bin';
423 final File file = File(path.join(genDefaultsBinDir, 'gen_defaults.dart'));
424 final List<String> lines = file.readAsLinesSync();
425 final String materialDirPath = getMaterialDirPath(lines);
426 bool atLeastOneTargetLineExists = false;
427
428 for (final String line in lines) {
429 if (line.contains('updateFile();')) {
430 atLeastOneTargetLineExists = true;
431 final String fileName = getFileName(line);
432 final String filePath = path.join(materialDirPath, fileName);
433 final File file = File(filePath);
434
435 if (!file.existsSync()) {
436 errors.add('file $filePath does not exist.');
437 }
438 }
439 }
440
441 assert(
442 atLeastOneTargetLineExists,
443 'No lines exist that this test expects to '
444 'verify. Check if the target file is correct or remove this test',
445 );
446
447 // Fail if any errors
448 if (errors.isNotEmpty) {
449 final String s = errors.length > 1 ? 's' : '';
450 final String itThem = errors.length > 1 ? 'them' : 'it';
451 foundError(<String>[
452 ...errors,
453 '${bold}Please correct the file name$s or remove $itThem from /dev/tools/gen_defaults/bin/gen_defaults.dart$reset',
454 ]);
455 }
456}
457
458/// Verify Material library files are up-to-date with the token template files
459/// when running /dev/tools/gen_defaults/bin/gen_defaults.dart.
460Future<void> verifyMaterialFilesAreUpToDateWithTemplateFiles(
461 String workingDirectory,
462 String dartExecutable,
463) async {
464 final List<String> errors = <String>[];
465 const String beginGeneratedComment = '// BEGIN GENERATED TOKEN PROPERTIES';
466
467 String getMaterialDirPath(List<String> lines) {
468 final String line = lines.firstWhere((String line) => line.contains('String materialLib'));
469 final String relativePath = line.substring(line.indexOf("'") + 1, line.lastIndexOf("'"));
470 return path.join(workingDirectory, relativePath);
471 }
472
473 String getFileName(String line) {
474 const String materialLibString = r"'$materialLib/";
475 final String leftClamp = line.substring(
476 line.indexOf(materialLibString) + materialLibString.length,
477 );
478 return leftClamp.substring(0, leftClamp.indexOf("'"));
479 }
480
481 // Get the template generated code from the file.
482 List<String> getGeneratedCode(List<String> lines) {
483 return lines.skipWhile((String line) => !line.contains(beginGeneratedComment)).toList();
484 }
485
486 final String genDefaultsBinDir = '$workingDirectory/dev/tools/gen_defaults/bin';
487 final File file = File(path.join(genDefaultsBinDir, 'gen_defaults.dart'));
488 final List<String> lines = file.readAsLinesSync();
489 final String materialDirPath = getMaterialDirPath(lines);
490 final Map<String, List<String>> beforeGeneratedCode = <String, List<String>>{};
491 final Map<String, List<String>> afterGeneratedCode = <String, List<String>>{};
492
493 for (final String line in lines) {
494 if (line.contains('updateFile();')) {
495 final String fileName = getFileName(line);
496 final String filePath = path.join(materialDirPath, fileName);
497 final File file = File(filePath);
498 beforeGeneratedCode[fileName] = getGeneratedCode(file.readAsLinesSync());
499 }
500 }
501
502 // Run gen_defaults.dart to generate the token template files.
503 await runCommand(dartExecutable, <String>[
504 '--enable-asserts',
505 path.join('dev', 'tools', 'gen_defaults', 'bin', 'gen_defaults.dart'),
506 ], workingDirectory: workingDirectory);
507
508 for (final String line in lines) {
509 if (line.contains('updateFile();')) {
510 final String fileName = getFileName(line);
511 final String filePath = path.join(materialDirPath, fileName);
512 final File file = File(filePath);
513 afterGeneratedCode[fileName] = getGeneratedCode(file.readAsLinesSync());
514 }
515 }
516
517 // Compare the generated code before and after running gen_defaults.dart.
518 for (final String fileName in beforeGeneratedCode.keys) {
519 final List<String> before = beforeGeneratedCode[fileName]!;
520 final List<String> after = afterGeneratedCode[fileName]!;
521 if (!const IterableEquality<String>().equals(before, after)) {
522 errors.add('$fileName is not up-to-date with the token template file.');
523 }
524 }
525
526 // Fail if any errors.
527 if (errors.isNotEmpty) {
528 foundError(<String>[
529 ...errors,
530 '${bold}See: https://github.com/flutter/flutter/blob/main/dev/tools/gen_defaults to update the token template files.$reset',
531 ]);
532 }
533}
534
535/// Verify tool test files end in `_test.dart`.
536///
537/// The test runner will only recognize files ending in `_test.dart` as tests to
538/// be run: https://github.com/dart-lang/test/tree/master/pkgs/test#running-tests
539Future<void> verifyToolTestsEndInTestDart(String workingDirectory) async {
540 final String toolsTestPath = path.join(workingDirectory, 'packages', 'flutter_tools', 'test');
541 final List<String> violations = <String>[];
542
543 // detect files that contains calls to test(), testUsingContext(), and testWithoutContext()
544 final RegExp callsTestFunctionPattern = RegExp(
545 r'^ *(test\(.*\)|testUsingContext\(.*\)|testWithoutContext\(.*\))',
546 );
547
548 await for (final File file in _allFiles(toolsTestPath, 'dart', minimumMatches: 300)) {
549 final bool isValidTestFile = file.path.endsWith('_test.dart');
550 if (isValidTestFile) {
551 continue;
552 }
553
554 final bool isTestData = file.path.contains(r'test_data');
555 if (isTestData) {
556 continue;
557 }
558
559 final bool isInTestShard = file.path.contains(r'.shard/');
560 if (!isInTestShard) {
561 continue;
562 }
563
564 final bool callsTestFunction = file.readAsStringSync().contains(callsTestFunctionPattern);
565 if (!callsTestFunction) {
566 continue;
567 }
568
569 violations.add(file.path);
570 }
571 if (violations.isNotEmpty) {
572 foundError(<String>[
573 '${bold}Found flutter_tools tests that do not end in `_test.dart`; these will not be run by the test runner$reset',
574 ...violations,
575 ]);
576 }
577}
578
579Future<void> verifyNoSyncAsyncStar(String workingDirectory, {int minimumMatches = 2000}) async {
580 final RegExp syncPattern = RegExp(r'\s*?a?sync\*\s*?{');
581 final RegExp ignorePattern = RegExp(r'^\s*?// The following uses a?sync\* because:? ');
582 final RegExp commentPattern = RegExp(r'^\s*?//');
583 final List<String> errors = <String>[];
584 await for (final File file in _allFiles(
585 workingDirectory,
586 'dart',
587 minimumMatches: minimumMatches,
588 )) {
589 if (file.path.contains('test')) {
590 continue;
591 }
592 final List<String> lines = file.readAsLinesSync();
593 for (int index = 0; index < lines.length; index += 1) {
594 final String line = lines[index];
595 if (line.startsWith(commentPattern)) {
596 continue;
597 }
598 if (line.contains(syncPattern)) {
599 int lookBehindIndex = index - 1;
600 bool hasExplanation = false;
601 while (lookBehindIndex >= 0 && lines[lookBehindIndex].startsWith(commentPattern)) {
602 if (lines[lookBehindIndex].startsWith(ignorePattern)) {
603 hasExplanation = true;
604 break;
605 }
606 lookBehindIndex -= 1;
607 }
608 if (!hasExplanation) {
609 errors.add('${file.path}:$index: sync*/async* without an explanation.');
610 }
611 }
612 }
613 }
614 if (errors.isNotEmpty) {
615 foundError(<String>[
616 '${bold}Do not use sync*/async* methods. See https://github.com/flutter/flutter/blob/main/docs/contributing/Style-guide-for-Flutter-repo.md#avoid-syncasync for details.$reset',
617 ...errors,
618 ]);
619 }
620}
621
622final RegExp _findGoldenTestPattern = RegExp(r'matchesGoldenFile\(');
623final RegExp _findGoldenDefinitionPattern = RegExp(r'matchesGoldenFile\(Object');
624final RegExp _leadingComment = RegExp(r'//');
625final RegExp _goldenTagPattern1 = RegExp(r'@Tags\(');
626final RegExp _goldenTagPattern2 = RegExp(r"'reduced-test-set'");
627
628/// Only golden file tests in the flutter package are subject to reduced testing,
629/// for example, invocations in flutter_test to validate comparator
630/// functionality do not require tagging.
631const String _ignoreGoldenTag = '// flutter_ignore: golden_tag (see analyze.dart)';
632const String _ignoreGoldenTagForFile = '// flutter_ignore_for_file: golden_tag (see analyze.dart)';
633
634Future<void> verifyGoldenTags(String workingDirectory, {int minimumMatches = 2000}) async {
635 final List<String> errors = <String>[];
636 await for (final File file in _allFiles(
637 workingDirectory,
638 'dart',
639 minimumMatches: minimumMatches,
640 )) {
641 bool needsTag = false;
642 bool hasTagNotation = false;
643 bool hasReducedTag = false;
644 bool ignoreForFile = false;
645 final List<String> lines = file.readAsLinesSync();
646 for (final String line in lines) {
647 if (line.contains(_goldenTagPattern1)) {
648 hasTagNotation = true;
649 }
650 if (line.contains(_goldenTagPattern2)) {
651 hasReducedTag = true;
652 }
653 if (line.contains(_findGoldenTestPattern) &&
654 !line.contains(_findGoldenDefinitionPattern) &&
655 !line.contains(_leadingComment) &&
656 !line.contains(_ignoreGoldenTag)) {
657 needsTag = true;
658 }
659 if (line.contains(_ignoreGoldenTagForFile)) {
660 ignoreForFile = true;
661 }
662 // If the file is being ignored or a reduced test tag is already accounted
663 // for, skip parsing the rest of the lines for golden file tests.
664 if (ignoreForFile || (hasTagNotation && hasReducedTag)) {
665 break;
666 }
667 }
668 // If a reduced test tag is already accounted for, move on to the next file.
669 if (ignoreForFile || (hasTagNotation && hasReducedTag)) {
670 continue;
671 }
672 // If there are golden file tests, ensure they are tagged for all reduced
673 // test environments.
674 if (needsTag) {
675 if (!hasTagNotation) {
676 errors.add(
677 '${file.path}: Files containing golden tests must be tagged using '
678 "@Tags(<String>['reduced-test-set']) at the top of the file before import statements.",
679 );
680 } else if (!hasReducedTag) {
681 errors.add(
682 '${file.path}: Files containing golden tests must be tagged with '
683 "'reduced-test-set'.",
684 );
685 }
686 }
687 }
688 if (errors.isNotEmpty) {
689 foundError(<String>[
690 ...errors,
691 '${bold}See: https://github.com/flutter/flutter/blob/main/docs/contributing/testing/Writing-a-golden-file-test-for-package-flutter.md$reset',
692 ]);
693 }
694}
695
696class _DeprecationMessagesVisitor extends RecursiveAstVisitor<void> {
697 _DeprecationMessagesVisitor(this.parseResult, this.filePath);
698
699 final ParseStringResult parseResult;
700 final String filePath;
701 final List<String> errors = <String>[];
702
703 /// Some deprecation notices are special, for example they're used to annotate members that
704 /// will never go away and were never allowed but which we are trying to show messages for.
705 /// (One example would be a library that intentionally conflicts with a member in another
706 /// library to indicate that it is incompatible with that other library. Another would be
707 /// the regexp just above...)
708 static const Pattern ignoreDeprecration =
709 '// flutter_ignore: deprecation_syntax (see analyze.dart)';
710
711 /// Some deprecation notices are exempt for historical reasons. They must have an issue listed.
712 static final RegExp legacyDeprecation = RegExp(
713 r'// flutter_ignore: deprecation_syntax, https://github.com/flutter/flutter/issues/\d+',
714 );
715
716 static final RegExp deprecationVersionPattern = RegExp(
717 r'This feature was deprecated after v(?<major>\d+)\.(?<minor>\d+)\.(?<patch>\d+)(?<build>-\d+\.\d+\.pre)?\.$',
718 );
719
720 void _addErrorWithLineInfo(AstNode node, {required String error}) {
721 final int lineNumber = parseResult.lineInfo.getLocation(node.offset).lineNumber;
722 errors.add('$filePath:$lineNumber: $error');
723 }
724
725 @override
726 void visitAnnotation(Annotation node) {
727 super.visitAnnotation(node);
728 final bool shouldCheckAnnotation =
729 node.name.name == 'Deprecated' &&
730 !hasInlineIgnore(node, parseResult, ignoreDeprecration) &&
731 !hasInlineIgnore(node, parseResult, legacyDeprecation);
732 if (!shouldCheckAnnotation) {
733 return;
734 }
735 final NodeList<Expression>? arguments = node.arguments?.arguments;
736 if (arguments == null || arguments.length != 1) {
737 _addErrorWithLineInfo(
738 node,
739 error: 'A @Deprecation annotation must have exactly one deprecation notice String.',
740 );
741 return;
742 }
743 final Expression deprecationNotice = arguments.first;
744 if (deprecationNotice is! AdjacentStrings) {
745 _addErrorWithLineInfo(node, error: 'Deprecation notice must be an adjacent string.');
746 return;
747 }
748 final List<StringLiteral> strings = deprecationNotice.strings;
749 final Iterator<StringLiteral> deprecationMessageIterator = strings.iterator;
750 final bool isNotEmpty = deprecationMessageIterator.moveNext();
751 assert(isNotEmpty); // An AdjacentString always has 2 or more string literals.
752
753 final [...List<StringLiteral> messageLiterals, StringLiteral versionLiteral] = strings;
754
755 // Verify the version literal has the correct pattern.
756 final RegExpMatch? versionMatch = versionLiteral is SimpleStringLiteral
757 ? deprecationVersionPattern.firstMatch(versionLiteral.value)
758 : null;
759 if (versionMatch == null) {
760 _addErrorWithLineInfo(
761 versionLiteral,
762 error:
763 'Deprecation notice must end with a line saying "This feature was deprecated after...".',
764 );
765 return;
766 }
767
768 final int major = int.parse(versionMatch.namedGroup('major')!);
769 final int minor = int.parse(versionMatch.namedGroup('minor')!);
770 final int patch = int.parse(versionMatch.namedGroup('patch')!);
771 final bool hasBuild = versionMatch.namedGroup('build') != null;
772 // There was a beta release that was mistakenly labeled 3.1.0 without a build.
773 final bool specialBeta = major == 3 && minor == 1 && patch == 0;
774 if (!specialBeta && (major > 1 || (major == 1 && minor >= 20))) {
775 if (!hasBuild) {
776 _addErrorWithLineInfo(
777 versionLiteral,
778 error:
779 'Deprecation notice does not accurately indicate a beta branch version number; please see https://flutter.dev/docs/development/tools/sdk/releases to find the latest beta build version number.',
780 );
781 return;
782 }
783 }
784
785 // Verify the version literal has the correct pattern.
786 assert(messageLiterals.isNotEmpty); // An AdjacentString always has 2 or more string literals.
787 for (final StringLiteral message in messageLiterals) {
788 if (message is! SingleStringLiteral) {
789 _addErrorWithLineInfo(
790 message,
791 error: 'Deprecation notice does not match required pattern.',
792 );
793 return;
794 }
795 if (!message.isSingleQuoted) {
796 _addErrorWithLineInfo(
797 message,
798 error:
799 'Deprecation notice does not match required pattern. You might have used double quotes (") for the string instead of single quotes (\').',
800 );
801 return;
802 }
803 }
804 final String fullExplanation = messageLiterals
805 .map((StringLiteral message) => message.stringValue ?? '')
806 .join()
807 .trimRight();
808 if (fullExplanation.isEmpty) {
809 _addErrorWithLineInfo(
810 messageLiterals.last,
811 error:
812 'Deprecation notice should be a grammatically correct sentence and end with a period; There might not be an explanatory message.',
813 );
814 return;
815 }
816 final String firstChar = String.fromCharCode(fullExplanation.runes.first);
817 if (firstChar.toUpperCase() != firstChar) {
818 _addErrorWithLineInfo(
819 messageLiterals.first,
820 error:
821 'Deprecation notice should be a grammatically correct sentence and start with a capital letter; see style guide: https://github.com/flutter/flutter/blob/main/docs/contributing/Style-guide-for-Flutter-repo.md',
822 );
823 return;
824 }
825 if (!fullExplanation.endsWith('.') &&
826 !fullExplanation.endsWith('?') &&
827 !fullExplanation.endsWith('!')) {
828 _addErrorWithLineInfo(
829 messageLiterals.last,
830 error:
831 'Deprecation notice should be a grammatically correct sentence and end with a period; notice appears to be "$fullExplanation".',
832 );
833 return;
834 }
835 }
836}
837
838Future<void> verifyDeprecations(String workingDirectory, {int minimumMatches = 2000}) async {
839 final List<String> errors = <String>[];
840 await for (final File file in _allFiles(
841 workingDirectory,
842 'dart',
843 minimumMatches: minimumMatches,
844 )) {
845 final ParseStringResult parseResult = parseFile(
846 featureSet: _parsingFeatureSet(),
847 path: file.absolute.path,
848 );
849 final _DeprecationMessagesVisitor visitor = _DeprecationMessagesVisitor(parseResult, file.path);
850 visitor.visitCompilationUnit(parseResult.unit);
851 errors.addAll(visitor.errors);
852 }
853 // Fail if any errors
854 if (errors.isNotEmpty) {
855 foundError(<String>[
856 ...errors,
857 '${bold}See: https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md#handling-breaking-changes$reset',
858 ]);
859 }
860}
861
862String _generateLicense(String prefix) {
863 return '${prefix}Copyright 2014 The Flutter Authors. All rights reserved.\n'
864 '${prefix}Use of this source code is governed by a BSD-style license that can be\n'
865 '${prefix}found in the LICENSE file.';
866}
867
868Future<void> verifyNoMissingLicense(String workingDirectory, {bool checkMinimums = true}) async {
869 final int? overrideMinimumMatches = checkMinimums ? null : 0;
870 await _verifyNoMissingLicenseForExtension(
871 workingDirectory,
872 'dart',
873 overrideMinimumMatches ?? 2000,
874 _generateLicense('// '),
875 );
876 await _verifyNoMissingLicenseForExtension(
877 workingDirectory,
878 'java',
879 overrideMinimumMatches ?? 1,
880 _generateLicense('// '),
881 );
882 await _verifyNoMissingLicenseForExtension(
883 workingDirectory,
884 'h',
885 overrideMinimumMatches ?? 30,
886 _generateLicense('// '),
887 );
888 await _verifyNoMissingLicenseForExtension(
889 workingDirectory,
890 'm',
891 overrideMinimumMatches ?? 30,
892 _generateLicense('// '),
893 );
894 await _verifyNoMissingLicenseForExtension(
895 workingDirectory,
896 'cc',
897 overrideMinimumMatches ?? 10,
898 _generateLicense('// '),
899 );
900 await _verifyNoMissingLicenseForExtension(
901 workingDirectory,
902 'cpp',
903 overrideMinimumMatches ?? 0,
904 _generateLicense('// '),
905 );
906 await _verifyNoMissingLicenseForExtension(
907 workingDirectory,
908 'swift',
909 overrideMinimumMatches ?? 10,
910 _generateLicense('// '),
911 );
912 await _verifyNoMissingLicenseForExtension(
913 workingDirectory,
914 'gradle',
915 overrideMinimumMatches ?? 80,
916 _generateLicense('// '),
917 );
918 await _verifyNoMissingLicenseForExtension(
919 workingDirectory,
920 'gn',
921 overrideMinimumMatches ?? 0,
922 _generateLicense('# '),
923 );
924 await _verifyNoMissingLicenseForExtension(
925 workingDirectory,
926 'sh',
927 overrideMinimumMatches ?? 1,
928 _generateLicense('# '),
929 header: r'#!/usr/bin/env bash\n',
930 );
931 await _verifyNoMissingLicenseForExtension(
932 workingDirectory,
933 'bat',
934 overrideMinimumMatches ?? 1,
935 _generateLicense('REM '),
936 header: r'@ECHO off\n',
937 );
938 await _verifyNoMissingLicenseForExtension(
939 workingDirectory,
940 'ps1',
941 overrideMinimumMatches ?? 1,
942 _generateLicense('# '),
943 );
944 await _verifyNoMissingLicenseForExtension(
945 workingDirectory,
946 'html',
947 overrideMinimumMatches ?? 1,
948 '<!-- ${_generateLicense('')} -->',
949 trailingBlank: false,
950 header: r'<!DOCTYPE HTML>\n',
951 );
952 await _verifyNoMissingLicenseForExtension(
953 workingDirectory,
954 'xml',
955 overrideMinimumMatches ?? 1,
956 '<!-- ${_generateLicense('')} -->',
957 header: r'(<\?xml version="1.0" encoding="utf-8"\?>\n)?',
958 );
959 await _verifyNoMissingLicenseForExtension(
960 workingDirectory,
961 'frag',
962 overrideMinimumMatches ?? 1,
963 _generateLicense('// '),
964 header: r'#version 320 es(\n)+',
965 );
966}
967
968Future<void> _verifyNoMissingLicenseForExtension(
969 String workingDirectory,
970 String extension,
971 int minimumMatches,
972 String license, {
973 bool trailingBlank = true,
974 // The "header" is a regular expression matching the header that comes before
975 // the license in some files.
976 String header = '',
977}) async {
978 assert(!license.endsWith('\n'));
979 final String licensePattern = RegExp.escape('$license\n${trailingBlank ? '\n' : ''}');
980 final List<String> errors = <String>[];
981 await for (final File file in _allFiles(
982 workingDirectory,
983 extension,
984 minimumMatches: minimumMatches,
985 )) {
986 final String contents = file.readAsStringSync().replaceAll('\r\n', '\n');
987 if (contents.isEmpty) {
988 continue; // let's not go down the /bin/true rabbit hole
989 }
990 if (path.basename(file.path) == 'Package.swift') {
991 continue;
992 }
993 if (!contents.startsWith(RegExp(header + licensePattern))) {
994 errors.add(file.path);
995 }
996 }
997 // Fail if any errors
998 if (errors.isNotEmpty) {
999 final String fileDoes = errors.length == 1 ? 'file does' : '${errors.length} files do';
1000 foundError(<String>[
1001 '${bold}The following $fileDoes not have the right license header for $extension files:$reset',
1002 ...errors.map<String>((String error) => ' $error'),
1003 'The expected license header is:',
1004 if (header.isNotEmpty) 'A header matching the regular expression "$header",',
1005 if (header.isNotEmpty) 'followed by the following license text:',
1006 license,
1007 if (trailingBlank) '...followed by a blank line.',
1008 ]);
1009 }
1010}
1011
1012class _Line {
1013 _Line(this.line, this.content);
1014
1015 final int line;
1016 final String content;
1017}
1018
1019Iterable<_Line> _getTestSkips(File file) {
1020 final ParseStringResult parseResult = parseFile(
1021 featureSet: _parsingFeatureSet(),
1022 path: file.absolute.path,
1023 );
1024 final _TestSkipLinesVisitor<CompilationUnit> visitor = _TestSkipLinesVisitor<CompilationUnit>(
1025 parseResult,
1026 );
1027 visitor.visitCompilationUnit(parseResult.unit);
1028 return visitor.skips;
1029}
1030
1031class _TestSkipLinesVisitor<T> extends RecursiveAstVisitor<T> {
1032 _TestSkipLinesVisitor(this.parseResult) : skips = <_Line>{};
1033
1034 final ParseStringResult parseResult;
1035 final Set<_Line> skips;
1036
1037 static bool isTestMethod(String name) {
1038 return name.startsWith('test') || name == 'group' || name == 'expect';
1039 }
1040
1041 static final Pattern _skipTestIntentionalPattern = RegExp(r'// .*[intended]');
1042 static final Pattern _skipTestTrackingBugPattern = RegExp(
1043 r'// .*https+?://github.com/.*/issues/\d+',
1044 );
1045 bool _hasValidJustificationComment(Label skipLabel) {
1046 return hasInlineIgnore(skipLabel, parseResult, _skipTestIntentionalPattern) ||
1047 hasInlineIgnore(skipLabel, parseResult, _skipTestTrackingBugPattern);
1048 }
1049
1050 @override
1051 T? visitMethodInvocation(MethodInvocation node) {
1052 if (isTestMethod(node.methodName.toString())) {
1053 for (final Expression argument in node.argumentList.arguments) {
1054 if (argument is NamedExpression &&
1055 argument.name.label.name == 'skip' &&
1056 !_hasValidJustificationComment(argument.name)) {
1057 skips.add(_getLine(parseResult, argument.beginToken.charOffset));
1058 }
1059 }
1060 }
1061 return super.visitMethodInvocation(node);
1062 }
1063}
1064
1065Future<void> verifySkipTestComments(String workingDirectory) async {
1066 final List<String> errors = <String>[];
1067 final Stream<File> testFiles = _allFiles(
1068 workingDirectory,
1069 'dart',
1070 minimumMatches: 1500,
1071 ).where((File f) => f.path.endsWith('_test.dart'));
1072
1073 await for (final File file in testFiles) {
1074 for (final _Line skip in _getTestSkips(file)) {
1075 errors.add('${file.path}:${skip.line}: skip test without a justification comment.');
1076 }
1077 }
1078
1079 // Fail if any errors
1080 if (errors.isNotEmpty) {
1081 foundError(<String>[
1082 ...errors,
1083 '\n${bold}See: https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md#skipped-tests$reset',
1084 ]);
1085 }
1086}
1087
1088final RegExp _testImportPattern = RegExp(r'''import (['"])([^'"]+_test\.dart)\1''');
1089const Set<String> _exemptTestImports = <String>{
1090 'package:flutter_test/flutter_test.dart',
1091 'hit_test.dart',
1092 'package:test_api/src/backend/live_test.dart',
1093 'package:integration_test/integration_test.dart',
1094};
1095
1096Future<void> verifyNoTestImports(String workingDirectory) async {
1097 final List<String> errors = <String>[];
1098 assert("// foo\nimport 'binding_test.dart' as binding;\n'".contains(_testImportPattern));
1099 final List<File> dartFiles = await _allFiles(
1100 path.join(workingDirectory, 'packages'),
1101 'dart',
1102 minimumMatches: 1500,
1103 ).toList();
1104 for (final File file in dartFiles) {
1105 for (final String line in file.readAsLinesSync()) {
1106 final Match? match = _testImportPattern.firstMatch(line);
1107 if (match != null && !_exemptTestImports.contains(match.group(2))) {
1108 errors.add(file.path);
1109 }
1110 }
1111 }
1112 // Fail if any errors
1113 if (errors.isNotEmpty) {
1114 foundError(<String>[
1115 '${bold}The following file(s) import a test directly. Test utilities should be in their own file.$reset',
1116 ...errors,
1117 ]);
1118 }
1119}
1120
1121Future<void> verifyNoBadImportsInFlutter(String workingDirectory) async {
1122 final List<String> errors = <String>[];
1123 final String libPath = path.join(workingDirectory, 'packages', 'flutter', 'lib');
1124 final String srcPath = path.join(workingDirectory, 'packages', 'flutter', 'lib', 'src');
1125 // Verify there's one libPath/*.dart for each srcPath/*/.
1126 final List<String> packages =
1127 Directory(libPath)
1128 .listSync()
1129 .where(
1130 (FileSystemEntity entity) => entity is File && path.extension(entity.path) == '.dart',
1131 )
1132 .map<String>((FileSystemEntity entity) => path.basenameWithoutExtension(entity.path))
1133 .toList()
1134 ..sort();
1135 final List<String> directories =
1136 Directory(srcPath)
1137 .listSync()
1138 .whereType<Directory>()
1139 .map<String>((Directory entity) => path.basename(entity.path))
1140 .toList()
1141 ..sort();
1142 if (!_listEquals<String>(packages, directories)) {
1143 errors.add(
1144 <String>[
1145 'flutter/lib/*.dart does not match flutter/lib/src/*/:',
1146 'These are the exported packages:',
1147 ...packages.map<String>((String path) => ' lib/$path.dart'),
1148 'These are the directories:',
1149 ...directories.map<String>((String path) => ' lib/src/$path/'),
1150 ].join('\n'),
1151 );
1152 }
1153 // Verify that the imports are well-ordered.
1154 final Map<String, Set<String>> dependencyMap = <String, Set<String>>{};
1155 for (final String directory in directories) {
1156 dependencyMap[directory] = await _findFlutterDependencies(
1157 path.join(srcPath, directory),
1158 errors,
1159 checkForMeta: directory != 'foundation',
1160 );
1161 }
1162 assert(
1163 dependencyMap['material']!.contains('widgets') &&
1164 dependencyMap['widgets']!.contains('rendering') &&
1165 dependencyMap['rendering']!.contains('painting'),
1166 ); // to make sure we're convinced _findFlutterDependencies is finding some
1167 for (final String package in dependencyMap.keys) {
1168 if (dependencyMap[package]!.contains(package)) {
1169 errors.add(
1170 'One of the files in the $yellow$package$reset package imports that package recursively.',
1171 );
1172 }
1173 }
1174
1175 for (final String key in dependencyMap.keys) {
1176 for (final String dependency in dependencyMap[key]!) {
1177 if (dependencyMap[dependency] != null) {
1178 continue;
1179 }
1180 // Sanity check before performing _deepSearch, to ensure there's no rogue
1181 // dependencies.
1182 final String validFilenames = dependencyMap.keys
1183 .map((String name) => '$name.dart')
1184 .join(', ');
1185 errors.add(
1186 '$key imported package:flutter/$dependency.dart '
1187 'which is not one of the valid exports { $validFilenames }.\n'
1188 'Consider changing $dependency.dart to one of them.',
1189 );
1190 }
1191 }
1192
1193 for (final String package in dependencyMap.keys) {
1194 final List<String>? loop = _deepSearch<String>(dependencyMap, package);
1195 if (loop != null) {
1196 errors.add('${yellow}Dependency loop:$reset ${loop.join(' depends on ')}');
1197 }
1198 }
1199 // Fail if any errors
1200 if (errors.isNotEmpty) {
1201 foundError(<String>[
1202 if (errors.length == 1)
1203 '${bold}An error was detected when looking at import dependencies within the Flutter package:$reset'
1204 else
1205 '${bold}Multiple errors were detected when looking at import dependencies within the Flutter package:$reset',
1206 ...errors,
1207 ]);
1208 }
1209}
1210
1211Future<void> verifyNoBadImportsInFlutterTools(String workingDirectory) async {
1212 final List<String> errors = <String>[];
1213 final List<File> files = await _allFiles(
1214 path.join(workingDirectory, 'packages', 'flutter_tools', 'lib'),
1215 'dart',
1216 minimumMatches: 200,
1217 ).toList();
1218 for (final File file in files) {
1219 if (file.readAsStringSync().contains('package:flutter_tools/')) {
1220 errors.add('$yellow${file.path}$reset imports flutter_tools.');
1221 }
1222 }
1223 // Fail if any errors
1224 if (errors.isNotEmpty) {
1225 foundError(<String>[
1226 if (errors.length == 1)
1227 '${bold}An error was detected when looking at import dependencies within the flutter_tools package:$reset'
1228 else
1229 '${bold}Multiple errors were detected when looking at import dependencies within the flutter_tools package:$reset',
1230 ...errors.map((String paragraph) => '$paragraph\n'),
1231 ]);
1232 }
1233}
1234
1235Future<void> verifyIntegrationTestTimeouts(String workingDirectory) async {
1236 final List<String> errors = <String>[];
1237 final String dev = path.join(workingDirectory, 'dev');
1238 final List<File> files = await _allFiles(dev, 'dart', minimumMatches: 1)
1239 .where(
1240 (File file) =>
1241 file.path.contains('test_driver') &&
1242 (file.path.endsWith('_test.dart') || file.path.endsWith('util.dart')),
1243 )
1244 .toList();
1245 for (final File file in files) {
1246 final String contents = file.readAsStringSync();
1247 final int testCount = ' test('.allMatches(contents).length;
1248 final int timeoutNoneCount = 'timeout: Timeout.none'.allMatches(contents).length;
1249 if (testCount != timeoutNoneCount) {
1250 errors.add(
1251 '$yellow${file.path}$reset has at least $testCount test(s) but only $timeoutNoneCount `Timeout.none`(s).',
1252 );
1253 }
1254 }
1255 if (errors.isNotEmpty) {
1256 foundError(<String>[
1257 if (errors.length == 1)
1258 '${bold}An error was detected when looking at integration test timeouts:$reset'
1259 else
1260 '${bold}Multiple errors were detected when looking at integration test timeouts:$reset',
1261 ...errors.map((String paragraph) => '$paragraph\n'),
1262 ]);
1263 }
1264}
1265
1266Future<void> verifyInternationalizations(String workingDirectory, String dartExecutable) async {
1267 final EvalResult materialGenResult = await _evalCommand(dartExecutable, <String>[
1268 path.join('dev', 'tools', 'localization', 'bin', 'gen_localizations.dart'),
1269 '--material',
1270 '--remove-undefined',
1271 ], workingDirectory: workingDirectory);
1272 final EvalResult cupertinoGenResult = await _evalCommand(dartExecutable, <String>[
1273 path.join('dev', 'tools', 'localization', 'bin', 'gen_localizations.dart'),
1274 '--cupertino',
1275 '--remove-undefined',
1276 ], workingDirectory: workingDirectory);
1277
1278 final String materialLocalizationsFile = path.join(
1279 workingDirectory,
1280 'packages',
1281 'flutter_localizations',
1282 'lib',
1283 'src',
1284 'l10n',
1285 'generated_material_localizations.dart',
1286 );
1287 final String cupertinoLocalizationsFile = path.join(
1288 workingDirectory,
1289 'packages',
1290 'flutter_localizations',
1291 'lib',
1292 'src',
1293 'l10n',
1294 'generated_cupertino_localizations.dart',
1295 );
1296 final String expectedMaterialResult = await File(materialLocalizationsFile).readAsString();
1297 final String expectedCupertinoResult = await File(cupertinoLocalizationsFile).readAsString();
1298
1299 if (materialGenResult.stdout.trim() != expectedMaterialResult.trim()) {
1300 foundError(<String>[
1301 '<<<<<<< $materialLocalizationsFile',
1302 expectedMaterialResult.trim(),
1303 '=======',
1304 materialGenResult.stdout.trim(),
1305 '>>>>>>> gen_localizations',
1306 'The contents of $materialLocalizationsFile are different from that produced by gen_localizations.',
1307 '',
1308 'Did you forget to run gen_localizations.dart after updating a .arb file?',
1309 ]);
1310 }
1311 if (cupertinoGenResult.stdout.trim() != expectedCupertinoResult.trim()) {
1312 foundError(<String>[
1313 '<<<<<<< $cupertinoLocalizationsFile',
1314 expectedCupertinoResult.trim(),
1315 '=======',
1316 cupertinoGenResult.stdout.trim(),
1317 '>>>>>>> gen_localizations',
1318 'The contents of $cupertinoLocalizationsFile are different from that produced by gen_localizations.',
1319 '',
1320 'Did you forget to run gen_localizations.dart after updating a .arb file?',
1321 ]);
1322 }
1323}
1324
1325Future<void> verifyStockAppLocalizations(String workingDirectory) async {
1326 final Directory appRoot = Directory(
1327 path.join(workingDirectory, 'dev', 'benchmarks', 'test_apps', 'stocks'),
1328 );
1329 if (!appRoot.existsSync()) {
1330 foundError(<String>['Stocks app does not exist at expected location: ${appRoot.path}']);
1331 }
1332
1333 // Regenerate the localizations.
1334 final String flutterExecutable = path.join(
1335 workingDirectory,
1336 'bin',
1337 'flutter${Platform.isWindows ? '.bat' : ''}',
1338 );
1339 await _evalCommand(flutterExecutable, const <String>['gen-l10n'], workingDirectory: appRoot.path);
1340 final Directory i10nDirectory = Directory(path.join(appRoot.path, 'lib', 'i18n'));
1341 if (!i10nDirectory.existsSync()) {
1342 foundError(<String>[
1343 'Localization files for stocks app not found at expected location: ${i10nDirectory.path}',
1344 ]);
1345 }
1346
1347 // Check that regeneration did not dirty the tree.
1348 final EvalResult result = await _evalCommand('git', <String>[
1349 'diff',
1350 '--name-only',
1351 '--exit-code',
1352 i10nDirectory.path,
1353 ], workingDirectory: workingDirectory);
1354 if (result.exitCode == 1) {
1355 foundError(<String>[
1356 'The following localization files for the stocks app appear to be out of date:',
1357 ...(const LineSplitter().convert(result.stdout).map((String line) => ' * $line')),
1358 'Run "flutter gen-l10n" in "${path.relative(appRoot.path, from: workingDirectory)}" to regenerate.',
1359 ]);
1360 } else if (result.exitCode != 0) {
1361 foundError(<String>[
1362 'Failed to run "git diff" on localization files of stocks app:',
1363 result.stderr,
1364 ]);
1365 }
1366}
1367
1368/// Verifies that all instances of "checked mode" have been migrated to "debug mode".
1369Future<void> verifyNoCheckedMode(String workingDirectory) async {
1370 final String flutterPackages = path.join(workingDirectory, 'packages');
1371 final List<File> files = await _allFiles(
1372 flutterPackages,
1373 'dart',
1374 minimumMatches: 400,
1375 ).where((File file) => path.extension(file.path) == '.dart').toList();
1376 final List<String> problems = <String>[];
1377 for (final File file in files) {
1378 int lineCount = 0;
1379 for (final String line in file.readAsLinesSync()) {
1380 if (line.toLowerCase().contains('checked mode')) {
1381 problems.add(
1382 '${file.path}:$lineCount uses deprecated "checked mode" instead of "debug mode".',
1383 );
1384 }
1385 lineCount += 1;
1386 }
1387 }
1388 if (problems.isNotEmpty) {
1389 foundError(problems);
1390 }
1391}
1392
1393Future<void> verifyNoRuntimeTypeInToString(String workingDirectory) async {
1394 final String flutterLib = path.join(workingDirectory, 'packages', 'flutter', 'lib');
1395 final Set<String> excludedFiles = <String>{
1396 path.join(flutterLib, 'src', 'foundation', 'object.dart'), // Calls this from within an assert.
1397 };
1398 final List<File> files = await _allFiles(
1399 flutterLib,
1400 'dart',
1401 minimumMatches: 400,
1402 ).where((File file) => !excludedFiles.contains(file.path)).toList();
1403 final RegExp toStringRegExp = RegExp(r'^\s+String\s+to(.+?)?String(.+?)?\(\)\s+(\{|=>)');
1404 final List<String> problems = <String>[];
1405 for (final File file in files) {
1406 final List<String> lines = file.readAsLinesSync();
1407 for (int index = 0; index < lines.length; index++) {
1408 if (toStringRegExp.hasMatch(lines[index])) {
1409 final int sourceLine = index + 1;
1410 bool checkForRuntimeType(String line) {
1411 if (line.contains(r'$runtimeType') || line.contains('runtimeType.toString()')) {
1412 problems.add('${file.path}:$sourceLine}: toString calls runtimeType.toString');
1413 return true;
1414 }
1415 return false;
1416 }
1417
1418 if (checkForRuntimeType(lines[index])) {
1419 continue;
1420 }
1421 if (lines[index].contains('=>')) {
1422 while (!lines[index].contains(';')) {
1423 index++;
1424 assert(index < lines.length, 'Source file $file has unterminated toString method.');
1425 if (checkForRuntimeType(lines[index])) {
1426 break;
1427 }
1428 }
1429 } else {
1430 int openBraceCount =
1431 '{'.allMatches(lines[index]).length - '}'.allMatches(lines[index]).length;
1432 while (!lines[index].contains('}') && openBraceCount > 0) {
1433 index++;
1434 assert(
1435 index < lines.length,
1436 'Source file $file has unbalanced braces in a toString method.',
1437 );
1438 if (checkForRuntimeType(lines[index])) {
1439 break;
1440 }
1441 openBraceCount += '{'.allMatches(lines[index]).length;
1442 openBraceCount -= '}'.allMatches(lines[index]).length;
1443 }
1444 }
1445 }
1446 }
1447 }
1448 if (problems.isNotEmpty) {
1449 foundError(problems);
1450 }
1451}
1452
1453Future<void> verifyNoTrailingSpaces(String workingDirectory, {int minimumMatches = 4000}) async {
1454 final List<File> files = await _allFiles(workingDirectory, null, minimumMatches: minimumMatches)
1455 .where((File file) => path.basename(file.path) != 'serviceaccount.enc')
1456 .where((File file) => path.basename(file.path) != 'Ahem.ttf')
1457 .where((File file) => path.extension(file.path) != '.snapshot')
1458 .where((File file) => path.extension(file.path) != '.png')
1459 .where((File file) => path.extension(file.path) != '.jpg')
1460 .where((File file) => path.extension(file.path) != '.ico')
1461 .where((File file) => path.extension(file.path) != '.jar')
1462 .where((File file) => path.extension(file.path) != '.swp')
1463 .toList();
1464 final List<String> problems = <String>[];
1465 for (final File file in files) {
1466 final List<String> lines = file.readAsLinesSync();
1467 for (int index = 0; index < lines.length; index += 1) {
1468 if (lines[index].endsWith(' ')) {
1469 problems.add('${file.path}:${index + 1}: trailing U+0020 space character');
1470 } else if (lines[index].endsWith('\t')) {
1471 problems.add('${file.path}:${index + 1}: trailing U+0009 tab character');
1472 }
1473 }
1474 if (lines.isNotEmpty && lines.last == '') {
1475 problems.add('${file.path}:${lines.length}: trailing blank line');
1476 }
1477 }
1478 if (problems.isNotEmpty) {
1479 foundError(problems);
1480 }
1481}
1482
1483final RegExp _flowControlStatementWithoutSpace = RegExp(
1484 r'(^|[ \t])(if|switch|for|do|while|catch)\(',
1485 multiLine: true,
1486);
1487
1488Future<void> verifySpacesAfterFlowControlStatements(
1489 String workingDirectory, {
1490 int minimumMatches = 4000,
1491}) async {
1492 const Set<String> extensions = <String>{
1493 // .dart omitted from this list because the Dart auto formatter ensures
1494 // spaces after flow control statements.
1495 '.java',
1496 '.js',
1497 '.kt',
1498 '.swift',
1499 '.c',
1500 '.cc',
1501 '.cpp',
1502 '.h',
1503 '.m',
1504 };
1505 final List<File> files = await _allFiles(
1506 workingDirectory,
1507 null,
1508 minimumMatches: minimumMatches,
1509 ).where((File file) => extensions.contains(path.extension(file.path))).toList();
1510 final List<String> problems = <String>[];
1511 for (final File file in files) {
1512 final List<String> lines = file.readAsLinesSync();
1513 for (int index = 0; index < lines.length; index += 1) {
1514 if (lines[index].contains(_flowControlStatementWithoutSpace)) {
1515 problems.add('${file.path}:${index + 1}: no space after flow control statement');
1516 }
1517 }
1518 }
1519 if (problems.isNotEmpty) {
1520 foundError(problems);
1521 }
1522}
1523
1524String _bullets(String value) => ' * $value';
1525
1526Future<void> verifyIssueLinks(String workingDirectory) async {
1527 const String issueLinkPrefix = 'https://github.com/flutter/flutter/issues/new';
1528 const Set<String> stops = <String>{'\n', ' ', "'", '"', r'\', ')', '>'};
1529 assert(
1530 !stops.contains('.'),
1531 ); // instead of "visit https://foo." say "visit: https://foo", it copy-pastes better
1532 const String kGiveTemplates =
1533 'Prefer to provide a link either to $issueLinkPrefix/choose (the list of issue '
1534 'templates) or to a specific template directly ($issueLinkPrefix?template=...).\n';
1535 final Set<String> templateNames =
1536 Directory(path.join(workingDirectory, '.github', 'ISSUE_TEMPLATE'))
1537 .listSync()
1538 .whereType<File>()
1539 .where(
1540 (File file) =>
1541 path.extension(file.path) == '.md' || path.extension(file.path) == '.yml',
1542 )
1543 .map<String>((File file) => path.basename(file.path))
1544 .toSet();
1545 final String kTemplates =
1546 'The available templates are:\n${templateNames.map(_bullets).join("\n")}';
1547 final List<String> problems = <String>[];
1548 final Set<String> suggestions = <String>{};
1549 final List<File> files = await _gitFiles(workingDirectory);
1550 for (final File file in files) {
1551 if (path.basename(file.path).endsWith('_test.dart') ||
1552 path.basename(file.path) == 'analyze.dart') {
1553 continue; // Skip tests, they're not public-facing.
1554 }
1555 final Uint8List bytes = file.readAsBytesSync();
1556 // We allow invalid UTF-8 here so that binaries don't trip us up.
1557 // There's a separate test in this file that verifies that all text
1558 // files are actually valid UTF-8 (see verifyNoBinaries below).
1559 final String contents = utf8.decode(bytes, allowMalformed: true);
1560 int start = 0;
1561 while ((start = contents.indexOf(issueLinkPrefix, start)) >= 0) {
1562 int end = start + issueLinkPrefix.length;
1563 while (end < contents.length && !stops.contains(contents[end])) {
1564 end += 1;
1565 }
1566 final String url = contents.substring(start, end);
1567 if (url == issueLinkPrefix) {
1568 if (file.path != path.join(workingDirectory, 'dev', 'bots', 'analyze.dart')) {
1569 problems.add('${file.path} contains a direct link to $issueLinkPrefix.');
1570 suggestions.add(kGiveTemplates);
1571 suggestions.add(kTemplates);
1572 }
1573 } else if (url.startsWith('$issueLinkPrefix?')) {
1574 final Uri parsedUrl = Uri.parse(url);
1575 final List<String>? templates = parsedUrl.queryParametersAll['template'];
1576 if (templates == null) {
1577 problems.add('${file.path} contains $url, which has no "template" argument specified.');
1578 suggestions.add(kGiveTemplates);
1579 suggestions.add(kTemplates);
1580 } else if (templates.length != 1) {
1581 problems.add(
1582 '${file.path} contains $url, which has ${templates.length} templates specified.',
1583 );
1584 suggestions.add(kGiveTemplates);
1585 suggestions.add(kTemplates);
1586 } else if (!templateNames.contains(templates.single)) {
1587 problems.add(
1588 '${file.path} contains $url, which specifies a non-existent template ("${templates.single}").',
1589 );
1590 suggestions.add(kTemplates);
1591 } else if (parsedUrl.queryParametersAll.keys.length > 1) {
1592 problems.add(
1593 '${file.path} contains $url, which the analyze.dart script is not sure how to handle.',
1594 );
1595 suggestions.add(
1596 'Update analyze.dart to handle the URLs above, or change them to the expected pattern.',
1597 );
1598 }
1599 } else if (url != '$issueLinkPrefix/choose') {
1600 problems.add(
1601 '${file.path} contains $url, which the analyze.dart script is not sure how to handle.',
1602 );
1603 suggestions.add(
1604 'Update analyze.dart to handle the URLs above, or change them to the expected pattern.',
1605 );
1606 }
1607 start = end;
1608 }
1609 }
1610 assert(problems.isEmpty == suggestions.isEmpty);
1611 if (problems.isNotEmpty) {
1612 foundError(<String>[...problems, ...suggestions]);
1613 }
1614}
1615
1616Future<void> verifyRepositoryLinks(String workingDirectory) async {
1617 const Set<String> stops = <String>{'\n', ' ', "'", '"', r'\', ')', '>'};
1618 assert(
1619 !stops.contains('.'),
1620 ); // instead of "visit https://foo." say "visit: https://foo", it copy-pastes better
1621
1622 // Repos whose default branch is still 'master'
1623 const Set<String> repoExceptions = <String>{
1624 'bdero/flutter-gpu-examples',
1625 'chromium/chromium',
1626 'clojure/clojure',
1627 'dart-lang/test', // TODO(guidezpl): remove when https://github.com/dart-lang/test/issues/2209 is closed
1628 'dart-lang/webdev',
1629 'eseidelGoogle/bezier_perf',
1630 'flutter/devtools', // TODO(guidezpl): remove when https://github.com/flutter/devtools/issues/7551 is closed
1631 'flutter/flutter_gallery_assets', // TODO(guidezpl): remove when subtask in https://github.com/flutter/flutter/issues/121564 is complete
1632 'flutter/flutter-intellij', // TODO(guidezpl): remove when https://github.com/flutter/flutter-intellij/issues/7342 is closed
1633 'flutter/platform_tests', // TODO(guidezpl): remove when subtask in https://github.com/flutter/flutter/issues/121564 is complete
1634 'flutter/web_installers',
1635 'glfw/glfw',
1636 'GoogleCloudPlatform/artifact-registry-maven-tools',
1637 'material-components/material-components-android', // TODO(guidezpl): remove when https://github.com/material-components/material-components-android/issues/4144 is closed
1638 'ninja-build/ninja',
1639 'torvalds/linux',
1640 'tpn/winsdk-10',
1641 };
1642
1643 // See dev/bots/test/analyze-test-input/root/packages/foo/bad_repository_links.dart
1644 // for examples of repository links that are not allowed.
1645 final RegExp pattern = RegExp(
1646 r'^(https:\/\/(?:cs\.opensource\.google|github|raw\.githubusercontent|source\.chromium|([a-z0-9\-]+)\.googlesource)\.)',
1647 );
1648
1649 final List<String> problems = <String>[];
1650 final Set<String> suggestions = <String>{};
1651 final List<File> files = await _allFiles(workingDirectory, null, minimumMatches: 10).toList();
1652 for (final File file in files) {
1653 final Uint8List bytes = file.readAsBytesSync();
1654 // We allow invalid UTF-8 here so that binaries don't trip us up.
1655 // There's a separate test in this file that verifies that all text
1656 // files are actually valid UTF-8 (see verifyNoBinaries below).
1657 final String contents = utf8.decode(bytes, allowMalformed: true);
1658 int start = 0;
1659 while ((start = contents.indexOf('https://', start)) >= 0) {
1660 // Find all 'https://' links
1661 int end = start + 8; // Length of 'https://'
1662 while (end < contents.length && !stops.contains(contents[end])) {
1663 end += 1;
1664 }
1665 final String url = contents.substring(start, end).replaceAll('\r', '');
1666
1667 if (pattern.hasMatch(url) && !repoExceptions.any(url.contains)) {
1668 if (url.contains('master')) {
1669 problems.add('${file.path} contains $url, which uses the banned "master" branch.');
1670 suggestions.add(
1671 'Change the URLs above to the expected pattern by '
1672 'using the "main" branch if it exists, otherwise adding the '
1673 'repository to the list of exceptions in analyze.dart.',
1674 );
1675 }
1676 }
1677 start = end;
1678 }
1679 }
1680 assert(problems.isEmpty == suggestions.isEmpty);
1681 if (problems.isNotEmpty) {
1682 foundError(<String>[...problems, ...suggestions]);
1683 }
1684}
1685
1686@immutable
1687class Hash256 {
1688 const Hash256(this.a, this.b, this.c, this.d);
1689
1690 factory Hash256.fromDigest(Digest digest) {
1691 assert(digest.bytes.length == 32);
1692 return Hash256(
1693 digest.bytes[0] << 56 |
1694 digest.bytes[1] << 48 |
1695 digest.bytes[2] << 40 |
1696 digest.bytes[3] << 32 |
1697 digest.bytes[4] << 24 |
1698 digest.bytes[5] << 16 |
1699 digest.bytes[6] << 8 |
1700 digest.bytes[7] << 0,
1701 digest.bytes[8] << 56 |
1702 digest.bytes[9] << 48 |
1703 digest.bytes[10] << 40 |
1704 digest.bytes[11] << 32 |
1705 digest.bytes[12] << 24 |
1706 digest.bytes[13] << 16 |
1707 digest.bytes[14] << 8 |
1708 digest.bytes[15] << 0,
1709 digest.bytes[16] << 56 |
1710 digest.bytes[17] << 48 |
1711 digest.bytes[18] << 40 |
1712 digest.bytes[19] << 32 |
1713 digest.bytes[20] << 24 |
1714 digest.bytes[21] << 16 |
1715 digest.bytes[22] << 8 |
1716 digest.bytes[23] << 0,
1717 digest.bytes[24] << 56 |
1718 digest.bytes[25] << 48 |
1719 digest.bytes[26] << 40 |
1720 digest.bytes[27] << 32 |
1721 digest.bytes[28] << 24 |
1722 digest.bytes[29] << 16 |
1723 digest.bytes[30] << 8 |
1724 digest.bytes[31] << 0,
1725 );
1726 }
1727
1728 final int a;
1729 final int b;
1730 final int c;
1731 final int d;
1732
1733 @override
1734 bool operator ==(Object other) {
1735 if (other.runtimeType != runtimeType) {
1736 return false;
1737 }
1738 return other is Hash256 && other.a == a && other.b == b && other.c == c && other.d == d;
1739 }
1740
1741 @override
1742 int get hashCode => Object.hash(a, b, c, d);
1743}
1744
1745// DO NOT ADD ANY ENTRIES TO THIS LIST.
1746// We have a policy of not checking in binaries into this repository.
1747// If you are adding/changing template images, use the flutter_template_images
1748// package and a .img.tmpl placeholder instead.
1749// If you have other binaries to add, please consult johnmccutchan for advice.
1750final Set<Hash256> _legacyBinaries = <Hash256>{
1751 // DEFAULT ICON IMAGES
1752
1753 // packages/flutter_tools/templates/app/android.tmpl/app/src/main/res/mipmap-hdpi/ic_launcher.png
1754 // packages/flutter_tools/templates/module/android/host_app_common/app.tmpl/src/main/res/mipmap-hdpi/ic_launcher.png
1755 // (also used by many examples)
1756 const Hash256(0x6A7C8F0D703E3682, 0x108F9662F8133022, 0x36240D3F8F638BB3, 0x91E32BFB96055FEF),
1757
1758 // packages/flutter_tools/templates/app/android.tmpl/app/src/main/res/mipmap-mdpi/ic_launcher.png
1759 // (also used by many examples)
1760 const Hash256(0xC7C0C0189145E4E3, 0x2A401C61C9BDC615, 0x754B0264E7AFAE24, 0xE834BB81049EAF81),
1761
1762 // packages/flutter_tools/templates/app/android.tmpl/app/src/main/res/mipmap-xhdpi/ic_launcher.png
1763 // (also used by many examples)
1764 const Hash256(0xE14AA40904929BF3, 0x13FDED22CF7E7FFC, 0xBF1D1AAC4263B5EF, 0x1BE8BFCE650397AA),
1765
1766 // packages/flutter_tools/templates/app/android.tmpl/app/src/main/res/mipmap-xxhdpi/ic_launcher.png
1767 // (also used by many examples)
1768 const Hash256(0x4D470BF22D5C17D8, 0x4EDC5F82516D1BA8, 0xA1C09559CD761CEF, 0xB792F86D9F52B540),
1769
1770 // packages/flutter_tools/templates/app/android.tmpl/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png
1771 // (also used by many examples)
1772 const Hash256(0x3C34E1F298D0C9EA, 0x3455D46DB6B7759C, 0x8211A49E9EC6E44B, 0x635FC5C87DFB4180),
1773
1774 // packages/flutter_tools/templates/app/ios.tmpl/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png
1775 // packages/flutter_tools/templates/module/ios/host_app_ephemeral/Runner.tmpl/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png
1776 // (also used by a few examples)
1777 const Hash256(0x7770183009E91411, 0x2DE7D8EF1D235A6A, 0x30C5834424858E0D, 0x2F8253F6B8D31926),
1778
1779 // packages/flutter_tools/templates/app/ios.tmpl/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png
1780 // packages/flutter_tools/templates/module/ios/host_app_ephemeral/Runner.tmpl/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png
1781 // (also used by many examples)
1782 const Hash256(0x5925DAB509451F9E, 0xCBB12CE8A625F9D4, 0xC104718EE20CAFF8, 0xB1B51032D1CD8946),
1783
1784 // packages/flutter_tools/templates/app/ios.tmpl/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png
1785 // packages/flutter_tools/templates/app/ios.tmpl/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png
1786 // packages/flutter_tools/templates/module/ios/host_app_ephemeral/Runner.tmpl/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png
1787 // packages/flutter_tools/templates/module/ios/host_app_ephemeral/Runner.tmpl/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png
1788 // (also used by many examples)
1789 const Hash256(0xC4D9A284C12301D0, 0xF50E248EC53ED51A, 0x19A10147B774B233, 0x08399250B0D44C55),
1790
1791 // packages/flutter_tools/templates/app/ios.tmpl/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png
1792 // packages/flutter_tools/templates/module/ios/host_app_ephemeral/Runner.tmpl/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png
1793 // (also used by many examples)
1794 const Hash256(0xBF97F9D3233F33E1, 0x389B09F7B8ADD537, 0x41300CB834D6C7A5, 0xCA32CBED363A4FB2),
1795
1796 // packages/flutter_tools/templates/app/ios.tmpl/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png
1797 // packages/flutter_tools/templates/module/ios/host_app_ephemeral/Runner.tmpl/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png
1798 // (also used by many examples)
1799 const Hash256(0x285442F69A06B45D, 0x9D79DF80321815B5, 0x46473548A37B7881, 0x9B68959C7B8ED237),
1800
1801 // packages/flutter_tools/templates/app/ios.tmpl/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png
1802 // packages/flutter_tools/templates/module/ios/host_app_ephemeral/Runner.tmpl/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png
1803 // (also used by many examples)
1804 const Hash256(0x2AB64AF8AC727EA9, 0x9C6AB9EAFF847F46, 0xFBF2A9A0A78A0ABC, 0xBF3180F3851645B4),
1805
1806 // packages/flutter_tools/templates/app/ios.tmpl/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png
1807 // packages/flutter_tools/templates/module/ios/host_app_ephemeral/Runner.tmpl/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png
1808 // (also used by many examples)
1809 const Hash256(0x9DCA09F4E5ED5684, 0xD3C4DFF41F4E8B7C, 0xB864B438172D72BE, 0x069315FA362930F9),
1810
1811 // packages/flutter_tools/templates/app/ios.tmpl/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png
1812 // packages/flutter_tools/templates/module/ios/host_app_ephemeral/Runner.tmpl/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png
1813 // (also used by many examples)
1814 const Hash256(0xD5AD04DE321EF37C, 0xACC5A7B960AFCCE7, 0x1BDCB96FA020C482, 0x49C1545DD1A0F497),
1815
1816 // packages/flutter_tools/templates/app/ios.tmpl/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png
1817 // packages/flutter_tools/templates/app/ios.tmpl/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png
1818 // packages/flutter_tools/templates/module/ios/host_app_ephemeral/Runner.tmpl/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png
1819 // packages/flutter_tools/templates/module/ios/host_app_ephemeral/Runner.tmpl/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png
1820 // (also used by many examples)
1821 const Hash256(0x809ABFE75C440770, 0xC13C4E2E46D09603, 0xC22053E9D4E0E227, 0x5DCB9C1DCFBB2C75),
1822
1823 // packages/flutter_tools/templates/app/ios.tmpl/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png
1824 // packages/flutter_tools/templates/module/ios/host_app_ephemeral/Runner.tmpl/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png
1825 // (also used by many examples)
1826 const Hash256(0x3DB08CB79E7B01B9, 0xE81F956E3A0AE101, 0x48D0FAFDE3EA7AA7, 0x0048DF905AA52CFD),
1827
1828 // packages/flutter_tools/templates/app/ios.tmpl/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png
1829 // packages/flutter_tools/templates/module/ios/host_app_ephemeral/Runner.tmpl/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png
1830 // (also used by many examples)
1831 const Hash256(0x23C13D463F5DCA5C, 0x1F14A14934003601, 0xC29F1218FD461016, 0xD8A22CEF579A665F),
1832
1833 // packages/flutter_tools/templates/app/ios.tmpl/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png
1834 // packages/flutter_tools/templates/module/ios/host_app_ephemeral/Runner.tmpl/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png
1835 // (also used by many examples)
1836 const Hash256(0x6DB7726530D71D3F, 0x52CB59793EB69131, 0x3BAA04796E129E1E, 0x043C0A58A1BFFD2F),
1837
1838 // packages/flutter_tools/templates/app/ios.tmpl/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png
1839 // packages/flutter_tools/templates/module/ios/host_app_ephemeral/Runner.tmpl/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png
1840 // (also used by many examples)
1841 const Hash256(0xCEE565F5E6211656, 0x9B64980B209FD5CA, 0x4B3D3739011F5343, 0x250B33A1A2C6EB65),
1842
1843 // packages/flutter_tools/templates/app/ios.tmpl/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png
1844 // packages/flutter_tools/templates/app/ios.tmpl/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png
1845 // packages/flutter_tools/templates/app/ios.tmpl/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png
1846 // packages/flutter_tools/templates/module/ios/host_app_ephemeral/Runner.tmpl/Assets.xcassets/LaunchImage.imageset/LaunchImage.png
1847 // packages/flutter_tools/templates/module/ios/host_app_ephemeral/Runner.tmpl/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png
1848 // packages/flutter_tools/templates/module/ios/host_app_ephemeral/Runner.tmpl/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png
1849 // (also used by many examples)
1850 const Hash256(0x93AE7D494FAD0FB3, 0x0CBF3AE746A39C4B, 0xC7A0F8BBF87FBB58, 0x7A3F3C01F3C5CE20),
1851
1852 // packages/flutter_tools/templates/app/macos.tmpl/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_1024.png
1853 // (also used by a few examples)
1854 const Hash256(0xB18BEBAAD1AD6724, 0xE48BCDF699BA3927, 0xDF3F258FEBE646A3, 0xAB5C62767C6BAB40),
1855
1856 // packages/flutter_tools/templates/app/macos.tmpl/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_128.png
1857 // (also used by a few examples)
1858 const Hash256(0xF90D839A289ECADB, 0xF2B0B3400DA43EB8, 0x08B84908335AE4A0, 0x07457C4D5A56A57C),
1859
1860 // packages/flutter_tools/templates/app/macos.tmpl/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_16.png
1861 // (also used by a few examples)
1862 const Hash256(0x592C2ABF84ADB2D3, 0x91AED8B634D3233E, 0x2C65369F06018DCD, 0x8A4B27BA755EDCBE),
1863
1864 // packages/flutter_tools/templates/app/macos.tmpl/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_256.png
1865 // (also used by a few examples)
1866 const Hash256(0x75D9A0C034113CA8, 0xA1EC11C24B81F208, 0x6630A5A5C65C7D26, 0xA5DC03A1C0A4478C),
1867
1868 // packages/flutter_tools/templates/app/macos.tmpl/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_32.png
1869 // (also used by a few examples)
1870 const Hash256(0xA896E65745557732, 0xC72BD4EE3A10782F, 0xE2AA95590B5AF659, 0x869E5808DB9C01C1),
1871
1872 // packages/flutter_tools/templates/app/macos.tmpl/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_512.png
1873 // (also used by a few examples)
1874 const Hash256(0x3A69A8A1AAC5D9A8, 0x374492AF4B6D07A4, 0xCE637659EB24A784, 0x9C4DFB261D75C6A3),
1875
1876 // packages/flutter_tools/templates/app/macos.tmpl/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_64.png
1877 // (also used by a few examples)
1878 const Hash256(0xD29D4E0AF9256DC9, 0x2D0A8F8810608A5E, 0x64A132AD8B397CA2, 0xC4DDC0B1C26A68C3),
1879
1880 // packages/flutter_tools/templates/app/web/icons/Icon-192.png.copy.tmpl
1881 // dev/integration_tests/flutter_gallery/web/icons/Icon-192.png
1882 const Hash256(0x3DCE99077602F704, 0x21C1C6B2A240BC9B, 0x83D64D86681D45F2, 0x154143310C980BE3),
1883
1884 // packages/flutter_tools/templates/app/web/icons/Icon-512.png.copy.tmpl
1885 // dev/integration_tests/flutter_gallery/web/icons/Icon-512.png
1886 const Hash256(0xBACCB205AE45f0B4, 0x21BE1657259B4943, 0xAC40C95094AB877F, 0x3BCBE12CD544DCBE),
1887
1888 // packages/flutter_tools/templates/app/web/favicon.png.copy.tmpl
1889 // dev/integration_tests/flutter_gallery/web/favicon.png
1890 const Hash256(0x7AB2525F4B86B65D, 0x3E4C70358A17E5A1, 0xAAF6F437f99CBCC0, 0x46DAD73d59BB9015),
1891
1892 // GALLERY ICONS
1893
1894 // dev/integration_tests/flutter_gallery/android/app/src/main/res/mipmap-hdpi/ic_background.png
1895 const Hash256(0x03CFDE53C249475C, 0x277E8B8E90AC8A13, 0xE5FC13C358A94CCB, 0x67CA866C9862A0DD),
1896
1897 // dev/integration_tests/flutter_gallery/android/app/src/main/res/mipmap-hdpi/ic_foreground.png
1898 const Hash256(0x86A83E23A505EFCC, 0x39C358B699EDE12F, 0xC088EE516A1D0C73, 0xF3B5D74DDAD164B1),
1899
1900 // dev/integration_tests/flutter_gallery/android/app/src/main/res/mipmap-hdpi/ic_launcher.png
1901 const Hash256(0xD813B1A77320355E, 0xB68C485CD47D0F0F, 0x3C7E1910DCD46F08, 0x60A6401B8DC13647),
1902
1903 // dev/integration_tests/flutter_gallery/android/app/src/main/res/mipmap-xhdpi/ic_background.png
1904 const Hash256(0x35AFA76BD5D6053F, 0xEE927436C78A8794, 0xA8BA5F5D9FC9653B, 0xE5B96567BB7215ED),
1905
1906 // dev/integration_tests/flutter_gallery/android/app/src/main/res/mipmap-xhdpi/ic_foreground.png
1907 const Hash256(0x263CE9B4F1F69B43, 0xEBB08AE9FE8F80E7, 0x95647A59EF2C040B, 0xA8AEB246861A7DFF),
1908
1909 // dev/integration_tests/flutter_gallery/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png
1910 const Hash256(0x5E1A93C3653BAAFF, 0x1AAC6BCEB8DCBC2F, 0x2AE7D68ECB07E507, 0xCB1FA8354B28313A),
1911
1912 // dev/integration_tests/flutter_gallery/android/app/src/main/res/mipmap-xxhdpi/ic_background.png
1913 const Hash256(0xA5C77499151DDEC6, 0xDB40D0AC7321FD74, 0x0646C0C0F786743F, 0x8F3C3C408CAC5E8C),
1914
1915 // dev/integration_tests/flutter_gallery/android/app/src/main/res/mipmap-xxhdpi/ic_foreground.png
1916 const Hash256(0x33DE450980A2A16B, 0x1982AC7CDC1E7B01, 0x919E07E0289C2139, 0x65F85BCED8895FEF),
1917
1918 // dev/integration_tests/flutter_gallery/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png
1919 const Hash256(0xC3B8577F4A89BA03, 0x830944FB06C3566B, 0x4C99140A2CA52958, 0x089BFDC3079C59B7),
1920
1921 // dev/integration_tests/flutter_gallery/android/app/src/main/res/mipmap-xxxhdpi/ic_background.png
1922 const Hash256(0xDEBC241D6F9C5767, 0x8980FDD46FA7ED0C, 0x5B8ACD26BCC5E1BC, 0x473C89B432D467AD),
1923
1924 // dev/integration_tests/flutter_gallery/android/app/src/main/res/mipmap-xxxhdpi/ic_foreground.png
1925 const Hash256(0xBEFE5F7E82BF8B64, 0x148D869E3742004B, 0xF821A9F5A1BCDC00, 0x357D246DCC659DC2),
1926
1927 // dev/integration_tests/flutter_gallery/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png
1928 const Hash256(0xC385404341FF9EDD, 0x30FBE76F0EC99155, 0x8EA4F4AFE8CC0C60, 0x1CA3EDEF177E1DA8),
1929
1930 // dev/integration_tests/flutter_gallery/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-1024.png
1931 const Hash256(0x6BE5751A29F57A80, 0x36A4B31CC542C749, 0x984E49B22BD65CAA, 0x75AE8B2440848719),
1932
1933 // dev/integration_tests/flutter_gallery/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-120.png
1934 const Hash256(0x9972A2264BFA8F8D, 0x964AFE799EADC1FA, 0x2247FB31097F994A, 0x1495DC32DF071793),
1935
1936 // dev/integration_tests/flutter_gallery/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-152.png
1937 const Hash256(0x4C7CC9B09BEEDA24, 0x45F57D6967753910, 0x57D68E1A6B883D2C, 0x8C52701A74F1400F),
1938
1939 // dev/integration_tests/flutter_gallery/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-167.png
1940 const Hash256(0x66DACAC1CFE4D349, 0xDBE994CB9125FFD7, 0x2D795CFC9CF9F739, 0xEDBB06CE25082E9C),
1941
1942 // dev/integration_tests/flutter_gallery/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-180.png
1943 const Hash256(0x5188621015EBC327, 0xC9EF63AD76E60ECE, 0xE82BDC3E4ABF09E2, 0xEE0139FA7C0A2BE5),
1944
1945 // dev/integration_tests/flutter_gallery/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-20.png
1946 const Hash256(0x27D2752D04EE9A6B, 0x78410E208F74A6CD, 0xC90D9E03B73B8C60, 0xD05F7D623E790487),
1947
1948 // dev/integration_tests/flutter_gallery/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-29.png
1949 const Hash256(0xBB20556B2826CF85, 0xD5BAC73AA69C2AC3, 0x8E71DAD64F15B855, 0xB30CB73E0AF89307),
1950
1951 // dev/integration_tests/flutter_gallery/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-40.png
1952 const Hash256(0x623820FA45CDB0AC, 0x808403E34AD6A53E, 0xA3E9FDAE83EE0931, 0xB020A3A4EF2CDDE7),
1953
1954 // dev/integration_tests/flutter_gallery/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-58.png
1955 const Hash256(0xC6D631D1E107215E, 0xD4A58FEC5F3AA4B5, 0x0AE9724E07114C0C, 0x453E5D87C2CAD3B3),
1956
1957 // dev/integration_tests/flutter_gallery/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-60.png
1958 const Hash256(0x4B6F58D1EB8723C6, 0xE717A0D09FEC8806, 0x90C6D1EF4F71836E, 0x618672827979B1A2),
1959
1960 // dev/integration_tests/flutter_gallery/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-76.png
1961 const Hash256(0x0A1744CC7634D508, 0xE85DD793331F0C8A, 0x0B7C6DDFE0975D8F, 0x29E91C905BBB1BED),
1962
1963 // dev/integration_tests/flutter_gallery/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-80.png
1964 const Hash256(0x24032FBD1E6519D6, 0x0BA93C0D5C189554, 0xF50EAE23756518A2, 0x3FABACF4BD5DAF08),
1965
1966 // dev/integration_tests/flutter_gallery/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-87.png
1967 const Hash256(0xC17BAE6DF6BB234A, 0xE0AF4BEB0B805F12, 0x14E74EB7AA9A30F1, 0x5763689165DA7DDF),
1968
1969 // STOCKS ICONS
1970
1971 // dev/benchmarks/test_apps/stocks/android/app/src/main/res/mipmap-hdpi/ic_launcher.png
1972 const Hash256(0x74052AB5241D4418, 0x7085180608BC3114, 0xD12493C50CD8BBC7, 0x56DED186C37ACE84),
1973
1974 // dev/benchmarks/test_apps/stocks/android/app/src/main/res/mipmap-mdpi/ic_launcher.png
1975 const Hash256(0xE37947332E3491CB, 0x82920EE86A086FEA, 0xE1E0A70B3700A7DA, 0xDCAFBDD8F40E2E19),
1976
1977 // dev/benchmarks/test_apps/stocks/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png
1978 const Hash256(0xE608CDFC0C8579FB, 0xE38873BAAF7BC944, 0x9C9D2EE3685A4FAE, 0x671EF0C8BC41D17C),
1979
1980 // dev/benchmarks/test_apps/stocks/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png
1981 const Hash256(0xBD53D86977DF9C54, 0xF605743C5ABA114C, 0x9D51D1A8BB917E1A, 0x14CAA26C335CAEBD),
1982
1983 // dev/benchmarks/test_apps/stocks/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png
1984 const Hash256(0x64E4D02262C4F3D0, 0xBB4FDC21CD0A816C, 0x4CD2A0194E00FB0F, 0x1C3AE4142FAC0D15),
1985
1986 // dev/benchmarks/test_apps/stocks/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-60@2x.png
1987 // dev/benchmarks/test_apps/stocks/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-Small-40@3x.png
1988 const Hash256(0x5BA3283A76918FC0, 0xEE127D0F22D7A0B6, 0xDF03DAED61669427, 0x93D89DDD87A08117),
1989
1990 // dev/benchmarks/test_apps/stocks/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-60@3x.png
1991 const Hash256(0xCD7F26ED31DEA42A, 0x535D155EC6261499, 0x34E6738255FDB2C4, 0xBD8D4BDDE9A99B05),
1992
1993 // dev/benchmarks/test_apps/stocks/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-76.png
1994 const Hash256(0x3FA1225FC9A96A7E, 0xCD071BC42881AB0E, 0x7747EB72FFB72459, 0xA37971BBAD27EE24),
1995
1996 // dev/benchmarks/test_apps/stocks/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-76@2x.png
1997 const Hash256(0xCD867001ACD7BBDB, 0x25CDFD452AE89FA2, 0x8C2DC980CAF55F48, 0x0B16C246CFB389BC),
1998
1999 // dev/benchmarks/test_apps/stocks/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-83.5@2x.png
2000 const Hash256(0x848E9736E5C4915A, 0x7945BCF6B32FD56B, 0x1F1E7CDDD914352E, 0xC9681D38EF2A70DA),
2001
2002 // dev/benchmarks/test_apps/stocks/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-Notification.png
2003 const Hash256(0x654BA7D6C4E05CA0, 0x7799878884EF8F11, 0xA383E1F24CEF5568, 0x3C47604A966983C8),
2004
2005 // dev/benchmarks/test_apps/stocks/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-Notification@2x.png
2006 // dev/benchmarks/test_apps/stocks/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-Small-40.png
2007 const Hash256(0x743056FE7D83FE42, 0xA2990825B6AD0415, 0x1AF73D0D43B227AA, 0x07EBEA9B767381D9),
2008
2009 // dev/benchmarks/test_apps/stocks/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-Notification@3x.png
2010 const Hash256(0xA7E1570812D119CF, 0xEF4B602EF28DD0A4, 0x100D066E66F5B9B9, 0x881765DC9303343B),
2011
2012 // dev/benchmarks/test_apps/stocks/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-Small-40@2x.png
2013 const Hash256(0xB4102839A1E41671, 0x62DACBDEFA471953, 0xB1EE89A0AB7594BE, 0x1D9AC1E67DC2B2CE),
2014
2015 // dev/benchmarks/test_apps/stocks/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-Small.png
2016 const Hash256(0x70AC6571B593A967, 0xF1CBAEC9BC02D02D, 0x93AD766D8290ADE6, 0x840139BF9F219019),
2017
2018 // dev/benchmarks/test_apps/stocks/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-Small@2x.png
2019 const Hash256(0x5D87A78386DA2C43, 0xDDA8FEF2CA51438C, 0xE5A276FE28C6CF0A, 0xEBE89085B56665B6),
2020
2021 // dev/benchmarks/test_apps/stocks/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-Small@3x.png
2022 const Hash256(0x4D9F5E81F668DA44, 0xB20A77F8BF7BA2E1, 0xF384533B5AD58F07, 0xB3A2F93F8635CD96),
2023
2024 // LEGACY ICONS
2025
2026 // dev/benchmarks/complex_layout/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@3x.png
2027 // dev/benchmarks/microbenchmarks/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@3x.png
2028 // examples/flutter_view/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@3x.png
2029 // (not really sure where this came from, or why neither the template nor most examples use them)
2030 const Hash256(0x6E645DC9ED913AAD, 0xB50ED29EEB16830D, 0xB32CA12F39121DB9, 0xB7BC1449DDDBF8B8),
2031
2032 // dev/benchmarks/macrobenchmarks/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png
2033 // dev/integration_tests/codegen/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png
2034 // dev/integration_tests/ios_add2app/ios_add2app/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png
2035 // dev/integration_tests/release_smoke_test/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png
2036 const Hash256(0xDEFAC77E08EC71EC, 0xA04CCA3C95D1FC33, 0xB9F26E1CB15CB051, 0x47DEFC79CDD7C158),
2037
2038 // examples/flutter_view/ios/Runner/ic_add.png
2039 // examples/platform_view/ios/Runner/ic_add.png
2040 const Hash256(0x3CCE7450334675E2, 0xE3AABCA20B028993, 0x127BE82FE0EB3DFF, 0x8B027B3BAF052F2F),
2041
2042 // examples/image_list/images/coast.jpg
2043 const Hash256(0xDA957FD30C51B8D2, 0x7D74C2C918692DC4, 0xD3C5C99BB00F0D6B, 0x5EBB30395A6EDE82),
2044
2045 // examples/image_list/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png
2046 const Hash256(0xB5792CA06F48A431, 0xD4379ABA2160BD5D, 0xE92339FC64C6A0D3, 0x417AA359634CD905),
2047
2048 // TEST ASSETS
2049
2050 // dev/benchmarks/macrobenchmarks/assets/999x1000.png
2051 const Hash256(0x553E9C36DFF3E610, 0x6A608BDE822A0019, 0xDE4F1769B6FBDB97, 0xBC3C20E26B839F59),
2052
2053 // dev/bots/test/analyze-test-input/root/packages/foo/serviceaccount.enc
2054 const Hash256(0xA8100AE6AA1940D0, 0xB663BB31CD466142, 0xEBBDBD5187131B92, 0xD93818987832EB89),
2055
2056 // dev/automated_tests/icon/test.png
2057 const Hash256(0xE214B4A0FEEEC6FA, 0x8E7AA8CC9BFBEC40, 0xBCDAC2F2DEBC950F, 0x75AF8EBF02BCE459),
2058
2059 // dev/integration_tests/android_splash_screens/splash_screen_kitchen_sink/android/app/src/main/res/drawable-land-xxhdpi/flutter_splash_screen.png
2060 // dev/integration_tests/android_splash_screens/splash_screen_kitchen_sink/android/app/src/main/res/mipmap-land-xxhdpi/flutter_splash_screen.png
2061 const Hash256(0x2D4F8D7A3DFEF9D3, 0xA0C66938E169AB58, 0x8C6BBBBD1973E34E, 0x03C428416D010182),
2062
2063 // dev/integration_tests/android_splash_screens/splash_screen_kitchen_sink/android/app/src/main/res/drawable-xxhdpi/flutter_splash_screen.png
2064 // dev/integration_tests/android_splash_screens/splash_screen_kitchen_sink/android/app/src/main/res/mipmap-xxhdpi/flutter_splash_screen.png
2065 const Hash256(0xCD46C01BAFA3B243, 0xA6AA1645EEDDE481, 0x143AC8ABAB1A0996, 0x22CAA9D41F74649A),
2066
2067 // dev/integration_tests/flutter_driver_screenshot_test/assets/red_square.png
2068 const Hash256(0x40054377E1E084F4, 0x4F4410CE8F44C210, 0xABA945DFC55ED0EF, 0x23BDF9469E32F8D3),
2069
2070 // dev/integration_tests/flutter_driver_screenshot_test/test_driver/goldens/red_square_image/iPhone7,2.png
2071 const Hash256(0x7F9D27C7BC418284, 0x01214E21CA886B2F, 0x40D9DA2B31AE7754, 0x71D68375F9C8A824),
2072
2073 // examples/flutter_view/assets/flutter-mark-square-64.png
2074 // examples/platform_view/assets/flutter-mark-square-64.png
2075 const Hash256(0xF416B0D8AC552EC8, 0x819D1F492D1AB5E6, 0xD4F20CF45DB47C22, 0x7BB431FEFB5B67B2),
2076
2077 // packages/flutter_tools/test/data/intellij/plugins/Dart/lib/Dart.jar
2078 const Hash256(0x576E489D788A13DB, 0xBF40E4A39A3DAB37, 0x15CCF0002032E79C, 0xD260C69B29E06646),
2079
2080 // packages/flutter_tools/test/data/intellij/plugins/flutter-intellij.jar
2081 const Hash256(0x4C67221E25626CB2, 0x3F94E1F49D34E4CF, 0x3A9787A514924FC5, 0x9EF1E143E5BC5690),
2082
2083 // MISCELLANEOUS
2084
2085 // dev/bots/serviceaccount.enc
2086 const Hash256(0x1F19ADB4D80AFE8C, 0xE61899BA776B1A8D, 0xCA398C75F5F7050D, 0xFB0E72D7FBBBA69B),
2087
2088 // dev/docs/favicon.ico
2089 const Hash256(0x67368CA1733E933A, 0xCA3BC56EF0695012, 0xE862C371AD4412F0, 0x3EC396039C609965),
2090
2091 // dev/snippets/assets/code_sample.png
2092 const Hash256(0xAB2211A47BDA001D, 0x173A52FD9C75EBC7, 0xE158942FFA8243AD, 0x2A148871990D4297),
2093
2094 // dev/snippets/assets/code_snippet.png
2095 const Hash256(0xDEC70574DA46DFBB, 0xFA657A771F3E1FBD, 0xB265CFC6B2AA5FE3, 0x93BA4F325D1520BA),
2096
2097 // packages/flutter_tools/static/Ahem.ttf
2098 const Hash256(0x63D2ABD0041C3E3B, 0x4B52AD8D382353B5, 0x3C51C6785E76CE56, 0xED9DACAD2D2E31C4),
2099};
2100
2101Future<void> verifyNoBinaries(String workingDirectory, {Set<Hash256>? legacyBinaries}) async {
2102 // Please do not add anything to the _legacyBinaries set above.
2103 // We have a policy of not checking in binaries into this repository.
2104 // If you are adding/changing template images, use the flutter_template_images
2105 // package and a .img.tmpl placeholder instead.
2106 // If you have other binaries to add, please consult johnmccutchan for advice.
2107 assert(
2108 _legacyBinaries
2109 .expand<int>((Hash256 hash) => <int>[hash.a, hash.b, hash.c, hash.d])
2110 .reduce((int value, int element) => value ^ element) ==
2111 0x606B51C908B40BFA, // Please do not modify this line.
2112 );
2113 legacyBinaries ??= _legacyBinaries;
2114 if (!Platform.isWindows) {
2115 // TODO(ianh): Port this to Windows
2116 final List<File> files = await _gitFiles(workingDirectory);
2117 final List<String> problems = <String>[];
2118 for (final File file in files) {
2119 final Uint8List bytes = file.readAsBytesSync();
2120 try {
2121 utf8.decode(bytes);
2122 } on FormatException catch (error) {
2123 final Digest digest = sha256.convert(bytes);
2124 if (!legacyBinaries.contains(Hash256.fromDigest(digest))) {
2125 problems.add('${file.path}:${error.offset}: file is not valid UTF-8');
2126 }
2127 }
2128 }
2129 if (problems.isNotEmpty) {
2130 foundError(<String>[
2131 ...problems,
2132 'All files in this repository must be UTF-8. In particular, images and other binaries',
2133 'must not be checked into this repository. This is because we are very sensitive to the',
2134 'size of the repository as it is distributed to all our developers. If you have a binary',
2135 'to which you need access, you should consider how to fetch it from another repository;',
2136 'for example, the "assets-for-api-docs" repository is used for images in API docs.',
2137 'To add assets to flutter_tools templates, see the instructions in the wiki:',
2138 'https://github.com/flutter/flutter/blob/main/docs/tool/Managing-template-image-assets.md',
2139 ]);
2140 }
2141 }
2142}
2143
2144// UTILITY FUNCTIONS
2145
2146bool _listEquals<T>(List<T> a, List<T> b) {
2147 if (a.length != b.length) {
2148 return false;
2149 }
2150 for (int index = 0; index < a.length; index += 1) {
2151 if (a[index] != b[index]) {
2152 return false;
2153 }
2154 }
2155 return true;
2156}
2157
2158Future<List<File>> _gitFiles(String workingDirectory, {bool runSilently = true}) async {
2159 final EvalResult evalResult = await _evalCommand(
2160 'git',
2161 <String>['ls-files', '-z'],
2162 workingDirectory: workingDirectory,
2163 runSilently: runSilently,
2164 );
2165 if (evalResult.exitCode != 0) {
2166 foundError(<String>[
2167 'git ls-files failed with exit code ${evalResult.exitCode}',
2168 '${bold}stdout:$reset',
2169 evalResult.stdout,
2170 '${bold}stderr:$reset',
2171 evalResult.stderr,
2172 ]);
2173 }
2174 final List<String> filenames = evalResult.stdout.split('\x00');
2175 assert(filenames.last.isEmpty); // git ls-files gives a trailing blank 0x00
2176 filenames.removeLast();
2177 return filenames
2178 .where((String filename) => !filename.startsWith('engine/'))
2179 .map<File>((String filename) => File(path.join(workingDirectory, filename)))
2180 .toList();
2181}
2182
2183Stream<File> _allFiles(
2184 String workingDirectory,
2185 String? extension, {
2186 required int minimumMatches,
2187}) async* {
2188 final Set<String> gitFileNamesSet = <String>{};
2189 gitFileNamesSet.addAll(
2190 (await _gitFiles(workingDirectory)).map((File f) => path.canonicalize(f.absolute.path)),
2191 );
2192
2193 assert(
2194 extension == null || !extension.startsWith('.'),
2195 'Extension argument should not start with a period.',
2196 );
2197 final Set<FileSystemEntity> pending = <FileSystemEntity>{Directory(workingDirectory)};
2198 int matches = 0;
2199 while (pending.isNotEmpty) {
2200 final FileSystemEntity entity = pending.first;
2201 pending.remove(entity);
2202 if (path.extension(entity.path) == '.tmpl') {
2203 continue;
2204 }
2205 if (entity is File) {
2206 if (!gitFileNamesSet.contains(path.canonicalize(entity.absolute.path))) {
2207 continue;
2208 }
2209 if (_isGeneratedPluginRegistrant(entity)) {
2210 continue;
2211 }
2212 switch (path.basename(entity.path)) {
2213 case 'flutter_export_environment.sh' || 'gradlew.bat' || '.DS_Store':
2214 continue;
2215 }
2216 if (extension == null || path.extension(entity.path) == '.$extension') {
2217 matches += 1;
2218 yield entity;
2219 }
2220 } else if (entity is Directory) {
2221 if (File(path.join(entity.path, '.dartignore')).existsSync()) {
2222 continue;
2223 }
2224 switch (path.basename(entity.path)) {
2225 case '.git' || '.idea' || '.gradle' || '.dart_tool' || 'build':
2226 continue;
2227 }
2228 pending.addAll(entity.listSync());
2229 }
2230 }
2231 assert(
2232 matches >= minimumMatches,
2233 'Expected to find at least $minimumMatches files with extension ".$extension" in "$workingDirectory", but only found $matches.',
2234 );
2235}
2236
2237class EvalResult {
2238 EvalResult({required this.stdout, required this.stderr, this.exitCode = 0});
2239
2240 final String stdout;
2241 final String stderr;
2242 final int exitCode;
2243}
2244
2245// TODO(ianh): Refactor this to reuse the code in run_command.dart
2246Future<EvalResult> _evalCommand(
2247 String executable,
2248 List<String> arguments, {
2249 required String workingDirectory,
2250 Map<String, String>? environment,
2251 bool allowNonZeroExit = false,
2252 bool runSilently = false,
2253}) async {
2254 final String commandDescription =
2255 '${path.relative(executable, from: workingDirectory)} ${arguments.join(' ')}';
2256 final String relativeWorkingDir = path.relative(workingDirectory);
2257
2258 if (!runSilently) {
2259 print('RUNNING: cd $cyan$relativeWorkingDir$reset; $green$commandDescription$reset');
2260 }
2261
2262 final Stopwatch time = Stopwatch()..start();
2263 final Process process = await Process.start(
2264 executable,
2265 arguments,
2266 workingDirectory: workingDirectory,
2267 environment: environment,
2268 );
2269
2270 final Future<List<List<int>>> savedStdout = process.stdout.toList();
2271 final Future<List<List<int>>> savedStderr = process.stderr.toList();
2272 final int exitCode = await process.exitCode;
2273 final EvalResult result = EvalResult(
2274 stdout: utf8.decode((await savedStdout).expand<int>((List<int> ints) => ints).toList()),
2275 stderr: utf8.decode((await savedStderr).expand<int>((List<int> ints) => ints).toList()),
2276 exitCode: exitCode,
2277 );
2278
2279 if (!runSilently) {
2280 print(
2281 'ELAPSED TIME: $bold${prettyPrintDuration(time.elapsed)}$reset for $commandDescription in $relativeWorkingDir',
2282 );
2283 }
2284
2285 if (exitCode != 0 && !allowNonZeroExit) {
2286 foundError(<String>[
2287 result.stderr,
2288 '${bold}ERROR:$red Last command exited with $exitCode.$reset',
2289 '${bold}Command:$red $commandDescription$reset',
2290 '${bold}Relative working directory:$red $relativeWorkingDir$reset',
2291 ]);
2292 }
2293
2294 return result;
2295}
2296
2297Future<void> _checkConsumerDependencies() async {
2298 const List<String> kCorePackages = <String>[
2299 'flutter',
2300 'flutter_test',
2301 'flutter_driver',
2302 'flutter_localizations',
2303 'integration_test',
2304 'fuchsia_remote_debug_protocol',
2305 ];
2306 final Set<String> dependencies = <String>{};
2307
2308 // Parse the output of pub deps --json to determine all of the
2309 // current packages used by the core set of flutter packages.
2310 for (final String package in kCorePackages) {
2311 final ProcessResult result = await Process.run(flutter, <String>[
2312 'pub',
2313 'deps',
2314 '--json',
2315 '--directory=${path.join(flutterRoot, 'packages', package)}',
2316 ]);
2317 if (result.exitCode != 0) {
2318 foundError(<String>[result.stdout.toString(), result.stderr.toString()]);
2319 return;
2320 }
2321 final Map<String, Object?> rawJson =
2322 json.decode(result.stdout as String) as Map<String, Object?>;
2323 final Map<String, Map<String, Object?>> dependencyTree = <String, Map<String, Object?>>{
2324 for (final Map<String, Object?> package
2325 in (rawJson['packages']! as List<Object?>).cast<Map<String, Object?>>())
2326 package['name']! as String: package,
2327 };
2328 final List<Map<String, Object?>> workset = <Map<String, Object?>>[];
2329 workset.add(dependencyTree[package]!);
2330
2331 while (workset.isNotEmpty) {
2332 final Map<String, Object?> currentPackage = workset.removeLast();
2333 if (currentPackage['kind'] == 'dev') {
2334 continue;
2335 }
2336 dependencies.add(currentPackage['name']! as String);
2337
2338 final List<String> currentDependencies =
2339 (currentPackage['directDependencies']! as List<Object?>).cast<String>();
2340 for (final String dependency in currentDependencies) {
2341 // Don't add dependencies we've already seen or we will get stuck
2342 // forever if there are any circular references.
2343 // TODO(dantup): Consider failing gracefully with the names of the
2344 // packages once the cycle between test_api and matcher is resolved.
2345 // https://github.com/dart-lang/test/issues/1979
2346 if (!dependencies.contains(dependency)) {
2347 workset.add(dependencyTree[dependency]!);
2348 }
2349 }
2350 }
2351 }
2352
2353 final Set<String> removed = kCorePackageAllowList.difference(dependencies);
2354 final Set<String> added = dependencies.difference(kCorePackageAllowList);
2355
2356 String plural(int n, String s, String p) => n == 1 ? s : p;
2357
2358 if (added.isNotEmpty) {
2359 foundError(<String>[
2360 'The transitive closure of package dependencies contains ${plural(added.length, "a non-allowlisted package", "non-allowlisted packages")}:',
2361 ' ${added.join(', ')}',
2362 'We strongly desire to keep the number of dependencies to a minimum and',
2363 'therefore would much prefer not to add new dependencies.',
2364 'See dev/bots/allowlist.dart for instructions on how to update the package',
2365 'allowlist if you nonetheless believe this is a necessary addition.',
2366 ]);
2367 }
2368
2369 if (removed.isNotEmpty) {
2370 foundError(<String>[
2371 'Excellent news! ${plural(removed.length, "A package dependency has been removed!", "Multiple package dependencies have been removed!")}',
2372 ' ${removed.join(', ')}',
2373 'To make sure we do not accidentally add ${plural(removed.length, "this dependency", "these dependencies")} back in the future,',
2374 'please remove ${plural(removed.length, "this", "these")} packages from the allow-list in dev/bots/allowlist.dart.',
2375 'Thanks!',
2376 ]);
2377 }
2378}
2379
2380class _DebugOnlyFieldVisitor extends RecursiveAstVisitor<void> {
2381 _DebugOnlyFieldVisitor(this.parseResult);
2382
2383 final ParseStringResult parseResult;
2384 final List<AstNode> errors = <AstNode>[];
2385
2386 static const String _kDebugOnlyAnnotation = '_debugOnly';
2387 static final RegExp _nullInitializedField = RegExp(r'kDebugMode \? [\w<> ,{}()]+ : null;');
2388
2389 @override
2390 void visitFieldDeclaration(FieldDeclaration node) {
2391 super.visitFieldDeclaration(node);
2392 if (node.metadata.any(
2393 (Annotation annotation) => annotation.name.name == _kDebugOnlyAnnotation,
2394 )) {
2395 if (!node.toSource().contains(_nullInitializedField)) {
2396 errors.add(node.fields); // Use the fields node for line number.
2397 }
2398 }
2399 }
2400}
2401
2402Future<void> verifyNullInitializedDebugExpensiveFields(
2403 String workingDirectory, {
2404 int minimumMatches = 400,
2405}) async {
2406 final String flutterLib = path.join(workingDirectory, 'packages', 'flutter', 'lib');
2407 final List<File> files = await _allFiles(
2408 flutterLib,
2409 'dart',
2410 minimumMatches: minimumMatches,
2411 ).toList();
2412 final List<String> errors = <String>[];
2413 for (final File file in files) {
2414 final ParseStringResult parsedFile = parseFile(
2415 featureSet: _parsingFeatureSet(),
2416 path: file.absolute.path,
2417 );
2418 final _DebugOnlyFieldVisitor visitor = _DebugOnlyFieldVisitor(parsedFile);
2419 visitor.visitCompilationUnit(parsedFile.unit);
2420 for (final AstNode badNode in visitor.errors) {
2421 errors.add(
2422 '${file.path}:${parsedFile.lineInfo.getLocation(badNode.offset).lineNumber}: fields annotated with @_debugOnly must null initialize.',
2423 );
2424 }
2425 }
2426 if (errors.isNotEmpty) {
2427 foundError(<String>[
2428 ...errors,
2429 '',
2430 '$bold${red}Fields annotated with @_debugOnly must null initialize,$reset',
2431 'to ensure both the field and initializer are removed from profile/release mode.',
2432 'These fields should be written as:',
2433 'field = kDebugMode ? <DebugValue> : null;',
2434 ]);
2435 }
2436}
2437
2438final RegExp tabooPattern = RegExp(r'^ *///.*\b(simply|note:|note that)\b', caseSensitive: false);
2439
2440Future<void> verifyTabooDocumentation(String workingDirectory, {int minimumMatches = 100}) async {
2441 final List<String> errors = <String>[];
2442 await for (final File file in _allFiles(
2443 workingDirectory,
2444 'dart',
2445 minimumMatches: minimumMatches,
2446 )) {
2447 final List<String> lines = file.readAsLinesSync();
2448 for (int index = 0; index < lines.length; index += 1) {
2449 final String line = lines[index];
2450 final Match? match = tabooPattern.firstMatch(line);
2451 if (match != null) {
2452 errors.add(
2453 '${file.path}:${index + 1}: Found use of the taboo word "${match.group(1)}" in documentation string.',
2454 );
2455 }
2456 }
2457 }
2458 if (errors.isNotEmpty) {
2459 foundError(<String>[
2460 ...errors,
2461 '',
2462 '${bold}Avoid the word "simply" in documentation. See https://github.com/flutter/flutter/blob/main/docs/contributing/Style-guide-for-Flutter-repo.md#use-the-passive-voice-recommend-do-not-require-never-say-things-are-simple for details.$reset',
2463 '${bold}In many cases these words can be omitted without loss of generality; in other cases it may require a bit of rewording to avoid implying that the task is simple.$reset',
2464 '${bold}Similarly, avoid using "note:" or the phrase "note that". See https://github.com/flutter/flutter/blob/main/docs/contributing/Style-guide-for-Flutter-repo.md#avoid-empty-prose for details.$reset',
2465 ]);
2466 }
2467}
2468
2469final Map<String, String> _kKotlinTemplateKeys = <String, String>{
2470 'androidIdentifier': 'dummyPackage',
2471 'pluginClass': 'PluginClass',
2472 'projectName': 'dummy',
2473 'agpVersion': '0.0.0.1',
2474 'kotlinVersion': '0.0.0.1',
2475};
2476
2477final String _kKotlinTemplateRelativePath = path.join('packages', 'flutter_tools', 'templates');
2478
2479const List<String> _kKotlinExtList = <String>['.kt.tmpl', '.kts.tmpl'];
2480const String _kKotlinTmplExt = '.tmpl';
2481final RegExp _kKotlinTemplatePattern = RegExp(r'{{(.*?)}}');
2482
2483/// Copy kotlin template files from [_kKotlinTemplateRelativePath] into a system tmp folder
2484/// then replace template values with values from [_kKotlinTemplateKeys] or "'dummy'" if an
2485/// unknown key is found. Then run ktlint on the tmp folder to check for lint errors in the
2486/// generated Kotlin files.
2487Future<void> lintKotlinTemplatedFiles(String workingDirectory) async {
2488 final String templatePath = path.join(workingDirectory, _kKotlinTemplateRelativePath);
2489 final Iterable<File> files = Directory(templatePath)
2490 .listSync(recursive: true)
2491 .toList()
2492 .whereType<File>()
2493 .where((File file) => _kKotlinExtList.contains(path.extension(file.path, 2)));
2494
2495 if (files.isEmpty) {
2496 foundError(<String>['No Kotlin template files found']);
2497 return;
2498 }
2499
2500 final Directory tempDir = Directory.systemTemp.createTempSync('template_output');
2501 for (final File templateFile in files) {
2502 final String inputContent = await templateFile.readAsString();
2503 final String modifiedContent = inputContent.replaceAllMapped(
2504 _kKotlinTemplatePattern,
2505 (Match match) => _kKotlinTemplateKeys[match[1]] ?? 'dummy',
2506 );
2507
2508 String outputFilename = path.basename(templateFile.path);
2509 outputFilename = outputFilename.substring(
2510 0,
2511 outputFilename.length - _kKotlinTmplExt.length,
2512 ); // Remove '.tmpl' from file path
2513
2514 // Ensure the first letter of the generated class is uppercase (instead of pluginClass)
2515 outputFilename = outputFilename.substring(0, 1).toUpperCase() + outputFilename.substring(1);
2516
2517 final String relativePath = path.dirname(path.relative(templateFile.path, from: templatePath));
2518 final String outputDir = path.join(tempDir.path, relativePath);
2519 await Directory(outputDir).create(recursive: true);
2520 final String outputFile = path.join(outputDir, outputFilename);
2521 final File output = File(outputFile);
2522 await output.writeAsString(modifiedContent);
2523 }
2524 return lintKotlinFiles(tempDir.path).whenComplete(() {
2525 tempDir.deleteSync(recursive: true);
2526 });
2527}
2528
2529Future<void> lintKotlinFiles(String workingDirectory) async {
2530 const String baselineRelativePath = 'dev/bots/test/analyze-test-input/ktlint-baseline.xml';
2531 const String editorConfigRelativePath = 'dev/bots/test/analyze-test-input/.editorconfig';
2532 final EvalResult lintResult = await _evalCommand('ktlint', <String>[
2533 '--baseline=$flutterRoot/$baselineRelativePath',
2534 '--editorconfig=$flutterRoot/$editorConfigRelativePath',
2535 ], workingDirectory: workingDirectory);
2536 if (lintResult.exitCode != 0) {
2537 final String errorMessage =
2538 'Found lint violations in Kotlin files:\n ${lintResult.stdout}\n\n'
2539 'To reproduce this lint locally:\n'
2540 '1. Identify the CIPD version tag used to resolve this particular version of ktlint (check the dependencies section of this shard in the ci.yaml). \n'
2541 '2. Download that version from https://chrome-infra-packages.appspot.com/p/flutter/ktlint/linux-amd64/+/\n'
2542 '3. From the repository root, run `<path_to_ktlint>/ktlint --editorconfig=$editorConfigRelativePath --baseline=$baselineRelativePath`\n'
2543 'Alternatively, if you use Android Studio, follow the docs at docs/platforms/android/Kotlin-android-studio-formatting.md to enable auto formatting.';
2544 foundError(<String>[errorMessage]);
2545 }
2546}
2547
2548const List<String> _kIgnoreList = <String>['Runner.rc.tmpl', 'flutter_window.cpp'];
2549final String _kIntegrationTestsRelativePath = path.join('dev', 'integration_tests');
2550final String _kTemplateRelativePath = path.join(
2551 'packages',
2552 'flutter_tools',
2553 'templates',
2554 'app',
2555 'windows.tmpl',
2556 'runner',
2557);
2558final String _kWindowsRunnerSubPath = path.join('windows', 'runner');
2559const String _kProjectNameKey = '{{projectName}}';
2560const String _kTmplExt = '.tmpl';
2561
2562String _getFlutterLicense() {
2563 return '// Copyright 2014 The Flutter Authors. All rights reserved.\n'
2564 '// Use of this source code is governed by a BSD-style license that can be\n'
2565 '// found in the LICENSE file.\n'
2566 '\n';
2567}
2568
2569String _removeLicenseIfPresent(String fileContents, String license) {
2570 if (fileContents.startsWith(license)) {
2571 return fileContents.substring(license.length);
2572 }
2573 return fileContents;
2574}
2575
2576Future<void> verifyIntegrationTestTemplateFiles(String flutterRoot) async {
2577 final List<String> errors = <String>[];
2578 final String license = _getFlutterLicense();
2579 final String integrationTestsPath = path.join(flutterRoot, _kIntegrationTestsRelativePath);
2580 final String templatePath = path.join(flutterRoot, _kTemplateRelativePath);
2581 final Iterable<Directory> subDirs = Directory(
2582 integrationTestsPath,
2583 ).listSync().toList().whereType<Directory>();
2584 for (final Directory testPath in subDirs) {
2585 final String projectName = path.basename(testPath.path);
2586 final String runnerPath = path.join(testPath.path, _kWindowsRunnerSubPath);
2587 final Directory runner = Directory(runnerPath);
2588 if (!runner.existsSync()) {
2589 continue;
2590 }
2591 final Iterable<File> files = Directory(templatePath).listSync().toList().whereType<File>();
2592 for (final File templateFile in files) {
2593 final String fileName = path.basename(templateFile.path);
2594 if (_kIgnoreList.contains(fileName)) {
2595 continue;
2596 }
2597 String templateFileContents = templateFile.readAsLinesSync().join('\n');
2598 String appFilePath = path.join(runnerPath, fileName);
2599 if (fileName.endsWith(_kTmplExt)) {
2600 appFilePath = appFilePath.substring(
2601 0,
2602 appFilePath.length - _kTmplExt.length,
2603 ); // Remove '.tmpl' from app file path
2604 templateFileContents = templateFileContents.replaceAll(
2605 _kProjectNameKey,
2606 projectName,
2607 ); // Substitute template project name
2608 }
2609 String appFileContents = File(appFilePath).readAsLinesSync().join('\n');
2610 appFileContents = _removeLicenseIfPresent(appFileContents, license);
2611 if (appFileContents != templateFileContents) {
2612 int indexOfDifference;
2613 for (
2614 indexOfDifference = 0;
2615 indexOfDifference < appFileContents.length;
2616 indexOfDifference++
2617 ) {
2618 if (indexOfDifference >= templateFileContents.length ||
2619 templateFileContents.codeUnitAt(indexOfDifference) !=
2620 appFileContents.codeUnitAt(indexOfDifference)) {
2621 break;
2622 }
2623 }
2624 final String error =
2625 '''
2626Error: file $fileName mismatched for integration test $testPath
2627Verify the integration test has been migrated to the latest app template.
2628=====$appFilePath======
2629$appFileContents
2630=====${templateFile.path}======
2631$templateFileContents
2632==========
2633Diff at character #$indexOfDifference
2634 ''';
2635 errors.add(error);
2636 }
2637 }
2638 }
2639 if (errors.isNotEmpty) {
2640 foundError(errors);
2641 }
2642}
2643
2644Future<CommandResult> _runFlutterAnalyze(
2645 String workingDirectory, {
2646 List<String> options = const <String>[],
2647 String? failureMessage,
2648}) async {
2649 return runCommand(
2650 flutter,
2651 <String>['analyze', ...options],
2652 workingDirectory: workingDirectory,
2653 failureMessage: failureMessage,
2654 );
2655}
2656
2657// These files legitimately require executable permissions
2658const Set<String> kExecutableAllowlist = <String>{
2659 'bin/dart',
2660 'bin/flutter',
2661 'bin/flutter-dev',
2662 'bin/internal/last_engine_commit.sh',
2663 'bin/internal/update_dart_sdk.sh',
2664 'bin/internal/update_engine_version.sh',
2665 'bin/internal/content_aware_hash.sh',
2666
2667 'dev/bots/codelabs_build_test.sh',
2668 'dev/bots/docs.sh',
2669
2670 'dev/checks',
2671
2672 'dev/customer_testing/ci.sh',
2673
2674 'dev/integration_tests/flutter_gallery/tool/run_instrumentation_test.sh',
2675
2676 'dev/integration_tests/ios_add2app_life_cycle/build_and_test.sh',
2677
2678 'dev/integration_tests/deferred_components_test/download_assets.sh',
2679 'dev/integration_tests/deferred_components_test/run_release_test.sh',
2680
2681 'dev/packages_autoroller/run',
2682
2683 'dev/tools/gen_keycodes/bin/gen_keycodes',
2684 'dev/tools/repackage_gradle_wrapper.sh',
2685 'dev/tools/bin/engine_hash.sh',
2686 'dev/tools/format.sh',
2687 'dev/tools/test/mock_git.sh',
2688
2689 'packages/flutter_tools/bin/macos_assemble.sh',
2690 'packages/flutter_tools/bin/tool_backend.sh',
2691 'packages/flutter_tools/bin/xcode_backend.sh',
2692};
2693
2694Future<void> _checkForNewExecutables() async {
2695 // 0b001001001
2696 const int executableBitMask = 0x49;
2697 final List<File> files = await _gitFiles(flutterRoot);
2698 final List<String> errors = <String>[];
2699 for (final File file in files) {
2700 final String relativePath = path.relative(file.path, from: flutterRoot);
2701 final FileStat stat = file.statSync();
2702 final bool isExecutable = stat.mode & executableBitMask != 0x0;
2703 if (isExecutable && !kExecutableAllowlist.contains(relativePath)) {
2704 errors.add('$relativePath is executable: ${(stat.mode & 0x1FF).toRadixString(2)}');
2705 }
2706 }
2707 if (errors.isNotEmpty) {
2708 throw Exception(
2709 '${errors.join('\n')}\n'
2710 'found ${errors.length} unexpected executable file'
2711 '${errors.length == 1 ? '' : 's'}! If this was intended, you '
2712 'must add this file to kExecutableAllowlist in dev/bots/analyze.dart',
2713 );
2714 }
2715}
2716
2717final RegExp _importPattern = RegExp(r'''^\s*import (['"])package:flutter/([^.]+)\.dart\1''');
2718final RegExp _importMetaPattern = RegExp(r'''^\s*import (['"])package:meta/meta\.dart\1''');
2719
2720Future<Set<String>> _findFlutterDependencies(
2721 String srcPath,
2722 List<String> errors, {
2723 bool checkForMeta = false,
2724}) async {
2725 return _allFiles(srcPath, 'dart', minimumMatches: 1)
2726 .map<Set<String>>((File file) {
2727 final Set<String> result = <String>{};
2728 for (final String line in file.readAsLinesSync()) {
2729 Match? match = _importPattern.firstMatch(line);
2730 if (match != null) {
2731 result.add(match.group(2)!);
2732 }
2733 if (checkForMeta) {
2734 match = _importMetaPattern.firstMatch(line);
2735 if (match != null) {
2736 errors.add(
2737 '${file.path}\nThis package imports the ${yellow}meta$reset package.\n'
2738 'You should instead import the "foundation.dart" library.',
2739 );
2740 }
2741 }
2742 }
2743 return result;
2744 })
2745 .reduce((Set<String>? value, Set<String> element) {
2746 value ??= <String>{};
2747 value.addAll(element);
2748 return value;
2749 });
2750}
2751
2752List<T>? _deepSearch<T>(Map<T, Set<T>> map, T start, [Set<T>? seen]) {
2753 if (map[start] == null) {
2754 return null; // We catch these separately.
2755 }
2756
2757 for (final T key in map[start]!) {
2758 if (key == start) {
2759 continue; // we catch these separately
2760 }
2761 if (seen != null && seen.contains(key)) {
2762 return <T>[start, key];
2763 }
2764 final List<T>? result = _deepSearch<T>(map, key, <T>{
2765 if (seen == null) start else ...seen,
2766 key,
2767 });
2768 if (result != null) {
2769 result.insert(0, start);
2770 // Only report the shortest chains.
2771 // For example a->b->a, rather than c->a->b->a.
2772 // Since we visit every node, we know the shortest chains are those
2773 // that start and end on the loop.
2774 if (result.first == result.last) {
2775 return result;
2776 }
2777 }
2778 }
2779 return null;
2780}
2781
2782bool _isGeneratedPluginRegistrant(File file) {
2783 final String filename = path.basename(file.path);
2784 return !file.path.contains('.pub-cache') &&
2785 (filename == 'GeneratedPluginRegistrant.java' ||
2786 filename == 'GeneratedPluginRegistrant.swift' ||
2787 filename == 'GeneratedPluginRegistrant.h' ||
2788 filename == 'GeneratedPluginRegistrant.m' ||
2789 filename == 'generated_plugin_registrant.dart' ||
2790 filename == 'generated_plugin_registrant.h' ||
2791 filename == 'generated_plugin_registrant.cc');
2792}
2793