mirror of
https://github.com/flutter/flutter.git
synced 2026-01-20 12:42:31 +08:00
WIP Commits separated as follows: - Update lints in analysis_options files - Run `dart fix --apply` - Clean up leftover analysis issues - Run `dart format .` in the right places. Local analysis and testing passes. Checking CI now. Part of https://github.com/flutter/flutter/issues/178827 - Adoption of flutter_lints in examples/api coming in a separate change (cc @loic-sharma) ## Pre-launch Checklist - [ ] I read the [Contributor Guide] and followed the process outlined there for submitting PRs. - [ ] I read the [Tree Hygiene] wiki page, which explains my responsibilities. - [ ] I read and followed the [Flutter Style Guide], including [Features we expect every widget to implement]. - [ ] I signed the [CLA]. - [ ] I listed at least one issue that this PR fixes in the description above. - [ ] I updated/added relevant documentation (doc comments with `///`). - [ ] I added new tests to check the change I am making, or this PR is [test-exempt]. - [ ] I followed the [breaking change policy] and added [Data Driven Fixes] where supported. - [ ] All existing and new tests are passing. If you need help, consider asking for advice on the #hackers-new channel on [Discord]. **Note**: The Flutter team is currently trialing the use of [Gemini Code Assist for GitHub](https://developers.google.com/gemini-code-assist/docs/review-github-code). Comments from the `gemini-code-assist` bot should not be taken as authoritative feedback from the Flutter team. If you find its comments useful you can update your code accordingly, but if you are unsure or disagree with the feedback, please feel free to wait for a Flutter team member's review for guidance on which automated comments should be addressed. <!-- Links --> [Contributor Guide]: https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md#overview [Tree Hygiene]: https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md [test-exempt]: https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md#tests [Flutter Style Guide]: https://github.com/flutter/flutter/blob/main/docs/contributing/Style-guide-for-Flutter-repo.md [Features we expect every widget to implement]: https://github.com/flutter/flutter/blob/main/docs/contributing/Style-guide-for-Flutter-repo.md#features-we-expect-every-widget-to-implement [CLA]: https://cla.developers.google.com/ [flutter/tests]: https://github.com/flutter/tests [breaking change policy]: https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md#handling-breaking-changes [Discord]: https://github.com/flutter/flutter/blob/main/docs/contributing/Chat.md [Data Driven Fixes]: https://github.com/flutter/flutter/blob/main/docs/contributing/Data-driven-Fixes.md
401 lines
17 KiB
Dart
401 lines
17 KiB
Dart
// Copyright 2014 The Flutter Authors. All rights reserved.
|
|
// Use of this source code is governed by a BSD-style license that can be
|
|
// found in the LICENSE file.
|
|
|
|
import 'dart:io';
|
|
|
|
import 'package:path/path.dart' as path;
|
|
|
|
import '../analyze.dart';
|
|
import '../custom_rules/analyze.dart';
|
|
import '../custom_rules/no_double_clamp.dart';
|
|
import '../custom_rules/no_stop_watches.dart';
|
|
import '../custom_rules/render_box_intrinsics.dart';
|
|
import '../utils.dart';
|
|
import 'common.dart';
|
|
|
|
typedef AsyncVoidCallback = Future<void> Function();
|
|
|
|
Future<String> capture(AsyncVoidCallback callback, {bool shouldHaveErrors = false}) async {
|
|
final buffer = StringBuffer();
|
|
final PrintCallback oldPrint = print;
|
|
try {
|
|
print = (Object? line) {
|
|
buffer.writeln(line);
|
|
};
|
|
await callback();
|
|
expect(
|
|
hasError,
|
|
shouldHaveErrors,
|
|
reason: buffer.isEmpty
|
|
? '(No output to report.)'
|
|
: hasError
|
|
? 'Unexpected errors:\n$buffer'
|
|
: 'Unexpected success:\n$buffer',
|
|
);
|
|
} finally {
|
|
print = oldPrint;
|
|
resetErrorStatus();
|
|
}
|
|
if (stdout.supportsAnsiEscapes) {
|
|
// Remove ANSI escapes when this test is running on a terminal.
|
|
return buffer.toString().replaceAll(RegExp(r'(\x9B|\x1B\[)[0-?]{1,3}[ -/]*[@-~]'), '');
|
|
} else {
|
|
return buffer.toString();
|
|
}
|
|
}
|
|
|
|
void main() {
|
|
final String testRootPath = path.join('test', 'analyze-test-input', 'root');
|
|
final dartName = Platform.isWindows ? 'dart.exe' : 'dart';
|
|
final String dartPath = path.canonicalize(
|
|
path.join('..', '..', 'bin', 'cache', 'dart-sdk', 'bin', dartName),
|
|
);
|
|
final String testGenDefaultsPath = path.join('test', 'analyze-gen-defaults');
|
|
|
|
test('matchesErrorsInFile matcher basic test', () async {
|
|
final String result = await capture(() async {
|
|
foundError(<String>[
|
|
'meta.dart:5: error #1',
|
|
'meta.dart:5: error #2',
|
|
'meta.dart:6: error #3',
|
|
'',
|
|
'Error summary',
|
|
]);
|
|
}, shouldHaveErrors: true);
|
|
final fixture = File(path.join(testRootPath, 'packages', 'foo', 'meta.dart'));
|
|
expect(result, matchesErrorsInFile(fixture, endsWith: <String>['', 'Error summary']));
|
|
});
|
|
|
|
test('analyze.dart - verifyDeprecations', () async {
|
|
final String result = await capture(
|
|
() => verifyDeprecations(testRootPath, minimumMatches: 2),
|
|
shouldHaveErrors: true,
|
|
);
|
|
final fixture = File(path.join(testRootPath, 'packages', 'foo', 'deprecation.dart'));
|
|
expect(
|
|
result,
|
|
matchesErrorsInFile(
|
|
fixture,
|
|
endsWith: <String>[
|
|
'See: https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md#handling-breaking-changes',
|
|
],
|
|
),
|
|
);
|
|
});
|
|
|
|
test('analyze.dart - verifyGoldenTags', () async {
|
|
final List<String> result = (await capture(
|
|
() => verifyGoldenTags(testRootPath, minimumMatches: 6),
|
|
shouldHaveErrors: true,
|
|
)).split('\n');
|
|
const noTag =
|
|
"Files containing golden tests must be tagged using @Tags(<String>['reduced-test-set']) "
|
|
'at the top of the file before import statements.';
|
|
const missingTag = "Files containing golden tests must be tagged with 'reduced-test-set'.";
|
|
final List<String> lines = <String>[
|
|
'║ test/analyze-test-input/root/packages/foo/golden_missing_tag.dart: $missingTag',
|
|
'║ test/analyze-test-input/root/packages/foo/golden_no_tag.dart: $noTag',
|
|
].map((String line) => line.replaceAll('/', Platform.isWindows ? r'\' : '/')).toList();
|
|
expect(
|
|
result.length,
|
|
4 + lines.length,
|
|
reason: 'output had unexpected number of lines:\n${result.join('\n')}',
|
|
);
|
|
expect(
|
|
result[0],
|
|
'╔═╡ERROR #1╞════════════════════════════════════════════════════════════════════',
|
|
);
|
|
expect(result.getRange(1, result.length - 3).toSet(), lines.toSet());
|
|
expect(
|
|
result[result.length - 3],
|
|
'║ See: https://github.com/flutter/flutter/blob/main/docs/contributing/testing/Writing-a-golden-file-test-for-package-flutter.md',
|
|
);
|
|
expect(
|
|
result[result.length - 2],
|
|
'╚═══════════════════════════════════════════════════════════════════════════════',
|
|
);
|
|
expect(result[result.length - 1], ''); // trailing newline
|
|
});
|
|
|
|
test('analyze.dart - verifyNoMissingLicense', () async {
|
|
final String result = await capture(
|
|
() => verifyNoMissingLicense(testRootPath, checkMinimums: false),
|
|
shouldHaveErrors: true,
|
|
);
|
|
final String file = 'test/analyze-test-input/root/packages/foo/foo.dart'.replaceAll(
|
|
'/',
|
|
Platform.isWindows ? r'\' : '/',
|
|
);
|
|
expect(
|
|
result,
|
|
'╔═╡ERROR #1╞════════════════════════════════════════════════════════════════════\n'
|
|
'║ The following file does not have the right license header for dart files:\n'
|
|
'║ $file\n'
|
|
'║ The expected license header is:\n'
|
|
'║ // 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'
|
|
'║ ...followed by a blank line.\n'
|
|
'╚═══════════════════════════════════════════════════════════════════════════════\n',
|
|
);
|
|
});
|
|
|
|
test('analyze.dart - verifyNoTrailingSpaces', () async {
|
|
final String result = await capture(
|
|
() => verifyNoTrailingSpaces(testRootPath, minimumMatches: 2),
|
|
shouldHaveErrors: true,
|
|
);
|
|
final String lines = <String>[
|
|
'║ test/analyze-test-input/root/packages/foo/spaces.txt:5: trailing U+0020 space character',
|
|
'║ test/analyze-test-input/root/packages/foo/spaces.txt:9: trailing blank line',
|
|
].map((String line) => line.replaceAll('/', Platform.isWindows ? r'\' : '/')).join('\n');
|
|
expect(
|
|
result,
|
|
'╔═╡ERROR #1╞════════════════════════════════════════════════════════════════════\n'
|
|
'$lines\n'
|
|
'╚═══════════════════════════════════════════════════════════════════════════════\n',
|
|
);
|
|
});
|
|
|
|
test('analyze.dart - verifyRepositoryLinks', () async {
|
|
final String result = await capture(
|
|
() => verifyRepositoryLinks(testRootPath),
|
|
shouldHaveErrors: true,
|
|
);
|
|
const bannedBranch = 'master';
|
|
final file = Platform.isWindows
|
|
? r'test\analyze-test-input\root\packages\foo\bad_repository_links.dart'
|
|
: 'test/analyze-test-input/root/packages/foo/bad_repository_links.dart';
|
|
final String lines = <String>[
|
|
'║ $file contains https://android.googlesource.com/+/$bannedBranch/file1, which uses the banned "master" branch.',
|
|
'║ $file contains https://chromium.googlesource.com/+/$bannedBranch/file1, which uses the banned "master" branch.',
|
|
'║ $file contains https://cs.opensource.google.com/+/$bannedBranch/file1, which uses the banned "master" branch.',
|
|
'║ $file contains https://dart.googlesource.com/+/$bannedBranch/file1, which uses the banned "master" branch.',
|
|
'║ $file contains https://flutter.googlesource.com/+/$bannedBranch/file1, which uses the banned "master" branch.',
|
|
'║ $file contains https://source.chromium.org/+/$bannedBranch/file1, which uses the banned "master" branch.',
|
|
'║ $file contains https://github.com/flutter/flutter/tree/$bannedBranch/file1, which uses the banned "master" branch.',
|
|
'║ $file contains https://raw.githubusercontent.com/flutter/flutter/blob/$bannedBranch/file1, which uses the banned "master" branch.',
|
|
'║ 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.',
|
|
].join('\n');
|
|
expect(
|
|
result,
|
|
'╔═╡ERROR #1╞════════════════════════════════════════════════════════════════════\n'
|
|
'$lines\n'
|
|
'╚═══════════════════════════════════════════════════════════════════════════════\n',
|
|
);
|
|
});
|
|
|
|
test('analyze.dart - verifyNoBinaries - positive', () async {
|
|
final String result = await capture(
|
|
() => verifyNoBinaries(
|
|
testRootPath,
|
|
legacyBinaries: <Hash256>{const Hash256(0x39A050CD69434936, 0, 0, 0)},
|
|
),
|
|
shouldHaveErrors: !Platform.isWindows,
|
|
);
|
|
if (!Platform.isWindows) {
|
|
expect(
|
|
result,
|
|
'╔═╡ERROR #1╞════════════════════════════════════════════════════════════════════\n'
|
|
'║ test/analyze-test-input/root/packages/foo/serviceaccount.enc:0: file is not valid UTF-8\n'
|
|
'║ All files in this repository must be UTF-8. In particular, images and other binaries\n'
|
|
'║ must not be checked into this repository. This is because we are very sensitive to the\n'
|
|
'║ size of the repository as it is distributed to all our developers. If you have a binary\n'
|
|
'║ to which you need access, you should consider how to fetch it from another repository;\n'
|
|
'║ for example, the "assets-for-api-docs" repository is used for images in API docs.\n'
|
|
'║ To add assets to flutter_tools templates, see the instructions in the wiki:\n'
|
|
'║ https://github.com/flutter/flutter/blob/main/docs/tool/Managing-template-image-assets.md\n'
|
|
'╚═══════════════════════════════════════════════════════════════════════════════\n',
|
|
);
|
|
}
|
|
});
|
|
|
|
test('analyze.dart - verifyInternationalizations - comparison fails', () async {
|
|
final String result = await capture(
|
|
() => verifyInternationalizations(testRootPath, dartPath),
|
|
shouldHaveErrors: true,
|
|
);
|
|
final String genLocalizationsScript = path.join(
|
|
'dev',
|
|
'tools',
|
|
'localization',
|
|
'bin',
|
|
'gen_localizations.dart',
|
|
);
|
|
expect(result, contains('$dartName $genLocalizationsScript --cupertino'));
|
|
expect(result, contains('$dartName $genLocalizationsScript --material'));
|
|
final String generatedFile = path.join(
|
|
testRootPath,
|
|
'packages',
|
|
'flutter_localizations',
|
|
'lib',
|
|
'src',
|
|
'l10n',
|
|
'generated_material_localizations.dart',
|
|
);
|
|
expect(
|
|
result,
|
|
contains(
|
|
'The contents of $generatedFile are different from that produced by gen_localizations.',
|
|
),
|
|
);
|
|
expect(
|
|
result,
|
|
contains(r'Did you forget to run gen_localizations.dart after updating a .arb file?'),
|
|
);
|
|
});
|
|
|
|
test('analyze.dart - verifyNoBinaries - negative', () async {
|
|
await capture(
|
|
() => verifyNoBinaries(
|
|
testRootPath,
|
|
legacyBinaries: <Hash256>{
|
|
const Hash256(
|
|
0xA8100AE6AA1940D0,
|
|
0xB663BB31CD466142,
|
|
0xEBBDBD5187131B92,
|
|
0xD93818987832EB89,
|
|
), // sha256("\xff")
|
|
const Hash256(0x155644D3F13D98BF, 0, 0, 0),
|
|
},
|
|
),
|
|
);
|
|
});
|
|
|
|
test('analyze.dart - verifyNullInitializedDebugExpensiveFields', () async {
|
|
final String result = await capture(
|
|
() => verifyNullInitializedDebugExpensiveFields(testRootPath, minimumMatches: 1),
|
|
shouldHaveErrors: true,
|
|
);
|
|
|
|
final fixture = File(path.join(testRootPath, 'packages', 'flutter', 'lib', 'bar.dart'));
|
|
expect(
|
|
result,
|
|
matchesErrorsInFile(
|
|
fixture,
|
|
endsWith: <String>[
|
|
'',
|
|
'Fields annotated with @_debugOnly must null initialize,',
|
|
'to ensure both the field and initializer are removed from profile/release mode.',
|
|
'These fields should be written as:',
|
|
'field = kDebugMode ? <DebugValue> : null;',
|
|
],
|
|
),
|
|
);
|
|
});
|
|
|
|
test('analyze.dart - verifyTabooDocumentation', () async {
|
|
final String result = await capture(
|
|
() => verifyTabooDocumentation(testRootPath, minimumMatches: 1),
|
|
shouldHaveErrors: true,
|
|
);
|
|
|
|
final fixture = File(path.join(testRootPath, 'packages', 'flutter', 'lib', 'taboo_words.dart'));
|
|
expect(
|
|
result,
|
|
matchesErrorsInFile(
|
|
fixture,
|
|
endsWith: <String>[
|
|
'',
|
|
'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.',
|
|
'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.',
|
|
'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.',
|
|
],
|
|
),
|
|
);
|
|
});
|
|
|
|
test('analyze.dart - clampDouble', () async {
|
|
final String result = await capture(
|
|
() => analyzeWithRules(
|
|
testRootPath,
|
|
<AnalyzeRule>[noDoubleClamp],
|
|
includePaths: <String>['packages/flutter/lib'],
|
|
),
|
|
shouldHaveErrors: true,
|
|
);
|
|
|
|
final fixture = File(
|
|
path.join(testRootPath, 'packages', 'flutter', 'lib', 'double_clamp.dart'),
|
|
);
|
|
expect(
|
|
result,
|
|
matchesErrorsInFile(
|
|
fixture,
|
|
endsWith: <String>[
|
|
'', // empty line before the last sentence.
|
|
'For performance reasons, we use a custom "clampDouble" function instead of using "double.clamp".',
|
|
],
|
|
),
|
|
);
|
|
});
|
|
|
|
test('analyze.dart - stopwatch', () async {
|
|
final String result = await capture(
|
|
() => analyzeWithRules(
|
|
testRootPath,
|
|
<AnalyzeRule>[noStopwatches],
|
|
includePaths: <String>['packages/flutter/lib'],
|
|
),
|
|
shouldHaveErrors: true,
|
|
);
|
|
|
|
final fixture = File(path.join(testRootPath, 'packages', 'flutter', 'lib', 'stopwatch.dart'));
|
|
expect(
|
|
result,
|
|
matchesErrorsInFile(
|
|
fixture,
|
|
endsWith: <String>[
|
|
'',
|
|
'Stopwatches introduce flakes by falling out of sync with the FakeAsync used in testing.',
|
|
'A Stopwatch that stays in sync with FakeAsync is available through the Gesture or Test bindings, through samplingClock.',
|
|
],
|
|
),
|
|
);
|
|
});
|
|
|
|
test('analyze.dart - RenderBox intrinsics', () async {
|
|
final String result = await capture(
|
|
() => analyzeWithRules(
|
|
testRootPath,
|
|
<AnalyzeRule>[renderBoxIntrinsicCalculation],
|
|
includePaths: <String>['packages/flutter/lib'],
|
|
),
|
|
shouldHaveErrors: true,
|
|
);
|
|
final fixture = File(
|
|
path.join(testRootPath, 'packages', 'flutter', 'lib', 'renderbox_intrinsics.dart'),
|
|
);
|
|
expect(
|
|
result,
|
|
matchesErrorsInFile(
|
|
fixture,
|
|
endsWith: <String>[
|
|
'',
|
|
'Typically the get* methods should be used to obtain the intrinsics of a RenderBox.',
|
|
],
|
|
),
|
|
);
|
|
});
|
|
|
|
test('analyze.dart - verifyMaterialFilesAreUpToDateWithTemplateFiles', () async {
|
|
String result = await capture(
|
|
() => verifyMaterialFilesAreUpToDateWithTemplateFiles(testGenDefaultsPath, dartPath),
|
|
shouldHaveErrors: true,
|
|
);
|
|
final String lines = <String>[
|
|
'║ chip.dart is not up-to-date with the token template file.',
|
|
].map((String line) => line.replaceAll('/', Platform.isWindows ? r'\' : '/')).join('\n');
|
|
const errorStart = '╔═';
|
|
result = result.substring(result.indexOf(errorStart));
|
|
expect(
|
|
result,
|
|
'╔═╡ERROR #1╞════════════════════════════════════════════════════════════════════\n'
|
|
'$lines\n'
|
|
'║ See: https://github.com/flutter/flutter/blob/main/dev/tools/gen_defaults to update the token template files.\n'
|
|
'╚═══════════════════════════════════════════════════════════════════════════════\n',
|
|
);
|
|
});
|
|
}
|