import 'dart:convert';
import 'dart:core' hide print;
import 'dart:io' hide exit;
import 'dart:typed_data';
import 'package:analyzer/dart/analysis/features.dart';
import 'package:analyzer/dart/analysis/results.dart';
import 'package:analyzer/dart/analysis/utilities.dart';
import 'package:analyzer/dart/ast/ast.dart';
import 'package:analyzer/dart/ast/visitor.dart';
import 'package:collection/equality.dart';
import 'package:crypto/crypto.dart';
import 'package:meta/meta.dart';
import 'package:path/path.dart' as path;
import 'allowlist.dart';
import 'custom_rules/analyze.dart';
import 'custom_rules/avoid_future_catcherror.dart';
import 'custom_rules/no_double_clamp.dart';
import 'custom_rules/no_stop_watches.dart';
import 'custom_rules/protect_public_state_subtypes.dart';
import 'custom_rules/render_box_intrinsics.dart';
import 'run_command.dart';
import 'utils.dart';
final String flutterPackages = path.join(flutterRoot, 'packages');
final String flutterExamples = path.join(flutterRoot, 'examples');
late final String dart;
late final String pub;
Future<void> main(List<String> arguments) async {
final String dartSdk = path.join(
Directory.current.absolute.path,
_getDartSdkFromArguments(arguments) ?? path.join(flutterRoot, 'bin', 'cache', 'dart-sdk'),
);
dart = path.join(dartSdk, 'bin', Platform.isWindows ? 'dart.exe' : 'dart');
pub = path.join(dartSdk, 'bin', Platform.isWindows ? 'pub.bat' : 'pub');
printProgress('STARTING ANALYSIS');
await run(arguments);
if (hasError) {
reportErrorsAndExit('${bold}Analysis failed.$reset');
}
reportSuccessAndExit('${bold}Analysis successful.$reset');
}
String? _getDartSdkFromArguments(List<String> arguments) {
String? result;
for (int i = 0; i < arguments.length; i += 1) {
if (arguments[i] == '--dart-sdk') {
if (result != null) {
foundError(<String>['The --dart-sdk argument must not be used more than once.']);
return null;
}
if (i + 1 < arguments.length) {
result = arguments[i + 1];
} else {
foundError(<String>['--dart-sdk must be followed by a path.']);
return null;
}
}
if (arguments[i].startsWith('--dart-sdk=')) {
if (result != null) {
foundError(<String>['The --dart-sdk argument must not be used more than once.']);
return null;
}
result = arguments[i].substring('--dart-sdk='.length);
}
}
return result;
}
Future<void> run(List<String> arguments) async {
bool assertsEnabled = false;
assert(() {
assertsEnabled = true;
return true;
}());
if (!assertsEnabled) {
foundError(<String>['The analyze.dart script must be run with --enable-asserts.']);
}
printProgress('Release branch validation');
await verifyReleaseBranchState(flutterRoot);
printProgress('TargetPlatform tool/framework consistency');
await verifyTargetPlatform(flutterRoot);
printProgress('All tool test files end in _test.dart...');
await verifyToolTestsEndInTestDart(flutterRoot);
printProgress('No sync*/async*');
await verifyNoSyncAsyncStar(flutterPackages);
await verifyNoSyncAsyncStar(flutterExamples, minimumMatches: 200);
printProgress('No runtimeType in toString...');
await verifyNoRuntimeTypeInToString(flutterRoot);
printProgress('Debug mode instead of checked mode...');
await verifyNoCheckedMode(flutterRoot);
printProgress('Links for creating GitHub issues...');
await verifyIssueLinks(flutterRoot);
printProgress('Links to repositories...');
await verifyRepositoryLinks(flutterRoot);
printProgress('Unexpected binaries...');
await verifyNoBinaries(flutterRoot);
printProgress('Trailing spaces...');
await verifyNoTrailingSpaces(
flutterRoot,
);
printProgress('Spaces after flow control statements...');
await verifySpacesAfterFlowControlStatements(flutterRoot);
printProgress('Deprecations...');
await verifyDeprecations(flutterRoot);
printProgress('Goldens...');
await verifyGoldenTags(flutterPackages);
printProgress('Skip test comments...');
await verifySkipTestComments(flutterRoot);
printProgress('Licenses...');
await verifyNoMissingLicense(flutterRoot);
printProgress('Test imports...');
await verifyNoTestImports(flutterRoot);
printProgress('Bad imports (framework)...');
await verifyNoBadImportsInFlutter(flutterRoot);
printProgress('Bad imports (tools)...');
await verifyNoBadImportsInFlutterTools(flutterRoot);
printProgress('Internationalization...');
await verifyInternationalizations(flutterRoot, dart);
printProgress('Localization files of stocks app...');
await verifyStockAppLocalizations(flutterRoot);
printProgress('Integration test timeouts...');
await verifyIntegrationTestTimeouts(flutterRoot);
printProgress('null initialized debug fields...');
await verifyNullInitializedDebugExpensiveFields(flutterRoot);
printProgress('Taboo words...');
await verifyTabooDocumentation(flutterRoot);
printProgress('Lint Kotlin files...');
await lintKotlinFiles(flutterRoot);
printProgress('Lint generated Kotlin files from templates...');
await lintKotlinTemplatedFiles(flutterRoot);
printProgress('Package dependencies...');
await runCommand(flutter, <String>['update-packages'], workingDirectory: flutterRoot);
printProgress('Package Allowlist...');
await _checkConsumerDependencies();
printProgress('Dart analysis...');
final CommandResult dartAnalyzeResult = await _runFlutterAnalyze(
flutterRoot,
options: <String>['--flutter-repo', ...arguments],
);
printProgress('Check formatting of Dart files...');
await runCommand(dart, <String>[
'--enable-asserts',
path.join(flutterRoot, 'dev', 'tools', 'bin', 'format.dart'),
], workingDirectory: flutterRoot);
if (dartAnalyzeResult.exitCode == 0) {
final List<AnalyzeRule> rules = <AnalyzeRule>[
noDoubleClamp,
noStopwatches,
renderBoxIntrinsicCalculation,
protectPublicStateSubtypes,
];
final String ruleNames = rules.map((AnalyzeRule rule) => '\n * $rule').join();
printProgress('Analyzing code in the framework with the following rules:$ruleNames');
await analyzeWithRules(
flutterRoot,
rules,
includePaths: const <String>['packages/flutter/lib'],
excludePaths: const <String>['packages/flutter/lib/fix_data'],
);
final List<AnalyzeRule> testRules = <AnalyzeRule>[noStopwatches];
final String testRuleNames = testRules.map((AnalyzeRule rule) => '\n * $rule').join();
printProgress('Analyzing code in the test folder with the following rules:$testRuleNames');
await analyzeWithRules(flutterRoot, testRules, includePaths: <String>['packages/flutter/test']);
final List<AnalyzeRule> toolRules = <AnalyzeRule>[AvoidFutureCatchError()];
final String toolRuleNames = toolRules.map((AnalyzeRule rule) => '\n * $rule').join();
printProgress('Analyzing code in the tool with the following rules:$toolRuleNames');
await analyzeWithRules(
flutterRoot,
toolRules,
includePaths: const <String>['packages/flutter_tools/lib', 'packages/flutter_tools/test'],
);
} else {
printProgress(
'Skipped performing further analysis in the framework because "flutter analyze" finished with a non-zero exit code.',
);
}
printProgress('Executable allowlist...');
await _checkForNewExecutables();
printProgress('Dart analysis (with --watch)...');
await _runFlutterAnalyze(
flutterRoot,
failureMessage: 'Dart analyzer failed when --watch was used.',
options: <String>['--flutter-repo', '--watch', '--benchmark', ...arguments],
);
printProgress('Snippet code...');
await runCommand(dart, <String>[
'--enable-asserts',
path.join(flutterRoot, 'dev', 'bots', 'analyze_snippet_code.dart'),
'--verbose',
], workingDirectory: flutterRoot);
printProgress('Code sample link validation...');
await runCommand(dart, <String>[
'--enable-asserts',
path.join(flutterRoot, 'dev', 'bots', 'check_code_samples.dart'),
], workingDirectory: flutterRoot);
printProgress('Dart analysis (mega gallery)...');
final Directory outDir = Directory.systemTemp.createTempSync('flutter_mega_gallery.');
try {
await runCommand(dart, <String>[
path.join(flutterRoot, 'dev', 'tools', 'mega_gallery.dart'),
'--out',
outDir.path,
], workingDirectory: flutterRoot);
await _runFlutterAnalyze(
outDir.path,
failureMessage: 'Dart analyzer failed on mega_gallery benchmark.',
options: <String>['--watch', '--benchmark', ...arguments],
);
} finally {
outDir.deleteSync(recursive: true);
}
printProgress('Correct file names in gen_defaults.dart...');
await verifyTokenTemplatesUpdateCorrectFiles(flutterRoot);
printProgress('Material library files are up-to-date with token template files...');
await verifyMaterialFilesAreUpToDateWithTemplateFiles(flutterRoot, dart);
printProgress('Up to date integration test template files...');
await verifyIntegrationTestTemplateFiles(flutterRoot);
}
FeatureSet _parsingFeatureSet() => FeatureSet.latestLanguageVersion();
_Line _getLine(ParseStringResult parseResult, int offset) {
final int lineNumber = parseResult.lineInfo.getLocation(offset).lineNumber;
final String content = parseResult.content.substring(
parseResult.lineInfo.getOffsetOfLine(lineNumber - 1),
parseResult.lineInfo.getOffsetOfLine(lineNumber) - 1,
);
return _Line(lineNumber, content);
}
Future<void> verifyReleaseBranchState(String workringDirerctory) async {
final ProcessResult result = await Process.run(dart, <String>[
'bin/check_engine_version.dart',
], workingDirectory: path.join(workringDirerctory, 'dev', 'tools'));
if (result.exitCode != 0) {
foundError(<String>['${result.stderr}']);
}
}
Future<void> verifyTargetPlatform(String workingDirectory) async {
final File framework = File(
'$workingDirectory/packages/flutter/lib/src/foundation/platform.dart',
);
final Set<String> frameworkPlatforms = <String>{};
List<String> lines = framework.readAsLinesSync();
int index = 0;
while (true) {
if (index >= lines.length) {
foundError(<String>['${framework.path}: Can no longer find TargetPlatform enum.']);
return;
}
if (lines[index].startsWith('enum TargetPlatform {')) {
index += 1;
break;
}
index += 1;
}
while (true) {
if (index >= lines.length) {
foundError(<String>['${framework.path}: Could not find end of TargetPlatform enum.']);
return;
}
String line = lines[index].trim();
final int comment = line.indexOf('//');
if (comment >= 0) {
line = line.substring(0, comment);
}
if (line == '}') {
break;
}
if (line.isNotEmpty) {
if (line.endsWith(',')) {
frameworkPlatforms.add(line.substring(0, line.length - 1));
} else {
foundError(<String>[
'${framework.path}:$index: unparseable line when looking for TargetPlatform values',
]);
}
}
index += 1;
}
final File tool = File('$workingDirectory/packages/flutter_tools/lib/src/resident_runner.dart');
final Set<String> toolPlatforms = <String>{};
lines = tool.readAsLinesSync();
index = 0;
while (true) {
if (index >= lines.length) {
foundError(<String>['${tool.path}: Can no longer find nextPlatform logic.']);
return;
}
if (lines[index].trim().startsWith('const platforms = <String>[')) {
index += 1;
break;
}
index += 1;
}
while (true) {
if (index >= lines.length) {
foundError(<String>['${tool.path}: Could not find end of nextPlatform logic.']);
return;
}
final String line = lines[index].trim();
if (line.startsWith("'") && line.endsWith("',")) {
toolPlatforms.add(line.substring(1, line.length - 2));
} else if (line == '];') {
break;
} else {
foundError(<String>[
'${tool.path}:$index: unparseable line when looking for nextPlatform values',
]);
}
index += 1;
}
final Set<String> frameworkExtra = frameworkPlatforms.difference(toolPlatforms);
if (frameworkExtra.isNotEmpty) {
foundError(<String>[
'TargetPlatform has some extra values not found in the tool: ${frameworkExtra.join(", ")}',
]);
}
final Set<String> toolExtra = toolPlatforms.difference(frameworkPlatforms);
if (toolExtra.isNotEmpty) {
foundError(<String>[
'The nextPlatform logic in the tool has some extra values not found in TargetPlatform: ${toolExtra.join(", ")}',
]);
}
}
Future<void> verifyTokenTemplatesUpdateCorrectFiles(String workingDirectory) async {
final List<String> errors = <String>[];
String getMaterialDirPath(List<String> lines) {
final String line = lines.firstWhere((String line) => line.contains('String materialLib'));
final String relativePath = line.substring(line.indexOf("'") + 1, line.lastIndexOf("'"));
return path.join(workingDirectory, relativePath);
}
String getFileName(String line) {
const String materialLibString = r"'$materialLib/";
final String leftClamp = line.substring(
line.indexOf(materialLibString) + materialLibString.length,
);
return leftClamp.substring(0, leftClamp.indexOf("'"));
}
final String genDefaultsBinDir = '$workingDirectory/dev/tools/gen_defaults/bin';
final File file = File(path.join(genDefaultsBinDir, 'gen_defaults.dart'));
final List<String> lines = file.readAsLinesSync();
final String materialDirPath = getMaterialDirPath(lines);
bool atLeastOneTargetLineExists = false;
for (final String line in lines) {
if (line.contains('updateFile();')) {
atLeastOneTargetLineExists = true;
final String fileName = getFileName(line);
final String filePath = path.join(materialDirPath, fileName);
final File file = File(filePath);
if (!file.existsSync()) {
errors.add('file $filePath does not exist.');
}
}
}
assert(
atLeastOneTargetLineExists,
'No lines exist that this test expects to '
'verify. Check if the target file is correct or remove this test',
);
if (errors.isNotEmpty) {
final String s = errors.length > 1 ? 's' : '';
final String itThem = errors.length > 1 ? 'them' : 'it';
foundError(<String>[
...errors,
'${bold}Please correct the file name$s or remove $itThem from /dev/tools/gen_defaults/bin/gen_defaults.dart$reset',
]);
}
}
Future<void> verifyMaterialFilesAreUpToDateWithTemplateFiles(
String workingDirectory,
String dartExecutable,
) async {
final List<String> errors = <String>[];
const String beginGeneratedComment = '// BEGIN GENERATED TOKEN PROPERTIES';
String getMaterialDirPath(List<String> lines) {
final String line = lines.firstWhere((String line) => line.contains('String materialLib'));
final String relativePath = line.substring(line.indexOf("'") + 1, line.lastIndexOf("'"));
return path.join(workingDirectory, relativePath);
}
String getFileName(String line) {
const String materialLibString = r"'$materialLib/";
final String leftClamp = line.substring(
line.indexOf(materialLibString) + materialLibString.length,
);
return leftClamp.substring(0, leftClamp.indexOf("'"));
}
List<String> getGeneratedCode(List<String> lines) {
return lines.skipWhile((String line) => !line.contains(beginGeneratedComment)).toList();
}
final String genDefaultsBinDir = '$workingDirectory/dev/tools/gen_defaults/bin';
final File file = File(path.join(genDefaultsBinDir, 'gen_defaults.dart'));
final List<String> lines = file.readAsLinesSync();
final String materialDirPath = getMaterialDirPath(lines);
final Map<String, List<String>> beforeGeneratedCode = <String, List<String>>{};
final Map<String, List<String>> afterGeneratedCode = <String, List<String>>{};
for (final String line in lines) {
if (line.contains('updateFile();')) {
final String fileName = getFileName(line);
final String filePath = path.join(materialDirPath, fileName);
final File file = File(filePath);
beforeGeneratedCode[fileName] = getGeneratedCode(file.readAsLinesSync());
}
}
await runCommand(dartExecutable, <String>[
'--enable-asserts',
path.join('dev', 'tools', 'gen_defaults', 'bin', 'gen_defaults.dart'),
], workingDirectory: workingDirectory);
for (final String line in lines) {
if (line.contains('updateFile();')) {
final String fileName = getFileName(line);
final String filePath = path.join(materialDirPath, fileName);
final File file = File(filePath);
afterGeneratedCode[fileName] = getGeneratedCode(file.readAsLinesSync());
}
}
for (final String fileName in beforeGeneratedCode.keys) {
final List<String> before = beforeGeneratedCode[fileName]!;
final List<String> after = afterGeneratedCode[fileName]!;
if (!const IterableEquality<String>().equals(before, after)) {
errors.add('$fileName is not up-to-date with the token template file.');
}
}
if (errors.isNotEmpty) {
foundError(<String>[
...errors,
'${bold}See: https://github.com/flutter/flutter/blob/main/dev/tools/gen_defaults to update the token template files.$reset',
]);
}
}
Future<void> verifyToolTestsEndInTestDart(String workingDirectory) async {
final String toolsTestPath = path.join(workingDirectory, 'packages', 'flutter_tools', 'test');
final List<String> violations = <String>[];
final RegExp callsTestFunctionPattern = RegExp(
r'^ *(test\(.*\)|testUsingContext\(.*\)|testWithoutContext\(.*\))',
);
await for (final File file in _allFiles(toolsTestPath, 'dart', minimumMatches: 300)) {
final bool isValidTestFile = file.path.endsWith('_test.dart');
if (isValidTestFile) {
continue;
}
final bool isTestData = file.path.contains(r'test_data');
if (isTestData) {
continue;
}
final bool isInTestShard = file.path.contains(r'.shard/');
if (!isInTestShard) {
continue;
}
final bool callsTestFunction = file.readAsStringSync().contains(callsTestFunctionPattern);
if (!callsTestFunction) {
continue;
}
violations.add(file.path);
}
if (violations.isNotEmpty) {
foundError(<String>[
'${bold}Found flutter_tools tests that do not end in `_test.dart`; these will not be run by the test runner$reset',
...violations,
]);
}
}
Future<void> verifyNoSyncAsyncStar(String workingDirectory, {int minimumMatches = 2000}) async {
final RegExp syncPattern = RegExp(r'\s*?a?sync\*\s*?{');
final RegExp ignorePattern = RegExp(r'^\s*?// The following uses a?sync\* because:? ');
final RegExp commentPattern = RegExp(r'^\s*?//');
final List<String> errors = <String>[];
await for (final File file in _allFiles(
workingDirectory,
'dart',
minimumMatches: minimumMatches,
)) {
if (file.path.contains('test')) {
continue;
}
final List<String> lines = file.readAsLinesSync();
for (int index = 0; index < lines.length; index += 1) {
final String line = lines[index];
if (line.startsWith(commentPattern)) {
continue;
}
if (line.contains(syncPattern)) {
int lookBehindIndex = index - 1;
bool hasExplanation = false;
while (lookBehindIndex >= 0 && lines[lookBehindIndex].startsWith(commentPattern)) {
if (lines[lookBehindIndex].startsWith(ignorePattern)) {
hasExplanation = true;
break;
}
lookBehindIndex -= 1;
}
if (!hasExplanation) {
errors.add('${file.path}:$index: sync*/async* without an explanation.');
}
}
}
}
if (errors.isNotEmpty) {
foundError(<String>[
'${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',
...errors,
]);
}
}
final RegExp _findGoldenTestPattern = RegExp(r'matchesGoldenFile\(');
final RegExp _findGoldenDefinitionPattern = RegExp(r'matchesGoldenFile\(Object');
final RegExp _leadingComment = RegExp(r'//');
final RegExp _goldenTagPattern1 = RegExp(r'@Tags\(');
final RegExp _goldenTagPattern2 = RegExp(r"'reduced-test-set'");
const String _ignoreGoldenTag = '// flutter_ignore: golden_tag (see analyze.dart)';
const String _ignoreGoldenTagForFile = '// flutter_ignore_for_file: golden_tag (see analyze.dart)';
Future<void> verifyGoldenTags(String workingDirectory, {int minimumMatches = 2000}) async {
final List<String> errors = <String>[];
await for (final File file in _allFiles(
workingDirectory,
'dart',
minimumMatches: minimumMatches,
)) {
bool needsTag = false;
bool hasTagNotation = false;
bool hasReducedTag = false;
bool ignoreForFile = false;
final List<String> lines = file.readAsLinesSync();
for (final String line in lines) {
if (line.contains(_goldenTagPattern1)) {
hasTagNotation = true;
}
if (line.contains(_goldenTagPattern2)) {
hasReducedTag = true;
}
if (line.contains(_findGoldenTestPattern) &&
!line.contains(_findGoldenDefinitionPattern) &&
!line.contains(_leadingComment) &&
!line.contains(_ignoreGoldenTag)) {
needsTag = true;
}
if (line.contains(_ignoreGoldenTagForFile)) {
ignoreForFile = true;
}
if (ignoreForFile || (hasTagNotation && hasReducedTag)) {
break;
}
}
if (ignoreForFile || (hasTagNotation && hasReducedTag)) {
continue;
}
if (needsTag) {
if (!hasTagNotation) {
errors.add(
'${file.path}: Files containing golden tests must be tagged using '
"@Tags(<String>['reduced-test-set']) at the top of the file before import statements.",
);
} else if (!hasReducedTag) {
errors.add(
'${file.path}: Files containing golden tests must be tagged with '
"'reduced-test-set'.",
);
}
}
}
if (errors.isNotEmpty) {
foundError(<String>[
...errors,
'${bold}See: https://github.com/flutter/flutter/blob/main/docs/contributing/testing/Writing-a-golden-file-test-for-package-flutter.md$reset',
]);
}
}
class _DeprecationMessagesVisitor extends RecursiveAstVisitor<void> {
_DeprecationMessagesVisitor(this.parseResult, this.filePath);
final ParseStringResult parseResult;
final String filePath;
final List<String> errors = <String>[];
static const Pattern ignoreDeprecration =
'// flutter_ignore: deprecation_syntax (see analyze.dart)';
static final RegExp legacyDeprecation = RegExp(
r'// flutter_ignore: deprecation_syntax, https://github.com/flutter/flutter/issues/\d+',
);
static final RegExp deprecationVersionPattern = RegExp(
r'This feature was deprecated after v(?<major>\d+)\.(?<minor>\d+)\.(?<patch>\d+)(?<build>-\d+\.\d+\.pre)?\.$',
);
void _addErrorWithLineInfo(AstNode node, {required String error}) {
final int lineNumber = parseResult.lineInfo.getLocation(node.offset).lineNumber;
errors.add('$filePath:$lineNumber: $error');
}
@override
void visitAnnotation(Annotation node) {
super.visitAnnotation(node);
final bool shouldCheckAnnotation =
node.name.name == 'Deprecated' &&
!hasInlineIgnore(node, parseResult, ignoreDeprecration) &&
!hasInlineIgnore(node, parseResult, legacyDeprecation);
if (!shouldCheckAnnotation) {
return;
}
final NodeList<Expression>? arguments = node.arguments?.arguments;
if (arguments == null || arguments.length != 1) {
_addErrorWithLineInfo(
node,
error: 'A @Deprecation annotation must have exactly one deprecation notice String.',
);
return;
}
final Expression deprecationNotice = arguments.first;
if (deprecationNotice is! AdjacentStrings) {
_addErrorWithLineInfo(node, error: 'Deprecation notice must be an adjacent string.');
return;
}
final List<StringLiteral> strings = deprecationNotice.strings;
final Iterator<StringLiteral> deprecationMessageIterator = strings.iterator;
final bool isNotEmpty = deprecationMessageIterator.moveNext();
assert(isNotEmpty);
final [...List<StringLiteral> messageLiterals, StringLiteral versionLiteral] = strings;
final RegExpMatch? versionMatch = versionLiteral is SimpleStringLiteral
? deprecationVersionPattern.firstMatch(versionLiteral.value)
: null;
if (versionMatch == null) {
_addErrorWithLineInfo(
versionLiteral,
error:
'Deprecation notice must end with a line saying "This feature was deprecated after...".',
);
return;
}
final int major = int.parse(versionMatch.namedGroup('major')!);
final int minor = int.parse(versionMatch.namedGroup('minor')!);
final int patch = int.parse(versionMatch.namedGroup('patch')!);
final bool hasBuild = versionMatch.namedGroup('build') != null;
final bool specialBeta = major == 3 && minor == 1 && patch == 0;
if (!specialBeta && (major > 1 || (major == 1 && minor >= 20))) {
if (!hasBuild) {
_addErrorWithLineInfo(
versionLiteral,
error:
'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.',
);
return;
}
}
assert(messageLiterals.isNotEmpty);
for (final StringLiteral message in messageLiterals) {
if (message is! SingleStringLiteral) {
_addErrorWithLineInfo(
message,
error: 'Deprecation notice does not match required pattern.',
);
return;
}
if (!message.isSingleQuoted) {
_addErrorWithLineInfo(
message,
error:
'Deprecation notice does not match required pattern. You might have used double quotes (") for the string instead of single quotes (\').',
);
return;
}
}
final String fullExplanation = messageLiterals
.map((StringLiteral message) => message.stringValue ?? '')
.join()
.trimRight();
if (fullExplanation.isEmpty) {
_addErrorWithLineInfo(
messageLiterals.last,
error:
'Deprecation notice should be a grammatically correct sentence and end with a period; There might not be an explanatory message.',
);
return;
}
final String firstChar = String.fromCharCode(fullExplanation.runes.first);
if (firstChar.toUpperCase() != firstChar) {
_addErrorWithLineInfo(
messageLiterals.first,
error:
'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',
);
return;
}
if (!fullExplanation.endsWith('.') &&
!fullExplanation.endsWith('?') &&
!fullExplanation.endsWith('!')) {
_addErrorWithLineInfo(
messageLiterals.last,
error:
'Deprecation notice should be a grammatically correct sentence and end with a period; notice appears to be "$fullExplanation".',
);
return;
}
}
}
Future<void> verifyDeprecations(String workingDirectory, {int minimumMatches = 2000}) async {
final List<String> errors = <String>[];
await for (final File file in _allFiles(
workingDirectory,
'dart',
minimumMatches: minimumMatches,
)) {
final ParseStringResult parseResult = parseFile(
featureSet: _parsingFeatureSet(),
path: file.absolute.path,
);
final _DeprecationMessagesVisitor visitor = _DeprecationMessagesVisitor(parseResult, file.path);
visitor.visitCompilationUnit(parseResult.unit);
errors.addAll(visitor.errors);
}
if (errors.isNotEmpty) {
foundError(<String>[
...errors,
'${bold}See: https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md#handling-breaking-changes$reset',
]);
}
}
String _generateLicense(String prefix) {
return '${prefix}Copyright 2014 The Flutter Authors. All rights reserved.\n'
'${prefix}Use of this source code is governed by a BSD-style license that can be\n'
'${prefix}found in the LICENSE file.';
}
Future<void> verifyNoMissingLicense(String workingDirectory, {bool checkMinimums = true}) async {
final int? overrideMinimumMatches = checkMinimums ? null : 0;
await _verifyNoMissingLicenseForExtension(
workingDirectory,
'dart',
overrideMinimumMatches ?? 2000,
_generateLicense('// '),
);
await _verifyNoMissingLicenseForExtension(
workingDirectory,
'java',
overrideMinimumMatches ?? 1,
_generateLicense('// '),
);
await _verifyNoMissingLicenseForExtension(
workingDirectory,
'h',
overrideMinimumMatches ?? 30,
_generateLicense('// '),
);
await _verifyNoMissingLicenseForExtension(
workingDirectory,
'm',
overrideMinimumMatches ?? 30,
_generateLicense('// '),
);
await _verifyNoMissingLicenseForExtension(
workingDirectory,
'cc',
overrideMinimumMatches ?? 10,
_generateLicense('// '),
);
await _verifyNoMissingLicenseForExtension(
workingDirectory,
'cpp',
overrideMinimumMatches ?? 0,
_generateLicense('// '),
);
await _verifyNoMissingLicenseForExtension(
workingDirectory,
'swift',
overrideMinimumMatches ?? 10,
_generateLicense('// '),
);
await _verifyNoMissingLicenseForExtension(
workingDirectory,
'gradle',
overrideMinimumMatches ?? 80,
_generateLicense('// '),
);
await _verifyNoMissingLicenseForExtension(
workingDirectory,
'gn',
overrideMinimumMatches ?? 0,
_generateLicense('# '),
);
await _verifyNoMissingLicenseForExtension(
workingDirectory,
'sh',
overrideMinimumMatches ?? 1,
_generateLicense('# '),
header: r'#!/usr/bin/env bash\n',
);
await _verifyNoMissingLicenseForExtension(
workingDirectory,
'bat',
overrideMinimumMatches ?? 1,
_generateLicense('REM '),
header: r'@ECHO off\n',
);
await _verifyNoMissingLicenseForExtension(
workingDirectory,
'ps1',
overrideMinimumMatches ?? 1,
_generateLicense('# '),
);
await _verifyNoMissingLicenseForExtension(
workingDirectory,
'html',
overrideMinimumMatches ?? 1,
'<!-- ${_generateLicense('')} -->',
trailingBlank: false,
header: r'<!DOCTYPE HTML>\n',
);
await _verifyNoMissingLicenseForExtension(
workingDirectory,
'xml',
overrideMinimumMatches ?? 1,
'<!-- ${_generateLicense('')} -->',
header: r'(<\?xml version="1.0" encoding="utf-8"\?>\n)?',
);
await _verifyNoMissingLicenseForExtension(
workingDirectory,
'frag',
overrideMinimumMatches ?? 1,
_generateLicense('// '),
header: r'#version 320 es(\n)+',
);
}
Future<void> _verifyNoMissingLicenseForExtension(
String workingDirectory,
String extension,
int minimumMatches,
String license, {
bool trailingBlank = true,
String header = '',
}) async {
assert(!license.endsWith('\n'));
final String licensePattern = RegExp.escape('$license\n${trailingBlank ? '\n' : ''}');
final List<String> errors = <String>[];
await for (final File file in _allFiles(
workingDirectory,
extension,
minimumMatches: minimumMatches,
)) {
final String contents = file.readAsStringSync().replaceAll('\r\n', '\n');
if (contents.isEmpty) {
continue;
}
if (path.basename(file.path) == 'Package.swift') {
continue;
}
if (!contents.startsWith(RegExp(header + licensePattern))) {
errors.add(file.path);
}
}
if (errors.isNotEmpty) {
final String fileDoes = errors.length == 1 ? 'file does' : '${errors.length} files do';
foundError(<String>[
'${bold}The following $fileDoes not have the right license header for $extension files:$reset',
...errors.map<String>((String error) => ' $error'),
'The expected license header is:',
if (header.isNotEmpty) 'A header matching the regular expression "$header",',
if (header.isNotEmpty) 'followed by the following license text:',
license,
if (trailingBlank) '...followed by a blank line.',
]);
}
}
class _Line {
_Line(this.line, this.content);
final int line;
final String content;
}
Iterable<_Line> _getTestSkips(File file) {
final ParseStringResult parseResult = parseFile(
featureSet: _parsingFeatureSet(),
path: file.absolute.path,
);
final _TestSkipLinesVisitor<CompilationUnit> visitor = _TestSkipLinesVisitor<CompilationUnit>(
parseResult,
);
visitor.visitCompilationUnit(parseResult.unit);
return visitor.skips;
}
class _TestSkipLinesVisitor<T> extends RecursiveAstVisitor<T> {
_TestSkipLinesVisitor(this.parseResult) : skips = <_Line>{};
final ParseStringResult parseResult;
final Set<_Line> skips;
static bool isTestMethod(String name) {
return name.startsWith('test') || name == 'group' || name == 'expect';
}
static final Pattern _skipTestIntentionalPattern = RegExp(r'// .*[intended]');
static final Pattern _skipTestTrackingBugPattern = RegExp(
r'// .*https+?://github.com/.*/issues/\d+',
);
bool _hasValidJustificationComment(Label skipLabel) {
return hasInlineIgnore(skipLabel, parseResult, _skipTestIntentionalPattern) ||
hasInlineIgnore(skipLabel, parseResult, _skipTestTrackingBugPattern);
}
@override
T? visitMethodInvocation(MethodInvocation node) {
if (isTestMethod(node.methodName.toString())) {
for (final Expression argument in node.argumentList.arguments) {
if (argument is NamedExpression &&
argument.name.label.name == 'skip' &&
!_hasValidJustificationComment(argument.name)) {
skips.add(_getLine(parseResult, argument.beginToken.charOffset));
}
}
}
return super.visitMethodInvocation(node);
}
}
Future<void> verifySkipTestComments(String workingDirectory) async {
final List<String> errors = <String>[];
final Stream<File> testFiles = _allFiles(
workingDirectory,
'dart',
minimumMatches: 1500,
).where((File f) => f.path.endsWith('_test.dart'));
await for (final File file in testFiles) {
for (final _Line skip in _getTestSkips(file)) {
errors.add('${file.path}:${skip.line}: skip test without a justification comment.');
}
}
if (errors.isNotEmpty) {
foundError(<String>[
...errors,
'\n${bold}See: https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md#skipped-tests$reset',
]);
}
}
final RegExp _testImportPattern = RegExp(r'''import (['"])([^'"]+_test\.dart)\1''');
const Set<String> _exemptTestImports = <String>{
'package:flutter_test/flutter_test.dart',
'hit_test.dart',
'package:test_api/src/backend/live_test.dart',
'package:integration_test/integration_test.dart',
};
Future<void> verifyNoTestImports(String workingDirectory) async {
final List<String> errors = <String>[];
assert("// foo\nimport 'binding_test.dart' as binding;\n'".contains(_testImportPattern));
final List<File> dartFiles = await _allFiles(
path.join(workingDirectory, 'packages'),
'dart',
minimumMatches: 1500,
).toList();
for (final File file in dartFiles) {
for (final String line in file.readAsLinesSync()) {
final Match? match = _testImportPattern.firstMatch(line);
if (match != null && !_exemptTestImports.contains(match.group(2))) {
errors.add(file.path);
}
}
}
if (errors.isNotEmpty) {
foundError(<String>[
'${bold}The following file(s) import a test directly. Test utilities should be in their own file.$reset',
...errors,
]);
}
}
Future<void> verifyNoBadImportsInFlutter(String workingDirectory) async {
final List<String> errors = <String>[];
final String libPath = path.join(workingDirectory, 'packages', 'flutter', 'lib');
final String srcPath = path.join(workingDirectory, 'packages', 'flutter', 'lib', 'src');
final List<String> packages =
Directory(libPath)
.listSync()
.where(
(FileSystemEntity entity) => entity is File && path.extension(entity.path) == '.dart',
)
.map<String>((FileSystemEntity entity) => path.basenameWithoutExtension(entity.path))
.toList()
..sort();
final List<String> directories =
Directory(srcPath)
.listSync()
.whereType<Directory>()
.map<String>((Directory entity) => path.basename(entity.path))
.toList()
..sort();
if (!_listEquals<String>(packages, directories)) {
errors.add(
<String>[
'flutter/lib/*.dart does not match flutter/lib/src/*/:',
'These are the exported packages:',
...packages.map<String>((String path) => ' lib/$path.dart'),
'These are the directories:',
...directories.map<String>((String path) => ' lib/src/$path/'),
].join('\n'),
);
}
final Map<String, Set<String>> dependencyMap = <String, Set<String>>{};
for (final String directory in directories) {
dependencyMap[directory] = await _findFlutterDependencies(
path.join(srcPath, directory),
errors,
checkForMeta: directory != 'foundation',
);
}
assert(
dependencyMap['material']!.contains('widgets') &&
dependencyMap['widgets']!.contains('rendering') &&
dependencyMap['rendering']!.contains('painting'),
);
for (final String package in dependencyMap.keys) {
if (dependencyMap[package]!.contains(package)) {
errors.add(
'One of the files in the $yellow$package$reset package imports that package recursively.',
);
}
}
for (final String key in dependencyMap.keys) {
for (final String dependency in dependencyMap[key]!) {
if (dependencyMap[dependency] != null) {
continue;
}
final String validFilenames = dependencyMap.keys
.map((String name) => '$name.dart')
.join(', ');
errors.add(
'$key imported package:flutter/$dependency.dart '
'which is not one of the valid exports { $validFilenames }.\n'
'Consider changing $dependency.dart to one of them.',
);
}
}
for (final String package in dependencyMap.keys) {
final List<String>? loop = _deepSearch<String>(dependencyMap, package);
if (loop != null) {
errors.add('${yellow}Dependency loop:$reset ${loop.join(' depends on ')}');
}
}
if (errors.isNotEmpty) {
foundError(<String>[
if (errors.length == 1)
'${bold}An error was detected when looking at import dependencies within the Flutter package:$reset'
else
'${bold}Multiple errors were detected when looking at import dependencies within the Flutter package:$reset',
...errors,
]);
}
}
Future<void> verifyNoBadImportsInFlutterTools(String workingDirectory) async {
final List<String> errors = <String>[];
final List<File> files = await _allFiles(
path.join(workingDirectory, 'packages', 'flutter_tools', 'lib'),
'dart',
minimumMatches: 200,
).toList();
for (final File file in files) {
if (file.readAsStringSync().contains('package:flutter_tools/')) {
errors.add('$yellow${file.path}$reset imports flutter_tools.');
}
}
if (errors.isNotEmpty) {
foundError(<String>[
if (errors.length == 1)
'${bold}An error was detected when looking at import dependencies within the flutter_tools package:$reset'
else
'${bold}Multiple errors were detected when looking at import dependencies within the flutter_tools package:$reset',
...errors.map((String paragraph) => '$paragraph\n'),
]);
}
}
Future<void> verifyIntegrationTestTimeouts(String workingDirectory) async {
final List<String> errors = <String>[];
final String dev = path.join(workingDirectory, 'dev');
final List<File> files = await _allFiles(dev, 'dart', minimumMatches: 1)
.where(
(File file) =>
file.path.contains('test_driver') &&
(file.path.endsWith('_test.dart') || file.path.endsWith('util.dart')),
)
.toList();
for (final File file in files) {
final String contents = file.readAsStringSync();
final int testCount = ' test('.allMatches(contents).length;
final int timeoutNoneCount = 'timeout: Timeout.none'.allMatches(contents).length;
if (testCount != timeoutNoneCount) {
errors.add(
'$yellow${file.path}$reset has at least $testCount test(s) but only $timeoutNoneCount `Timeout.none`(s).',
);
}
}
if (errors.isNotEmpty) {
foundError(<String>[
if (errors.length == 1)
'${bold}An error was detected when looking at integration test timeouts:$reset'
else
'${bold}Multiple errors were detected when looking at integration test timeouts:$reset',
...errors.map((String paragraph) => '$paragraph\n'),
]);
}
}
Future<void> verifyInternationalizations(String workingDirectory, String dartExecutable) async {
final EvalResult materialGenResult = await _evalCommand(dartExecutable, <String>[
path.join('dev', 'tools', 'localization', 'bin', 'gen_localizations.dart'),
'--material',
'--remove-undefined',
], workingDirectory: workingDirectory);
final EvalResult cupertinoGenResult = await _evalCommand(dartExecutable, <String>[
path.join('dev', 'tools', 'localization', 'bin', 'gen_localizations.dart'),
'--cupertino',
'--remove-undefined',
], workingDirectory: workingDirectory);
final String materialLocalizationsFile = path.join(
workingDirectory,
'packages',
'flutter_localizations',
'lib',
'src',
'l10n',
'generated_material_localizations.dart',
);
final String cupertinoLocalizationsFile = path.join(
workingDirectory,
'packages',
'flutter_localizations',
'lib',
'src',
'l10n',
'generated_cupertino_localizations.dart',
);
final String expectedMaterialResult = await File(materialLocalizationsFile).readAsString();
final String expectedCupertinoResult = await File(cupertinoLocalizationsFile).readAsString();
if (materialGenResult.stdout.trim() != expectedMaterialResult.trim()) {
foundError(<String>[
'<<<<<<< $materialLocalizationsFile',
expectedMaterialResult.trim(),
'=======',
materialGenResult.stdout.trim(),
'>>>>>>> gen_localizations',
'The contents of $materialLocalizationsFile are different from that produced by gen_localizations.',
'',
'Did you forget to run gen_localizations.dart after updating a .arb file?',
]);
}
if (cupertinoGenResult.stdout.trim() != expectedCupertinoResult.trim()) {
foundError(<String>[
'<<<<<<< $cupertinoLocalizationsFile',
expectedCupertinoResult.trim(),
'=======',
cupertinoGenResult.stdout.trim(),
'>>>>>>> gen_localizations',
'The contents of $cupertinoLocalizationsFile are different from that produced by gen_localizations.',
'',
'Did you forget to run gen_localizations.dart after updating a .arb file?',
]);
}
}
Future<void> verifyStockAppLocalizations(String workingDirectory) async {
final Directory appRoot = Directory(
path.join(workingDirectory, 'dev', 'benchmarks', 'test_apps', 'stocks'),
);
if (!appRoot.existsSync()) {
foundError(<String>['Stocks app does not exist at expected location: ${appRoot.path}']);
}
final String flutterExecutable = path.join(
workingDirectory,
'bin',
'flutter${Platform.isWindows ? '.bat' : ''}',
);
await _evalCommand(flutterExecutable, const <String>['gen-l10n'], workingDirectory: appRoot.path);
final Directory i10nDirectory = Directory(path.join(appRoot.path, 'lib', 'i18n'));
if (!i10nDirectory.existsSync()) {
foundError(<String>[
'Localization files for stocks app not found at expected location: ${i10nDirectory.path}',
]);
}
final EvalResult result = await _evalCommand('git', <String>[
'diff',
'--name-only',
'--exit-code',
i10nDirectory.path,
], workingDirectory: workingDirectory);
if (result.exitCode == 1) {
foundError(<String>[
'The following localization files for the stocks app appear to be out of date:',
...(const LineSplitter().convert(result.stdout).map((String line) => ' * $line')),
'Run "flutter gen-l10n" in "${path.relative(appRoot.path, from: workingDirectory)}" to regenerate.',
]);
} else if (result.exitCode != 0) {
foundError(<String>[
'Failed to run "git diff" on localization files of stocks app:',
result.stderr,
]);
}
}
Future<void> verifyNoCheckedMode(String workingDirectory) async {
final String flutterPackages = path.join(workingDirectory, 'packages');
final List<File> files = await _allFiles(
flutterPackages,
'dart',
minimumMatches: 400,
).where((File file) => path.extension(file.path) == '.dart').toList();
final List<String> problems = <String>[];
for (final File file in files) {
int lineCount = 0;
for (final String line in file.readAsLinesSync()) {
if (line.toLowerCase().contains('checked mode')) {
problems.add(
'${file.path}:$lineCount uses deprecated "checked mode" instead of "debug mode".',
);
}
lineCount += 1;
}
}
if (problems.isNotEmpty) {
foundError(problems);
}
}
Future<void> verifyNoRuntimeTypeInToString(String workingDirectory) async {
final String flutterLib = path.join(workingDirectory, 'packages', 'flutter', 'lib');
final Set<String> excludedFiles = <String>{
path.join(flutterLib, 'src', 'foundation', 'object.dart'),
};
final List<File> files = await _allFiles(
flutterLib,
'dart',
minimumMatches: 400,
).where((File file) => !excludedFiles.contains(file.path)).toList();
final RegExp toStringRegExp = RegExp(r'^\s+String\s+to(.+?)?String(.+?)?\(\)\s+(\{|=>)');
final List<String> problems = <String>[];
for (final File file in files) {
final List<String> lines = file.readAsLinesSync();
for (int index = 0; index < lines.length; index++) {
if (toStringRegExp.hasMatch(lines[index])) {
final int sourceLine = index + 1;
bool checkForRuntimeType(String line) {
if (line.contains(r'$runtimeType') || line.contains('runtimeType.toString()')) {
problems.add('${file.path}:$sourceLine}: toString calls runtimeType.toString');
return true;
}
return false;
}
if (checkForRuntimeType(lines[index])) {
continue;
}
if (lines[index].contains('=>')) {
while (!lines[index].contains(';')) {
index++;
assert(index < lines.length, 'Source file $file has unterminated toString method.');
if (checkForRuntimeType(lines[index])) {
break;
}
}
} else {
int openBraceCount =
'{'.allMatches(lines[index]).length - '}'.allMatches(lines[index]).length;
while (!lines[index].contains('}') && openBraceCount > 0) {
index++;
assert(
index < lines.length,
'Source file $file has unbalanced braces in a toString method.',
);
if (checkForRuntimeType(lines[index])) {
break;
}
openBraceCount += '{'.allMatches(lines[index]).length;
openBraceCount -= '}'.allMatches(lines[index]).length;
}
}
}
}
}
if (problems.isNotEmpty) {
foundError(problems);
}
}
Future<void> verifyNoTrailingSpaces(String workingDirectory, {int minimumMatches = 4000}) async {
final List<File> files = await _allFiles(workingDirectory, null, minimumMatches: minimumMatches)
.where((File file) => path.basename(file.path) != 'serviceaccount.enc')
.where((File file) => path.basename(file.path) != 'Ahem.ttf')
.where((File file) => path.extension(file.path) != '.snapshot')
.where((File file) => path.extension(file.path) != '.png')
.where((File file) => path.extension(file.path) != '.jpg')
.where((File file) => path.extension(file.path) != '.ico')
.where((File file) => path.extension(file.path) != '.jar')
.where((File file) => path.extension(file.path) != '.swp')
.toList();
final List<String> problems = <String>[];
for (final File file in files) {
final List<String> lines = file.readAsLinesSync();
for (int index = 0; index < lines.length; index += 1) {
if (lines[index].endsWith(' ')) {
problems.add('${file.path}:${index + 1}: trailing U+0020 space character');
} else if (lines[index].endsWith('\t')) {
problems.add('${file.path}:${index + 1}: trailing U+0009 tab character');
}
}
if (lines.isNotEmpty && lines.last == '') {
problems.add('${file.path}:${lines.length}: trailing blank line');
}
}
if (problems.isNotEmpty) {
foundError(problems);
}
}
final RegExp _flowControlStatementWithoutSpace = RegExp(
r'(^|[ \t])(if|switch|for|do|while|catch)\(',
multiLine: true,
);
Future<void> verifySpacesAfterFlowControlStatements(
String workingDirectory, {
int minimumMatches = 4000,
}) async {
const Set<String> extensions = <String>{
'.java',
'.js',
'.kt',
'.swift',
'.c',
'.cc',
'.cpp',
'.h',
'.m',
};
final List<File> files = await _allFiles(
workingDirectory,
null,
minimumMatches: minimumMatches,
).where((File file) => extensions.contains(path.extension(file.path))).toList();
final List<String> problems = <String>[];
for (final File file in files) {
final List<String> lines = file.readAsLinesSync();
for (int index = 0; index < lines.length; index += 1) {
if (lines[index].contains(_flowControlStatementWithoutSpace)) {
problems.add('${file.path}:${index + 1}: no space after flow control statement');
}
}
}
if (problems.isNotEmpty) {
foundError(problems);
}
}
String _bullets(String value) => ' * $value';
Future<void> verifyIssueLinks(String workingDirectory) async {
const String issueLinkPrefix = 'https://github.com/flutter/flutter/issues/new';
const Set<String> stops = <String>{'\n', ' ', "'", '"', r'\', ')', '>'};
assert(
!stops.contains('.'),
);
const String kGiveTemplates =
'Prefer to provide a link either to $issueLinkPrefix/choose (the list of issue '
'templates) or to a specific template directly ($issueLinkPrefix?template=...).\n';
final Set<String> templateNames =
Directory(path.join(workingDirectory, '.github', 'ISSUE_TEMPLATE'))
.listSync()
.whereType<File>()
.where(
(File file) =>
path.extension(file.path) == '.md' || path.extension(file.path) == '.yml',
)
.map<String>((File file) => path.basename(file.path))
.toSet();
final String kTemplates =
'The available templates are:\n${templateNames.map(_bullets).join("\n")}';
final List<String> problems = <String>[];
final Set<String> suggestions = <String>{};
final List<File> files = await _gitFiles(workingDirectory);
for (final File file in files) {
if (path.basename(file.path).endsWith('_test.dart') ||
path.basename(file.path) == 'analyze.dart') {
continue;
}
final Uint8List bytes = file.readAsBytesSync();
final String contents = utf8.decode(bytes, allowMalformed: true);
int start = 0;
while ((start = contents.indexOf(issueLinkPrefix, start)) >= 0) {
int end = start + issueLinkPrefix.length;
while (end < contents.length && !stops.contains(contents[end])) {
end += 1;
}
final String url = contents.substring(start, end);
if (url == issueLinkPrefix) {
if (file.path != path.join(workingDirectory, 'dev', 'bots', 'analyze.dart')) {
problems.add('${file.path} contains a direct link to $issueLinkPrefix.');
suggestions.add(kGiveTemplates);
suggestions.add(kTemplates);
}
} else if (url.startsWith('$issueLinkPrefix?')) {
final Uri parsedUrl = Uri.parse(url);
final List<String>? templates = parsedUrl.queryParametersAll['template'];
if (templates == null) {
problems.add('${file.path} contains $url, which has no "template" argument specified.');
suggestions.add(kGiveTemplates);
suggestions.add(kTemplates);
} else if (templates.length != 1) {
problems.add(
'${file.path} contains $url, which has ${templates.length} templates specified.',
);
suggestions.add(kGiveTemplates);
suggestions.add(kTemplates);
} else if (!templateNames.contains(templates.single)) {
problems.add(
'${file.path} contains $url, which specifies a non-existent template ("${templates.single}").',
);
suggestions.add(kTemplates);
} else if (parsedUrl.queryParametersAll.keys.length > 1) {
problems.add(
'${file.path} contains $url, which the analyze.dart script is not sure how to handle.',
);
suggestions.add(
'Update analyze.dart to handle the URLs above, or change them to the expected pattern.',
);
}
} else if (url != '$issueLinkPrefix/choose') {
problems.add(
'${file.path} contains $url, which the analyze.dart script is not sure how to handle.',
);
suggestions.add(
'Update analyze.dart to handle the URLs above, or change them to the expected pattern.',
);
}
start = end;
}
}
assert(problems.isEmpty == suggestions.isEmpty);
if (problems.isNotEmpty) {
foundError(<String>[...problems, ...suggestions]);
}
}
Future<void> verifyRepositoryLinks(String workingDirectory) async {
const Set<String> stops = <String>{'\n', ' ', "'", '"', r'\', ')', '>'};
assert(
!stops.contains('.'),
);
const Set<String> repoExceptions = <String>{
'bdero/flutter-gpu-examples',
'chromium/chromium',
'clojure/clojure',
'dart-lang/test',
'dart-lang/webdev',
'eseidelGoogle/bezier_perf',
'flutter/devtools',
'flutter/flutter_gallery_assets',
'flutter/flutter-intellij',
'flutter/platform_tests',
'flutter/web_installers',
'glfw/glfw',
'GoogleCloudPlatform/artifact-registry-maven-tools',
'material-components/material-components-android',
'ninja-build/ninja',
'torvalds/linux',
'tpn/winsdk-10',
};
final RegExp pattern = RegExp(
r'^(https:\/\/(?:cs\.opensource\.google|github|raw\.githubusercontent|source\.chromium|([a-z0-9\-]+)\.googlesource)\.)',
);
final List<String> problems = <String>[];
final Set<String> suggestions = <String>{};
final List<File> files = await _allFiles(workingDirectory, null, minimumMatches: 10).toList();
for (final File file in files) {
final Uint8List bytes = file.readAsBytesSync();
final String contents = utf8.decode(bytes, allowMalformed: true);
int start = 0;
while ((start = contents.indexOf('https://', start)) >= 0) {
int end = start + 8;
while (end < contents.length && !stops.contains(contents[end])) {
end += 1;
}
final String url = contents.substring(start, end).replaceAll('\r', '');
if (pattern.hasMatch(url) && !repoExceptions.any(url.contains)) {
if (url.contains('master')) {
problems.add('${file.path} contains $url, which uses the banned "master" branch.');
suggestions.add(
'Change the URLs above to the expected pattern by '
'using the "main" branch if it exists, otherwise adding the '
'repository to the list of exceptions in analyze.dart.',
);
}
}
start = end;
}
}
assert(problems.isEmpty == suggestions.isEmpty);
if (problems.isNotEmpty) {
foundError(<String>[...problems, ...suggestions]);
}
}
@immutable
class Hash256 {
const Hash256(this.a, this.b, this.c, this.d);
factory Hash256.fromDigest(Digest digest) {
assert(digest.bytes.length == 32);
return Hash256(
digest.bytes[0] << 56 |
digest.bytes[1] << 48 |
digest.bytes[2] << 40 |
digest.bytes[3] << 32 |
digest.bytes[4] << 24 |
digest.bytes[5] << 16 |
digest.bytes[6] << 8 |
digest.bytes[7] << 0,
digest.bytes[8] << 56 |
digest.bytes[9] << 48 |
digest.bytes[10] << 40 |
digest.bytes[11] << 32 |
digest.bytes[12] << 24 |
digest.bytes[13] << 16 |
digest.bytes[14] << 8 |
digest.bytes[15] << 0,
digest.bytes[16] << 56 |
digest.bytes[17] << 48 |
digest.bytes[18] << 40 |
digest.bytes[19] << 32 |
digest.bytes[20] << 24 |
digest.bytes[21] << 16 |
digest.bytes[22] << 8 |
digest.bytes[23] << 0,
digest.bytes[24] << 56 |
digest.bytes[25] << 48 |
digest.bytes[26] << 40 |
digest.bytes[27] << 32 |
digest.bytes[28] << 24 |
digest.bytes[29] << 16 |
digest.bytes[30] << 8 |
digest.bytes[31] << 0,
);
}
final int a;
final int b;
final int c;
final int d;
@override
bool operator ==(Object other) {
if (other.runtimeType != runtimeType) {
return false;
}
return other is Hash256 && other.a == a && other.b == b && other.c == c && other.d == d;
}
@override
int get hashCode => Object.hash(a, b, c, d);
}
final Set<Hash256> _legacyBinaries = <Hash256>{
const Hash256(0x6A7C8F0D703E3682, 0x108F9662F8133022, 0x36240D3F8F638BB3, 0x91E32BFB96055FEF),
const Hash256(0xC7C0C0189145E4E3, 0x2A401C61C9BDC615, 0x754B0264E7AFAE24, 0xE834BB81049EAF81),
const Hash256(0xE14AA40904929BF3, 0x13FDED22CF7E7FFC, 0xBF1D1AAC4263B5EF, 0x1BE8BFCE650397AA),
const Hash256(0x4D470BF22D5C17D8, 0x4EDC5F82516D1BA8, 0xA1C09559CD761CEF, 0xB792F86D9F52B540),
const Hash256(0x3C34E1F298D0C9EA, 0x3455D46DB6B7759C, 0x8211A49E9EC6E44B, 0x635FC5C87DFB4180),
const Hash256(0x7770183009E91411, 0x2DE7D8EF1D235A6A, 0x30C5834424858E0D, 0x2F8253F6B8D31926),
const Hash256(0x5925DAB509451F9E, 0xCBB12CE8A625F9D4, 0xC104718EE20CAFF8, 0xB1B51032D1CD8946),
const Hash256(0xC4D9A284C12301D0, 0xF50E248EC53ED51A, 0x19A10147B774B233, 0x08399250B0D44C55),
const Hash256(0xBF97F9D3233F33E1, 0x389B09F7B8ADD537, 0x41300CB834D6C7A5, 0xCA32CBED363A4FB2),
const Hash256(0x285442F69A06B45D, 0x9D79DF80321815B5, 0x46473548A37B7881, 0x9B68959C7B8ED237),
const Hash256(0x2AB64AF8AC727EA9, 0x9C6AB9EAFF847F46, 0xFBF2A9A0A78A0ABC, 0xBF3180F3851645B4),
const Hash256(0x9DCA09F4E5ED5684, 0xD3C4DFF41F4E8B7C, 0xB864B438172D72BE, 0x069315FA362930F9),
const Hash256(0xD5AD04DE321EF37C, 0xACC5A7B960AFCCE7, 0x1BDCB96FA020C482, 0x49C1545DD1A0F497),
const Hash256(0x809ABFE75C440770, 0xC13C4E2E46D09603, 0xC22053E9D4E0E227, 0x5DCB9C1DCFBB2C75),
const Hash256(0x3DB08CB79E7B01B9, 0xE81F956E3A0AE101, 0x48D0FAFDE3EA7AA7, 0x0048DF905AA52CFD),
const Hash256(0x23C13D463F5DCA5C, 0x1F14A14934003601, 0xC29F1218FD461016, 0xD8A22CEF579A665F),
const Hash256(0x6DB7726530D71D3F, 0x52CB59793EB69131, 0x3BAA04796E129E1E, 0x043C0A58A1BFFD2F),
const Hash256(0xCEE565F5E6211656, 0x9B64980B209FD5CA, 0x4B3D3739011F5343, 0x250B33A1A2C6EB65),
const Hash256(0x93AE7D494FAD0FB3, 0x0CBF3AE746A39C4B, 0xC7A0F8BBF87FBB58, 0x7A3F3C01F3C5CE20),
const Hash256(0xB18BEBAAD1AD6724, 0xE48BCDF699BA3927, 0xDF3F258FEBE646A3, 0xAB5C62767C6BAB40),
const Hash256(0xF90D839A289ECADB, 0xF2B0B3400DA43EB8, 0x08B84908335AE4A0, 0x07457C4D5A56A57C),
const Hash256(0x592C2ABF84ADB2D3, 0x91AED8B634D3233E, 0x2C65369F06018DCD, 0x8A4B27BA755EDCBE),
const Hash256(0x75D9A0C034113CA8, 0xA1EC11C24B81F208, 0x6630A5A5C65C7D26, 0xA5DC03A1C0A4478C),
const Hash256(0xA896E65745557732, 0xC72BD4EE3A10782F, 0xE2AA95590B5AF659, 0x869E5808DB9C01C1),
const Hash256(0x3A69A8A1AAC5D9A8, 0x374492AF4B6D07A4, 0xCE637659EB24A784, 0x9C4DFB261D75C6A3),
const Hash256(0xD29D4E0AF9256DC9, 0x2D0A8F8810608A5E, 0x64A132AD8B397CA2, 0xC4DDC0B1C26A68C3),
const Hash256(0x3DCE99077602F704, 0x21C1C6B2A240BC9B, 0x83D64D86681D45F2, 0x154143310C980BE3),
const Hash256(0xBACCB205AE45f0B4, 0x21BE1657259B4943, 0xAC40C95094AB877F, 0x3BCBE12CD544DCBE),
const Hash256(0x7AB2525F4B86B65D, 0x3E4C70358A17E5A1, 0xAAF6F437f99CBCC0, 0x46DAD73d59BB9015),
const Hash256(0x03CFDE53C249475C, 0x277E8B8E90AC8A13, 0xE5FC13C358A94CCB, 0x67CA866C9862A0DD),
const Hash256(0x86A83E23A505EFCC, 0x39C358B699EDE12F, 0xC088EE516A1D0C73, 0xF3B5D74DDAD164B1),
const Hash256(0xD813B1A77320355E, 0xB68C485CD47D0F0F, 0x3C7E1910DCD46F08, 0x60A6401B8DC13647),
const Hash256(0x35AFA76BD5D6053F, 0xEE927436C78A8794, 0xA8BA5F5D9FC9653B, 0xE5B96567BB7215ED),
const Hash256(0x263CE9B4F1F69B43, 0xEBB08AE9FE8F80E7, 0x95647A59EF2C040B, 0xA8AEB246861A7DFF),
const Hash256(0x5E1A93C3653BAAFF, 0x1AAC6BCEB8DCBC2F, 0x2AE7D68ECB07E507, 0xCB1FA8354B28313A),
const Hash256(0xA5C77499151DDEC6, 0xDB40D0AC7321FD74, 0x0646C0C0F786743F, 0x8F3C3C408CAC5E8C),
const Hash256(0x33DE450980A2A16B, 0x1982AC7CDC1E7B01, 0x919E07E0289C2139, 0x65F85BCED8895FEF),
const Hash256(0xC3B8577F4A89BA03, 0x830944FB06C3566B, 0x4C99140A2CA52958, 0x089BFDC3079C59B7),
const Hash256(0xDEBC241D6F9C5767, 0x8980FDD46FA7ED0C, 0x5B8ACD26BCC5E1BC, 0x473C89B432D467AD),
const Hash256(0xBEFE5F7E82BF8B64, 0x148D869E3742004B, 0xF821A9F5A1BCDC00, 0x357D246DCC659DC2),
const Hash256(0xC385404341FF9EDD, 0x30FBE76F0EC99155, 0x8EA4F4AFE8CC0C60, 0x1CA3EDEF177E1DA8),
const Hash256(0x6BE5751A29F57A80, 0x36A4B31CC542C749, 0x984E49B22BD65CAA, 0x75AE8B2440848719),
const Hash256(0x9972A2264BFA8F8D, 0x964AFE799EADC1FA, 0x2247FB31097F994A, 0x1495DC32DF071793),
const Hash256(0x4C7CC9B09BEEDA24, 0x45F57D6967753910, 0x57D68E1A6B883D2C, 0x8C52701A74F1400F),
const Hash256(0x66DACAC1CFE4D349, 0xDBE994CB9125FFD7, 0x2D795CFC9CF9F739, 0xEDBB06CE25082E9C),
const Hash256(0x5188621015EBC327, 0xC9EF63AD76E60ECE, 0xE82BDC3E4ABF09E2, 0xEE0139FA7C0A2BE5),
const Hash256(0x27D2752D04EE9A6B, 0x78410E208F74A6CD, 0xC90D9E03B73B8C60, 0xD05F7D623E790487),
const Hash256(0xBB20556B2826CF85, 0xD5BAC73AA69C2AC3, 0x8E71DAD64F15B855, 0xB30CB73E0AF89307),
const Hash256(0x623820FA45CDB0AC, 0x808403E34AD6A53E, 0xA3E9FDAE83EE0931, 0xB020A3A4EF2CDDE7),
const Hash256(0xC6D631D1E107215E, 0xD4A58FEC5F3AA4B5, 0x0AE9724E07114C0C, 0x453E5D87C2CAD3B3),
const Hash256(0x4B6F58D1EB8723C6, 0xE717A0D09FEC8806, 0x90C6D1EF4F71836E, 0x618672827979B1A2),
const Hash256(0x0A1744CC7634D508, 0xE85DD793331F0C8A, 0x0B7C6DDFE0975D8F, 0x29E91C905BBB1BED),
const Hash256(0x24032FBD1E6519D6, 0x0BA93C0D5C189554, 0xF50EAE23756518A2, 0x3FABACF4BD5DAF08),
const Hash256(0xC17BAE6DF6BB234A, 0xE0AF4BEB0B805F12, 0x14E74EB7AA9A30F1, 0x5763689165DA7DDF),
const Hash256(0x74052AB5241D4418, 0x7085180608BC3114, 0xD12493C50CD8BBC7, 0x56DED186C37ACE84),
const Hash256(0xE37947332E3491CB, 0x82920EE86A086FEA, 0xE1E0A70B3700A7DA, 0xDCAFBDD8F40E2E19),
const Hash256(0xE608CDFC0C8579FB, 0xE38873BAAF7BC944, 0x9C9D2EE3685A4FAE, 0x671EF0C8BC41D17C),
const Hash256(0xBD53D86977DF9C54, 0xF605743C5ABA114C, 0x9D51D1A8BB917E1A, 0x14CAA26C335CAEBD),
const Hash256(0x64E4D02262C4F3D0, 0xBB4FDC21CD0A816C, 0x4CD2A0194E00FB0F, 0x1C3AE4142FAC0D15),
const Hash256(0x5BA3283A76918FC0, 0xEE127D0F22D7A0B6, 0xDF03DAED61669427, 0x93D89DDD87A08117),
const Hash256(0xCD7F26ED31DEA42A, 0x535D155EC6261499, 0x34E6738255FDB2C4, 0xBD8D4BDDE9A99B05),
const Hash256(0x3FA1225FC9A96A7E, 0xCD071BC42881AB0E, 0x7747EB72FFB72459, 0xA37971BBAD27EE24),
const Hash256(0xCD867001ACD7BBDB, 0x25CDFD452AE89FA2, 0x8C2DC980CAF55F48, 0x0B16C246CFB389BC),
const Hash256(0x848E9736E5C4915A, 0x7945BCF6B32FD56B, 0x1F1E7CDDD914352E, 0xC9681D38EF2A70DA),
const Hash256(0x654BA7D6C4E05CA0, 0x7799878884EF8F11, 0xA383E1F24CEF5568, 0x3C47604A966983C8),
const Hash256(0x743056FE7D83FE42, 0xA2990825B6AD0415, 0x1AF73D0D43B227AA, 0x07EBEA9B767381D9),
const Hash256(0xA7E1570812D119CF, 0xEF4B602EF28DD0A4, 0x100D066E66F5B9B9, 0x881765DC9303343B),
const Hash256(0xB4102839A1E41671, 0x62DACBDEFA471953, 0xB1EE89A0AB7594BE, 0x1D9AC1E67DC2B2CE),
const Hash256(0x70AC6571B593A967, 0xF1CBAEC9BC02D02D, 0x93AD766D8290ADE6, 0x840139BF9F219019),
const Hash256(0x5D87A78386DA2C43, 0xDDA8FEF2CA51438C, 0xE5A276FE28C6CF0A, 0xEBE89085B56665B6),
const Hash256(0x4D9F5E81F668DA44, 0xB20A77F8BF7BA2E1, 0xF384533B5AD58F07, 0xB3A2F93F8635CD96),
const Hash256(0x6E645DC9ED913AAD, 0xB50ED29EEB16830D, 0xB32CA12F39121DB9, 0xB7BC1449DDDBF8B8),
const Hash256(0xDEFAC77E08EC71EC, 0xA04CCA3C95D1FC33, 0xB9F26E1CB15CB051, 0x47DEFC79CDD7C158),
const Hash256(0x3CCE7450334675E2, 0xE3AABCA20B028993, 0x127BE82FE0EB3DFF, 0x8B027B3BAF052F2F),
const Hash256(0xDA957FD30C51B8D2, 0x7D74C2C918692DC4, 0xD3C5C99BB00F0D6B, 0x5EBB30395A6EDE82),
const Hash256(0xB5792CA06F48A431, 0xD4379ABA2160BD5D, 0xE92339FC64C6A0D3, 0x417AA359634CD905),
const Hash256(0x553E9C36DFF3E610, 0x6A608BDE822A0019, 0xDE4F1769B6FBDB97, 0xBC3C20E26B839F59),
const Hash256(0xA8100AE6AA1940D0, 0xB663BB31CD466142, 0xEBBDBD5187131B92, 0xD93818987832EB89),
const Hash256(0xE214B4A0FEEEC6FA, 0x8E7AA8CC9BFBEC40, 0xBCDAC2F2DEBC950F, 0x75AF8EBF02BCE459),
const Hash256(0x2D4F8D7A3DFEF9D3, 0xA0C66938E169AB58, 0x8C6BBBBD1973E34E, 0x03C428416D010182),
const Hash256(0xCD46C01BAFA3B243, 0xA6AA1645EEDDE481, 0x143AC8ABAB1A0996, 0x22CAA9D41F74649A),
const Hash256(0x40054377E1E084F4, 0x4F4410CE8F44C210, 0xABA945DFC55ED0EF, 0x23BDF9469E32F8D3),
const Hash256(0x7F9D27C7BC418284, 0x01214E21CA886B2F, 0x40D9DA2B31AE7754, 0x71D68375F9C8A824),
const Hash256(0xF416B0D8AC552EC8, 0x819D1F492D1AB5E6, 0xD4F20CF45DB47C22, 0x7BB431FEFB5B67B2),
const Hash256(0x576E489D788A13DB, 0xBF40E4A39A3DAB37, 0x15CCF0002032E79C, 0xD260C69B29E06646),
const Hash256(0x4C67221E25626CB2, 0x3F94E1F49D34E4CF, 0x3A9787A514924FC5, 0x9EF1E143E5BC5690),
const Hash256(0x1F19ADB4D80AFE8C, 0xE61899BA776B1A8D, 0xCA398C75F5F7050D, 0xFB0E72D7FBBBA69B),
const Hash256(0x67368CA1733E933A, 0xCA3BC56EF0695012, 0xE862C371AD4412F0, 0x3EC396039C609965),
const Hash256(0xAB2211A47BDA001D, 0x173A52FD9C75EBC7, 0xE158942FFA8243AD, 0x2A148871990D4297),
const Hash256(0xDEC70574DA46DFBB, 0xFA657A771F3E1FBD, 0xB265CFC6B2AA5FE3, 0x93BA4F325D1520BA),
const Hash256(0x63D2ABD0041C3E3B, 0x4B52AD8D382353B5, 0x3C51C6785E76CE56, 0xED9DACAD2D2E31C4),
};
Future<void> verifyNoBinaries(String workingDirectory, {Set<Hash256>? legacyBinaries}) async {
assert(
_legacyBinaries
.expand<int>((Hash256 hash) => <int>[hash.a, hash.b, hash.c, hash.d])
.reduce((int value, int element) => value ^ element) ==
0x606B51C908B40BFA,
);
legacyBinaries ??= _legacyBinaries;
if (!Platform.isWindows) {
final List<File> files = await _gitFiles(workingDirectory);
final List<String> problems = <String>[];
for (final File file in files) {
final Uint8List bytes = file.readAsBytesSync();
try {
utf8.decode(bytes);
} on FormatException catch (error) {
final Digest digest = sha256.convert(bytes);
if (!legacyBinaries.contains(Hash256.fromDigest(digest))) {
problems.add('${file.path}:${error.offset}: file is not valid UTF-8');
}
}
}
if (problems.isNotEmpty) {
foundError(<String>[
...problems,
'All files in this repository must be UTF-8. In particular, images and other binaries',
'must not be checked into this repository. This is because we are very sensitive to the',
'size of the repository as it is distributed to all our developers. If you have a binary',
'to which you need access, you should consider how to fetch it from another repository;',
'for example, the "assets-for-api-docs" repository is used for images in API docs.',
'To add assets to flutter_tools templates, see the instructions in the wiki:',
'https://github.com/flutter/flutter/blob/main/docs/tool/Managing-template-image-assets.md',
]);
}
}
}
bool _listEquals<T>(List<T> a, List<T> b) {
if (a.length != b.length) {
return false;
}
for (int index = 0; index < a.length; index += 1) {
if (a[index] != b[index]) {
return false;
}
}
return true;
}
Future<List<File>> _gitFiles(String workingDirectory, {bool runSilently = true}) async {
final EvalResult evalResult = await _evalCommand(
'git',
<String>['ls-files', '-z'],
workingDirectory: workingDirectory,
runSilently: runSilently,
);
if (evalResult.exitCode != 0) {
foundError(<String>[
'git ls-files failed with exit code ${evalResult.exitCode}',
'${bold}stdout:$reset',
evalResult.stdout,
'${bold}stderr:$reset',
evalResult.stderr,
]);
}
final List<String> filenames = evalResult.stdout.split('\x00');
assert(filenames.last.isEmpty);
filenames.removeLast();
return filenames
.where((String filename) => !filename.startsWith('engine/'))
.map<File>((String filename) => File(path.join(workingDirectory, filename)))
.toList();
}
Stream<File> _allFiles(
String workingDirectory,
String? extension, {
required int minimumMatches,
}) async* {
final Set<String> gitFileNamesSet = <String>{};
gitFileNamesSet.addAll(
(await _gitFiles(workingDirectory)).map((File f) => path.canonicalize(f.absolute.path)),
);
assert(
extension == null || !extension.startsWith('.'),
'Extension argument should not start with a period.',
);
final Set<FileSystemEntity> pending = <FileSystemEntity>{Directory(workingDirectory)};
int matches = 0;
while (pending.isNotEmpty) {
final FileSystemEntity entity = pending.first;
pending.remove(entity);
if (path.extension(entity.path) == '.tmpl') {
continue;
}
if (entity is File) {
if (!gitFileNamesSet.contains(path.canonicalize(entity.absolute.path))) {
continue;
}
if (_isGeneratedPluginRegistrant(entity)) {
continue;
}
switch (path.basename(entity.path)) {
case 'flutter_export_environment.sh' || 'gradlew.bat' || '.DS_Store':
continue;
}
if (extension == null || path.extension(entity.path) == '.$extension') {
matches += 1;
yield entity;
}
} else if (entity is Directory) {
if (File(path.join(entity.path, '.dartignore')).existsSync()) {
continue;
}
switch (path.basename(entity.path)) {
case '.git' || '.idea' || '.gradle' || '.dart_tool' || 'build':
continue;
}
pending.addAll(entity.listSync());
}
}
assert(
matches >= minimumMatches,
'Expected to find at least $minimumMatches files with extension ".$extension" in "$workingDirectory", but only found $matches.',
);
}
class EvalResult {
EvalResult({required this.stdout, required this.stderr, this.exitCode = 0});
final String stdout;
final String stderr;
final int exitCode;
}
Future<EvalResult> _evalCommand(
String executable,
List<String> arguments, {
required String workingDirectory,
Map<String, String>? environment,
bool allowNonZeroExit = false,
bool runSilently = false,
}) async {
final String commandDescription =
'${path.relative(executable, from: workingDirectory)} ${arguments.join(' ')}';
final String relativeWorkingDir = path.relative(workingDirectory);
if (!runSilently) {
print('RUNNING: cd $cyan$relativeWorkingDir$reset; $green$commandDescription$reset');
}
final Stopwatch time = Stopwatch()..start();
final Process process = await Process.start(
executable,
arguments,
workingDirectory: workingDirectory,
environment: environment,
);
final Future<List<List<int>>> savedStdout = process.stdout.toList();
final Future<List<List<int>>> savedStderr = process.stderr.toList();
final int exitCode = await process.exitCode;
final EvalResult result = EvalResult(
stdout: utf8.decode((await savedStdout).expand<int>((List<int> ints) => ints).toList()),
stderr: utf8.decode((await savedStderr).expand<int>((List<int> ints) => ints).toList()),
exitCode: exitCode,
);
if (!runSilently) {
print(
'ELAPSED TIME: $bold${prettyPrintDuration(time.elapsed)}$reset for $commandDescription in $relativeWorkingDir',
);
}
if (exitCode != 0 && !allowNonZeroExit) {
foundError(<String>[
result.stderr,
'${bold}ERROR:$red Last command exited with $exitCode.$reset',
'${bold}Command:$red $commandDescription$reset',
'${bold}Relative working directory:$red $relativeWorkingDir$reset',
]);
}
return result;
}
Future<void> _checkConsumerDependencies() async {
const List<String> kCorePackages = <String>[
'flutter',
'flutter_test',
'flutter_driver',
'flutter_localizations',
'integration_test',
'fuchsia_remote_debug_protocol',
];
final Set<String> dependencies = <String>{};
for (final String package in kCorePackages) {
final ProcessResult result = await Process.run(flutter, <String>[
'pub',
'deps',
'--json',
'--directory=${path.join(flutterRoot, 'packages', package)}',
]);
if (result.exitCode != 0) {
foundError(<String>[result.stdout.toString(), result.stderr.toString()]);
return;
}
final Map<String, Object?> rawJson =
json.decode(result.stdout as String) as Map<String, Object?>;
final Map<String, Map<String, Object?>> dependencyTree = <String, Map<String, Object?>>{
for (final Map<String, Object?> package
in (rawJson['packages']! as List<Object?>).cast<Map<String, Object?>>())
package['name']! as String: package,
};
final List<Map<String, Object?>> workset = <Map<String, Object?>>[];
workset.add(dependencyTree[package]!);
while (workset.isNotEmpty) {
final Map<String, Object?> currentPackage = workset.removeLast();
if (currentPackage['kind'] == 'dev') {
continue;
}
dependencies.add(currentPackage['name']! as String);
final List<String> currentDependencies =
(currentPackage['directDependencies']! as List<Object?>).cast<String>();
for (final String dependency in currentDependencies) {
if (!dependencies.contains(dependency)) {
workset.add(dependencyTree[dependency]!);
}
}
}
}
final Set<String> removed = kCorePackageAllowList.difference(dependencies);
final Set<String> added = dependencies.difference(kCorePackageAllowList);
String plural(int n, String s, String p) => n == 1 ? s : p;
if (added.isNotEmpty) {
foundError(<String>[
'The transitive closure of package dependencies contains ${plural(added.length, "a non-allowlisted package", "non-allowlisted packages")}:',
' ${added.join(', ')}',
'We strongly desire to keep the number of dependencies to a minimum and',
'therefore would much prefer not to add new dependencies.',
'See dev/bots/allowlist.dart for instructions on how to update the package',
'allowlist if you nonetheless believe this is a necessary addition.',
]);
}
if (removed.isNotEmpty) {
foundError(<String>[
'Excellent news! ${plural(removed.length, "A package dependency has been removed!", "Multiple package dependencies have been removed!")}',
' ${removed.join(', ')}',
'To make sure we do not accidentally add ${plural(removed.length, "this dependency", "these dependencies")} back in the future,',
'please remove ${plural(removed.length, "this", "these")} packages from the allow-list in dev/bots/allowlist.dart.',
'Thanks!',
]);
}
}
class _DebugOnlyFieldVisitor extends RecursiveAstVisitor<void> {
_DebugOnlyFieldVisitor(this.parseResult);
final ParseStringResult parseResult;
final List<AstNode> errors = <AstNode>[];
static const String _kDebugOnlyAnnotation = '_debugOnly';
static final RegExp _nullInitializedField = RegExp(r'kDebugMode \? [\w<> ,{}()]+ : null;');
@override
void visitFieldDeclaration(FieldDeclaration node) {
super.visitFieldDeclaration(node);
if (node.metadata.any(
(Annotation annotation) => annotation.name.name == _kDebugOnlyAnnotation,
)) {
if (!node.toSource().contains(_nullInitializedField)) {
errors.add(node.fields);
}
}
}
}
Future<void> verifyNullInitializedDebugExpensiveFields(
String workingDirectory, {
int minimumMatches = 400,
}) async {
final String flutterLib = path.join(workingDirectory, 'packages', 'flutter', 'lib');
final List<File> files = await _allFiles(
flutterLib,
'dart',
minimumMatches: minimumMatches,
).toList();
final List<String> errors = <String>[];
for (final File file in files) {
final ParseStringResult parsedFile = parseFile(
featureSet: _parsingFeatureSet(),
path: file.absolute.path,
);
final _DebugOnlyFieldVisitor visitor = _DebugOnlyFieldVisitor(parsedFile);
visitor.visitCompilationUnit(parsedFile.unit);
for (final AstNode badNode in visitor.errors) {
errors.add(
'${file.path}:${parsedFile.lineInfo.getLocation(badNode.offset).lineNumber}: fields annotated with @_debugOnly must null initialize.',
);
}
}
if (errors.isNotEmpty) {
foundError(<String>[
...errors,
'',
'$bold${red}Fields annotated with @_debugOnly must null initialize,$reset',
'to ensure both the field and initializer are removed from profile/release mode.',
'These fields should be written as:',
'field = kDebugMode ? <DebugValue> : null;',
]);
}
}
final RegExp tabooPattern = RegExp(r'^ *///.*\b(simply|note:|note that)\b', caseSensitive: false);
Future<void> verifyTabooDocumentation(String workingDirectory, {int minimumMatches = 100}) async {
final List<String> errors = <String>[];
await for (final File file in _allFiles(
workingDirectory,
'dart',
minimumMatches: minimumMatches,
)) {
final List<String> lines = file.readAsLinesSync();
for (int index = 0; index < lines.length; index += 1) {
final String line = lines[index];
final Match? match = tabooPattern.firstMatch(line);
if (match != null) {
errors.add(
'${file.path}:${index + 1}: Found use of the taboo word "${match.group(1)}" in documentation string.',
);
}
}
}
if (errors.isNotEmpty) {
foundError(<String>[
...errors,
'',
'${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',
'${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',
'${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',
]);
}
}
final Map<String, String> _kKotlinTemplateKeys = <String, String>{
'androidIdentifier': 'dummyPackage',
'pluginClass': 'PluginClass',
'projectName': 'dummy',
'agpVersion': '0.0.0.1',
'kotlinVersion': '0.0.0.1',
};
final String _kKotlinTemplateRelativePath = path.join('packages', 'flutter_tools', 'templates');
const List<String> _kKotlinExtList = <String>['.kt.tmpl', '.kts.tmpl'];
const String _kKotlinTmplExt = '.tmpl';
final RegExp _kKotlinTemplatePattern = RegExp(r'{{(.*?)}}');
Future<void> lintKotlinTemplatedFiles(String workingDirectory) async {
final String templatePath = path.join(workingDirectory, _kKotlinTemplateRelativePath);
final Iterable<File> files = Directory(templatePath)
.listSync(recursive: true)
.toList()
.whereType<File>()
.where((File file) => _kKotlinExtList.contains(path.extension(file.path, 2)));
if (files.isEmpty) {
foundError(<String>['No Kotlin template files found']);
return;
}
final Directory tempDir = Directory.systemTemp.createTempSync('template_output');
for (final File templateFile in files) {
final String inputContent = await templateFile.readAsString();
final String modifiedContent = inputContent.replaceAllMapped(
_kKotlinTemplatePattern,
(Match match) => _kKotlinTemplateKeys[match[1]] ?? 'dummy',
);
String outputFilename = path.basename(templateFile.path);
outputFilename = outputFilename.substring(
0,
outputFilename.length - _kKotlinTmplExt.length,
);
outputFilename = outputFilename.substring(0, 1).toUpperCase() + outputFilename.substring(1);
final String relativePath = path.dirname(path.relative(templateFile.path, from: templatePath));
final String outputDir = path.join(tempDir.path, relativePath);
await Directory(outputDir).create(recursive: true);
final String outputFile = path.join(outputDir, outputFilename);
final File output = File(outputFile);
await output.writeAsString(modifiedContent);
}
return lintKotlinFiles(tempDir.path).whenComplete(() {
tempDir.deleteSync(recursive: true);
});
}
Future<void> lintKotlinFiles(String workingDirectory) async {
const String baselineRelativePath = 'dev/bots/test/analyze-test-input/ktlint-baseline.xml';
const String editorConfigRelativePath = 'dev/bots/test/analyze-test-input/.editorconfig';
final EvalResult lintResult = await _evalCommand('ktlint', <String>[
'--baseline=$flutterRoot/$baselineRelativePath',
'--editorconfig=$flutterRoot/$editorConfigRelativePath',
], workingDirectory: workingDirectory);
if (lintResult.exitCode != 0) {
final String errorMessage =
'Found lint violations in Kotlin files:\n ${lintResult.stdout}\n\n'
'To reproduce this lint locally:\n'
'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'
'2. Download that version from https://chrome-infra-packages.appspot.com/p/flutter/ktlint/linux-amd64/+/<version_tag>\n'
'3. From the repository root, run `<path_to_ktlint>/ktlint --editorconfig=$editorConfigRelativePath --baseline=$baselineRelativePath`\n'
'Alternatively, if you use Android Studio, follow the docs at docs/platforms/android/Kotlin-android-studio-formatting.md to enable auto formatting.';
foundError(<String>[errorMessage]);
}
}
const List<String> _kIgnoreList = <String>['Runner.rc.tmpl', 'flutter_window.cpp'];
final String _kIntegrationTestsRelativePath = path.join('dev', 'integration_tests');
final String _kTemplateRelativePath = path.join(
'packages',
'flutter_tools',
'templates',
'app',
'windows.tmpl',
'runner',
);
final String _kWindowsRunnerSubPath = path.join('windows', 'runner');
const String _kProjectNameKey = '{{projectName}}';
const String _kTmplExt = '.tmpl';
String _getFlutterLicense() {
return '// Copyright 2014 The Flutter Authors. All rights reserved.\n'
'// Use of this source code is governed by a BSD-style license that can be\n'
'// found in the LICENSE file.\n'
'\n';
}
String _removeLicenseIfPresent(String fileContents, String license) {
if (fileContents.startsWith(license)) {
return fileContents.substring(license.length);
}
return fileContents;
}
Future<void> verifyIntegrationTestTemplateFiles(String flutterRoot) async {
final List<String> errors = <String>[];
final String license = _getFlutterLicense();
final String integrationTestsPath = path.join(flutterRoot, _kIntegrationTestsRelativePath);
final String templatePath = path.join(flutterRoot, _kTemplateRelativePath);
final Iterable<Directory> subDirs = Directory(
integrationTestsPath,
).listSync().toList().whereType<Directory>();
for (final Directory testPath in subDirs) {
final String projectName = path.basename(testPath.path);
final String runnerPath = path.join(testPath.path, _kWindowsRunnerSubPath);
final Directory runner = Directory(runnerPath);
if (!runner.existsSync()) {
continue;
}
final Iterable<File> files = Directory(templatePath).listSync().toList().whereType<File>();
for (final File templateFile in files) {
final String fileName = path.basename(templateFile.path);
if (_kIgnoreList.contains(fileName)) {
continue;
}
String templateFileContents = templateFile.readAsLinesSync().join('\n');
String appFilePath = path.join(runnerPath, fileName);
if (fileName.endsWith(_kTmplExt)) {
appFilePath = appFilePath.substring(
0,
appFilePath.length - _kTmplExt.length,
);
templateFileContents = templateFileContents.replaceAll(
_kProjectNameKey,
projectName,
);
}
String appFileContents = File(appFilePath).readAsLinesSync().join('\n');
appFileContents = _removeLicenseIfPresent(appFileContents, license);
if (appFileContents != templateFileContents) {
int indexOfDifference;
for (
indexOfDifference = 0;
indexOfDifference < appFileContents.length;
indexOfDifference++
) {
if (indexOfDifference >= templateFileContents.length ||
templateFileContents.codeUnitAt(indexOfDifference) !=
appFileContents.codeUnitAt(indexOfDifference)) {
break;
}
}
final String error =
'''
Error: file $fileName mismatched for integration test $testPath
Verify the integration test has been migrated to the latest app template.
=====$appFilePath======
$appFileContents
=====${templateFile.path}======
$templateFileContents
==========
Diff at character #$indexOfDifference
''';
errors.add(error);
}
}
}
if (errors.isNotEmpty) {
foundError(errors);
}
}
Future<CommandResult> _runFlutterAnalyze(
String workingDirectory, {
List<String> options = const <String>[],
String? failureMessage,
}) async {
return runCommand(
flutter,
<String>['analyze', ...options],
workingDirectory: workingDirectory,
failureMessage: failureMessage,
);
}
const Set<String> kExecutableAllowlist = <String>{
'bin/dart',
'bin/flutter',
'bin/flutter-dev',
'bin/internal/last_engine_commit.sh',
'bin/internal/update_dart_sdk.sh',
'bin/internal/update_engine_version.sh',
'bin/internal/content_aware_hash.sh',
'dev/bots/codelabs_build_test.sh',
'dev/bots/docs.sh',
'dev/checks',
'dev/customer_testing/ci.sh',
'dev/integration_tests/flutter_gallery/tool/run_instrumentation_test.sh',
'dev/integration_tests/ios_add2app_life_cycle/build_and_test.sh',
'dev/integration_tests/deferred_components_test/download_assets.sh',
'dev/integration_tests/deferred_components_test/run_release_test.sh',
'dev/packages_autoroller/run',
'dev/tools/gen_keycodes/bin/gen_keycodes',
'dev/tools/repackage_gradle_wrapper.sh',
'dev/tools/bin/engine_hash.sh',
'dev/tools/format.sh',
'dev/tools/test/mock_git.sh',
'packages/flutter_tools/bin/macos_assemble.sh',
'packages/flutter_tools/bin/tool_backend.sh',
'packages/flutter_tools/bin/xcode_backend.sh',
};
Future<void> _checkForNewExecutables() async {
const int executableBitMask = 0x49;
final List<File> files = await _gitFiles(flutterRoot);
final List<String> errors = <String>[];
for (final File file in files) {
final String relativePath = path.relative(file.path, from: flutterRoot);
final FileStat stat = file.statSync();
final bool isExecutable = stat.mode & executableBitMask != 0x0;
if (isExecutable && !kExecutableAllowlist.contains(relativePath)) {
errors.add('$relativePath is executable: ${(stat.mode & 0x1FF).toRadixString(2)}');
}
}
if (errors.isNotEmpty) {
throw Exception(
'${errors.join('\n')}\n'
'found ${errors.length} unexpected executable file'
'${errors.length == 1 ? '' : 's'}! If this was intended, you '
'must add this file to kExecutableAllowlist in dev/bots/analyze.dart',
);
}
}
final RegExp _importPattern = RegExp(r'''^\s*import (['"])package:flutter/([^.]+)\.dart\1''');
final RegExp _importMetaPattern = RegExp(r'''^\s*import (['"])package:meta/meta\.dart\1''');
Future<Set<String>> _findFlutterDependencies(
String srcPath,
List<String> errors, {
bool checkForMeta = false,
}) async {
return _allFiles(srcPath, 'dart', minimumMatches: 1)
.map<Set<String>>((File file) {
final Set<String> result = <String>{};
for (final String line in file.readAsLinesSync()) {
Match? match = _importPattern.firstMatch(line);
if (match != null) {
result.add(match.group(2)!);
}
if (checkForMeta) {
match = _importMetaPattern.firstMatch(line);
if (match != null) {
errors.add(
'${file.path}\nThis package imports the ${yellow}meta$reset package.\n'
'You should instead import the "foundation.dart" library.',
);
}
}
}
return result;
})
.reduce((Set<String>? value, Set<String> element) {
value ??= <String>{};
value.addAll(element);
return value;
});
}
List<T>? _deepSearch<T>(Map<T, Set<T>> map, T start, [Set<T>? seen]) {
if (map[start] == null) {
return null;
}
for (final T key in map[start]!) {
if (key == start) {
continue;
}
if (seen != null && seen.contains(key)) {
return <T>[start, key];
}
final List<T>? result = _deepSearch<T>(map, key, <T>{
if (seen == null) start else ...seen,
key,
});
if (result != null) {
result.insert(0, start);
if (result.first == result.last) {
return result;
}
}
}
return null;
}
bool _isGeneratedPluginRegistrant(File file) {
final String filename = path.basename(file.path);
return !file.path.contains('.pub-cache') &&
(filename == 'GeneratedPluginRegistrant.java' ||
filename == 'GeneratedPluginRegistrant.swift' ||
filename == 'GeneratedPluginRegistrant.h' ||
filename == 'GeneratedPluginRegistrant.m' ||
filename == 'generated_plugin_registrant.dart' ||
filename == 'generated_plugin_registrant.h' ||
filename == 'generated_plugin_registrant.cc');
}