Kate Lovett 9d96df2364
Modernize framework lints (#179089)
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
2025-11-26 01:10:39 +00:00

272 lines
9.0 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' show ProcessResult, exitCode, stderr;
import 'package:args/args.dart';
import 'package:file/file.dart';
import 'package:file/local.dart';
import 'package:path/path.dart' as path;
import 'package:platform/platform.dart';
import 'package:process/process.dart';
import 'package:snippets/snippets.dart';
const String _kElementOption = 'element';
const String _kHelpOption = 'help';
const String _kInputOption = 'input';
const String _kLibraryOption = 'library';
const String _kOutputDirectoryOption = 'output-directory';
const String _kOutputOption = 'output';
const String _kPackageOption = 'package';
const String _kSerialOption = 'serial';
const String _kTypeOption = 'type';
class GitStatusFailed implements Exception {
GitStatusFailed(this.gitResult);
final ProcessResult gitResult;
@override
String toString() {
return 'git status exited with a non-zero exit code: '
'${gitResult.exitCode}:\n${gitResult.stderr}\n${gitResult.stdout}';
}
}
/// A singleton filesystem that can be set by tests to a memory filesystem.
FileSystem filesystem = const LocalFileSystem();
/// A singleton snippet generator that can be set by tests to a mock, so that
/// we can test the command line parsing.
SnippetGenerator snippetGenerator = SnippetGenerator();
/// A singleton platform that can be set by tests for use in testing command line
/// parsing.
Platform platform = const LocalPlatform();
/// A singleton process manager that can be set by tests for use in testing.
ProcessManager processManager = const LocalProcessManager();
/// Get the name of the channel these docs are from.
///
/// First check env variable LUCI_BRANCH, then refer to the currently
/// checked out git branch.
String getChannelName({
Platform platform = const LocalPlatform(),
ProcessManager processManager = const LocalProcessManager(),
}) {
switch (platform.environment['LUCI_BRANCH']?.trim()) {
// Backward compatibility: Still support running on "master", but pretend it is "main".
case 'master' || 'main':
return 'main';
case 'stable':
return 'stable';
}
final gitBranchRegexp = RegExp(r'^## (?<branch>.*)');
final ProcessResult gitResult = processManager.runSync(
<String>['git', 'status', '-b', '--porcelain'],
// Use the FLUTTER_ROOT, if defined.
workingDirectory:
platform.environment['FLUTTER_ROOT']?.trim() ?? filesystem.currentDirectory.path,
// Adding extra debugging output to help debug why git status inexplicably fails
// (random non-zero error code) about 2% of the time.
environment: <String, String>{'GIT_TRACE': '2', 'GIT_TRACE_SETUP': '2'},
);
if (gitResult.exitCode != 0) {
throw GitStatusFailed(gitResult);
}
final RegExpMatch? gitBranchMatch = gitBranchRegexp.firstMatch(
(gitResult.stdout as String).trim().split('\n').first,
);
return gitBranchMatch == null
? '<unknown>'
: gitBranchMatch.namedGroup('branch')!.split('...').first;
}
const List<String> sampleTypes = <String>['snippet', 'sample', 'dartpad'];
// This is a hack to workaround the fact that git status inexplicably fails
// (with random non-zero error code) about 2% of the time.
String getChannelNameWithRetries({
Platform platform = const LocalPlatform(),
ProcessManager processManager = const LocalProcessManager(),
}) {
var retryCount = 0;
while (retryCount < 2) {
try {
return getChannelName(platform: platform, processManager: processManager);
} on GitStatusFailed catch (e) {
retryCount += 1;
stderr.write('git status failed, retrying ($retryCount)\nError report:\n$e');
}
}
return getChannelName(platform: platform, processManager: processManager);
}
/// Generates snippet dartdoc output for a given input, and creates any sample
/// applications needed by the snippet.
void main(List<String> argList) {
final Map<String, String> environment = platform.environment;
final parser = ArgParser();
parser.addOption(
_kTypeOption,
defaultsTo: 'dartpad',
allowed: sampleTypes,
allowedHelp: <String, String>{
'dartpad': 'Produce a code sample application for using in Dartpad.',
'sample': 'Produce a code sample application.',
'snippet': 'Produce a nicely formatted piece of sample code.',
},
help: 'The type of snippet to produce.',
);
parser.addOption(
_kOutputOption,
help:
'The output name for the generated sample application. Overrides '
'the naming generated by the --$_kPackageOption/--$_kLibraryOption/--$_kElementOption '
'arguments. Metadata will be written alongside in a .json file. '
'The basename of this argument is used as the ID. If this is a '
'relative path, will be placed under the --$_kOutputDirectoryOption location.',
);
parser.addOption(
_kOutputDirectoryOption,
defaultsTo: '.',
help: 'The output path for the generated sample application.',
);
parser.addOption(
_kInputOption,
defaultsTo: environment['INPUT'],
help: 'The input file containing the sample code to inject.',
);
parser.addOption(
_kPackageOption,
defaultsTo: environment['PACKAGE_NAME'],
help: 'The name of the package that this sample belongs to.',
);
parser.addOption(
_kLibraryOption,
defaultsTo: environment['LIBRARY_NAME'],
help: 'The name of the library that this sample belongs to.',
);
parser.addOption(
_kElementOption,
defaultsTo: environment['ELEMENT_NAME'],
help: 'The name of the element that this sample belongs to.',
);
parser.addOption(
_kSerialOption,
defaultsTo: environment['INVOCATION_INDEX'],
help: 'A unique serial number for this snippet tool invocation.',
);
parser.addFlag(
_kHelpOption,
negatable: false,
help: 'Prints help documentation for this command',
);
final ArgResults args = parser.parse(argList);
if (args[_kHelpOption]! as bool) {
stderr.writeln(parser.usage);
exitCode = 0;
return;
}
final sampleType = args[_kTypeOption]! as String;
if (args[_kInputOption] == null) {
stderr.writeln(parser.usage);
errorExit(
'The --$_kInputOption option must be specified, either on the command '
'line, or in the INPUT environment variable.',
);
return;
}
final File input = filesystem.file(args['input']! as String);
if (!input.existsSync()) {
errorExit('The input file ${input.path} does not exist.');
return;
}
final String packageName = args[_kPackageOption] as String? ?? '';
final String libraryName = args[_kLibraryOption] as String? ?? '';
final String elementName = args[_kElementOption] as String? ?? '';
final String serial = args[_kSerialOption] as String? ?? '';
late String id;
File? output;
final Directory outputDirectory = filesystem
.directory(args[_kOutputDirectoryOption]! as String)
.absolute;
if (args[_kOutputOption] != null) {
id = path.basenameWithoutExtension(args[_kOutputOption]! as String);
final File outputPath = filesystem.file(args[_kOutputOption]! as String);
if (outputPath.isAbsolute) {
output = outputPath;
} else {
output = filesystem.file(path.join(outputDirectory.path, outputPath.path));
}
} else {
final idParts = <String>[];
if (packageName.isNotEmpty && packageName != 'flutter') {
idParts.add(packageName.replaceAll(RegExp(r'\W'), '_').toLowerCase());
}
if (libraryName.isNotEmpty) {
idParts.add(libraryName.replaceAll(RegExp(r'\W'), '_').toLowerCase());
}
if (elementName.isNotEmpty) {
idParts.add(elementName);
}
if (serial.isNotEmpty) {
idParts.add(serial);
}
if (idParts.isEmpty) {
errorExit(
'Unable to determine ID. At least one of --$_kPackageOption, '
'--$_kLibraryOption, --$_kElementOption, -$_kSerialOption, or the environment variables '
'PACKAGE_NAME, LIBRARY_NAME, ELEMENT_NAME, or INVOCATION_INDEX must be non-empty.',
);
return;
}
id = idParts.join('.');
output = outputDirectory.childFile('$id.dart');
}
output.parent.createSync(recursive: true);
final int? sourceLine = environment['SOURCE_LINE'] != null
? int.tryParse(environment['SOURCE_LINE']!)
: null;
final String sourcePath = environment['SOURCE_PATH'] ?? 'unknown.dart';
final sampleParser = SnippetDartdocParser(filesystem);
final SourceElement element = sampleParser.parseFromDartdocToolFile(
input,
startLine: sourceLine,
element: elementName,
sourceFile: filesystem.file(sourcePath),
type: sampleType,
);
final metadata = <String, Object?>{
'channel': getChannelNameWithRetries(platform: platform, processManager: processManager),
'serial': serial,
'id': id,
'package': packageName,
'library': libraryName,
'element': elementName,
};
for (final CodeSample sample in element.samples) {
sample.metadata.addAll(metadata);
snippetGenerator.generateCode(sample, output: output);
print(snippetGenerator.generateHtml(sample));
}
exitCode = 0;
}