mirror of
https://github.com/flutter/packages.git
synced 2025-06-04 02:08:40 +08:00

Updates the repo tooling to Dart 3, now that the N-2 version has Dart 3, which allows us to use Dart 3 features (e.g., records) going forward. To allow the update: - Removes `break` commands from `switch`es (all done automatically with `dart fix --apply`) - Replaces mocking of `ProcessResult` with just creating an actual `ProcessResult` since it's a `final` data class and thus can't (but also doesn't need to be) mocked.
334 lines
11 KiB
Dart
334 lines
11 KiB
Dart
// Copyright 2013 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 'package:file/file.dart';
|
|
|
|
import 'common/output_utils.dart';
|
|
import 'common/package_looping_command.dart';
|
|
import 'common/repository_package.dart';
|
|
|
|
class _UpdateResult {
|
|
const _UpdateResult(this.changed, this.snippetCount, this.errors);
|
|
final bool changed;
|
|
final int snippetCount;
|
|
final List<String> errors;
|
|
}
|
|
|
|
enum _ExcerptParseMode { normal, pragma, injecting }
|
|
|
|
/// A command to update .md code excerpts from code files.
|
|
class UpdateExcerptsCommand extends PackageLoopingCommand {
|
|
/// Creates a excerpt updater command instance.
|
|
UpdateExcerptsCommand(
|
|
super.packagesDir, {
|
|
super.processRunner,
|
|
super.platform,
|
|
super.gitDir,
|
|
}) {
|
|
argParser.addFlag(
|
|
_failOnChangeFlag,
|
|
help: 'Fail if the command does anything. '
|
|
'(Used in CI to ensure excerpts are up to date.)',
|
|
);
|
|
}
|
|
|
|
static const String _failOnChangeFlag = 'fail-on-change';
|
|
|
|
@override
|
|
final String name = 'update-excerpts';
|
|
|
|
@override
|
|
final String description = 'Updates code excerpts in .md files, based '
|
|
'on code from code files, via <?code-excerpt?> pragmas.';
|
|
|
|
@override
|
|
bool get hasLongOutput => false;
|
|
|
|
@override
|
|
Future<PackageResult> runForPackage(RepositoryPackage package) async {
|
|
final List<File> changedFiles = <File>[];
|
|
final List<String> errors = <String>[];
|
|
final List<File> markdownFiles = package.directory
|
|
.listSync(recursive: true)
|
|
.where((FileSystemEntity entity) {
|
|
return entity is File &&
|
|
entity.basename != 'CHANGELOG.md' &&
|
|
entity.basename.toLowerCase().endsWith('.md');
|
|
})
|
|
.cast<File>()
|
|
.toList();
|
|
for (final File file in markdownFiles) {
|
|
final _UpdateResult result = _updateExcerptsIn(file);
|
|
if (result.snippetCount > 0) {
|
|
final String displayPath =
|
|
getRelativePosixPath(file, from: package.directory);
|
|
print('${indentation}Checked ${result.snippetCount} snippet(s) in '
|
|
'$displayPath.');
|
|
}
|
|
if (result.changed) {
|
|
changedFiles.add(file);
|
|
}
|
|
if (result.errors.isNotEmpty) {
|
|
errors.addAll(result.errors);
|
|
}
|
|
}
|
|
|
|
if (errors.isNotEmpty) {
|
|
printError('${indentation}Injecting excerpts failed:');
|
|
printError(errors.join('\n$indentation'));
|
|
return PackageResult.fail();
|
|
}
|
|
|
|
if (getBoolArg(_failOnChangeFlag) && changedFiles.isNotEmpty) {
|
|
printError(
|
|
'${indentation}The following files have out of date excerpts:\n'
|
|
'$indentation ${changedFiles.map((File file) => file.path).join("\n$indentation ")}\n'
|
|
'\n'
|
|
'${indentation}If you edited code in a .md file directly, you should '
|
|
'instead edit the files that contain the sources of the excerpts.\n'
|
|
'${indentation}If you did edit those source files, run the repository '
|
|
'tooling\'s "$name" command on this package, and update your PR with '
|
|
'the resulting changes.\n'
|
|
'\n'
|
|
'${indentation}For more information, see '
|
|
'https://github.com/flutter/flutter/wiki/Contributing-to-Plugins-and-Packages#readme-code',
|
|
);
|
|
return PackageResult.fail();
|
|
}
|
|
|
|
return PackageResult.success();
|
|
}
|
|
|
|
static const String _pragma = '<?code-excerpt';
|
|
static final RegExp _basePattern =
|
|
RegExp(r'^ *<\?code-excerpt path-base="([^"]+)"\?>$');
|
|
static final RegExp _injectPattern = RegExp(
|
|
r'^ *<\?code-excerpt "(?<path>[^ ]+) \((?<section>[^)]+)\)"(?: plaster="(?<plaster>[^"]*)")?\?>$',
|
|
);
|
|
|
|
_UpdateResult _updateExcerptsIn(File file) {
|
|
bool detectedChange = false;
|
|
int snippetCount = 0;
|
|
final List<String> errors = <String>[];
|
|
Directory pathBase = file.parent;
|
|
final StringBuffer output = StringBuffer();
|
|
final StringBuffer existingBlock = StringBuffer();
|
|
String? language;
|
|
String? excerpt;
|
|
_ExcerptParseMode mode = _ExcerptParseMode.normal;
|
|
int lineNumber = 0;
|
|
for (final String line in file.readAsLinesSync()) {
|
|
lineNumber += 1;
|
|
switch (mode) {
|
|
case _ExcerptParseMode.normal:
|
|
if (line.contains(_pragma)) {
|
|
RegExpMatch? match = _basePattern.firstMatch(line);
|
|
if (match != null) {
|
|
pathBase =
|
|
file.parent.childDirectory(path.normalize(match.group(1)!));
|
|
} else {
|
|
match = _injectPattern.firstMatch(line);
|
|
if (match != null) {
|
|
snippetCount++;
|
|
final String excerptPath =
|
|
path.normalize(match.namedGroup('path')!);
|
|
final File excerptSourceFile = pathBase.childFile(excerptPath);
|
|
final String extension = path.extension(excerptSourceFile.path);
|
|
switch (extension) {
|
|
case '':
|
|
language = 'txt';
|
|
case '.kt':
|
|
language = 'kotlin';
|
|
case '.cc':
|
|
case '.cpp':
|
|
language = 'c++';
|
|
case '.m':
|
|
language = 'objectivec';
|
|
case '.gradle':
|
|
language = 'groovy';
|
|
default:
|
|
language = extension.substring(1);
|
|
break;
|
|
}
|
|
final String section = match.namedGroup('section')!;
|
|
final String plaster = match.namedGroup('plaster') ?? '···';
|
|
if (!excerptSourceFile.existsSync()) {
|
|
errors.add(
|
|
'${file.path}:$lineNumber: specified file "$excerptPath" (resolved to "${excerptSourceFile.path}") does not exist');
|
|
} else {
|
|
excerpt = _extractExcerpt(
|
|
excerptSourceFile, section, plaster, language, errors);
|
|
}
|
|
mode = _ExcerptParseMode.pragma;
|
|
} else {
|
|
errors.add(
|
|
'${file.path}:$lineNumber: $_pragma?> pragma does not match expected syntax or is not alone on the line');
|
|
}
|
|
}
|
|
}
|
|
output.writeln(line);
|
|
case _ExcerptParseMode.pragma:
|
|
if (!line.startsWith('```')) {
|
|
errors.add(
|
|
'${file.path}:$lineNumber: expected code block but did not find one');
|
|
mode = _ExcerptParseMode.normal;
|
|
} else {
|
|
if (line.startsWith('``` ')) {
|
|
errors.add(
|
|
'${file.path}:$lineNumber: code block was followed by a space character instead of the language (expected "$language")');
|
|
mode = _ExcerptParseMode.injecting;
|
|
} else if (line != '```$language' &&
|
|
line != '```rfwtxt' &&
|
|
line != '```json') {
|
|
// We special-case rfwtxt and json because the rfw package extracts such sections from Dart files.
|
|
// If we get more special cases we should think about a more general solution.
|
|
errors.add(
|
|
'${file.path}:$lineNumber: code block has wrong language');
|
|
}
|
|
mode = _ExcerptParseMode.injecting;
|
|
}
|
|
output.writeln(line);
|
|
case _ExcerptParseMode.injecting:
|
|
if (line == '```') {
|
|
if (existingBlock.toString() != excerpt) {
|
|
detectedChange = true;
|
|
}
|
|
output.write(excerpt);
|
|
output.writeln(line);
|
|
mode = _ExcerptParseMode.normal;
|
|
language = null;
|
|
excerpt = null;
|
|
existingBlock.clear();
|
|
} else {
|
|
existingBlock.writeln(line);
|
|
}
|
|
}
|
|
}
|
|
if (detectedChange) {
|
|
if (errors.isNotEmpty) {
|
|
errors.add('${file.path}: skipped updating file due to errors');
|
|
} else {
|
|
try {
|
|
file.writeAsStringSync(output.toString());
|
|
} catch (e) {
|
|
errors.add(
|
|
'${file.path}: failed to update file (${e.runtimeType}: $e)');
|
|
}
|
|
}
|
|
}
|
|
return _UpdateResult(detectedChange, snippetCount, errors);
|
|
}
|
|
|
|
String _extractExcerpt(File excerptSourceFile, String section,
|
|
String plasterInside, String language, List<String> errors) {
|
|
final List<String> buffer = <String>[];
|
|
bool extracting = false;
|
|
int lineNumber = 0;
|
|
int maxLength = 0;
|
|
bool found = false;
|
|
String prefix = '';
|
|
String suffix = '';
|
|
String padding = '';
|
|
switch (language) {
|
|
case 'cc':
|
|
case 'c++':
|
|
case 'dart':
|
|
case 'js':
|
|
case 'kotlin':
|
|
case 'rfwtxt':
|
|
case 'java':
|
|
case 'groovy':
|
|
case 'objectivec':
|
|
case 'swift':
|
|
prefix = '// ';
|
|
case 'css':
|
|
prefix = '/* ';
|
|
suffix = ' */';
|
|
case 'html':
|
|
case 'xml':
|
|
prefix = '<!--';
|
|
suffix = '-->';
|
|
padding = ' ';
|
|
case 'yaml':
|
|
prefix = '# ';
|
|
case 'sh':
|
|
prefix = '# ';
|
|
}
|
|
final String startRegionMarker = '$prefix#docregion $section$suffix';
|
|
final String endRegionMarker = '$prefix#enddocregion $section$suffix';
|
|
final String plaster = '$prefix$padding$plasterInside$padding$suffix';
|
|
int? indentation;
|
|
for (final String excerptLine in excerptSourceFile.readAsLinesSync()) {
|
|
final String trimmedLine = excerptLine.trimLeft();
|
|
lineNumber += 1;
|
|
if (extracting) {
|
|
if (trimmedLine == endRegionMarker) {
|
|
extracting = false;
|
|
indentation = excerptLine.length - trimmedLine.length;
|
|
} else {
|
|
if (trimmedLine == startRegionMarker) {
|
|
errors.add(
|
|
'${excerptSourceFile.path}:$lineNumber: saw "$startRegionMarker" pragma while already in a "$section" doc region');
|
|
}
|
|
if (excerptLine.length > maxLength) {
|
|
maxLength = excerptLine.length;
|
|
}
|
|
if (!excerptLine.contains('$prefix#docregion ') &&
|
|
!excerptLine.contains('$prefix#enddocregion ')) {
|
|
buffer.add(excerptLine);
|
|
}
|
|
}
|
|
} else {
|
|
if (trimmedLine == startRegionMarker) {
|
|
found = true;
|
|
extracting = true;
|
|
if (buffer.isNotEmpty && plasterInside != 'none') {
|
|
assert(indentation != null);
|
|
buffer.add('${" " * indentation!}$plaster');
|
|
indentation = null;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
if (extracting) {
|
|
errors
|
|
.add('${excerptSourceFile.path}: missing "$endRegionMarker" pragma');
|
|
}
|
|
if (!found) {
|
|
errors.add(
|
|
'${excerptSourceFile.path}: did not find a "$startRegionMarker" pragma');
|
|
return '';
|
|
}
|
|
if (buffer.isEmpty) {
|
|
errors.add('${excerptSourceFile.path}: region "$section" is empty');
|
|
return '';
|
|
}
|
|
int indent = maxLength;
|
|
for (final String line in buffer) {
|
|
if (indent == 0) {
|
|
break;
|
|
}
|
|
if (line.isEmpty) {
|
|
continue;
|
|
}
|
|
for (int index = 0; index < line.length; index += 1) {
|
|
if (line[index] != ' ') {
|
|
if (index < indent) {
|
|
indent = index;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
final StringBuffer excerpt = StringBuffer();
|
|
for (final String line in buffer) {
|
|
if (line.isEmpty) {
|
|
excerpt.writeln();
|
|
} else {
|
|
excerpt.writeln(line.substring(indent));
|
|
}
|
|
}
|
|
return excerpt.toString();
|
|
}
|
|
}
|