mirror of
https://github.com/flutter/packages.git
synced 2025-06-02 00:49:11 +08:00
[flutter_plugin_tools] Validate code blocks in readme-check (#5436)
This commit is contained in:
@ -1,3 +1,10 @@
|
||||
## 0.8.4
|
||||
|
||||
- `readme-check` now validates that there's a info tag on code blocks to
|
||||
identify (and for supported languages, syntax highlight) the language.
|
||||
- `readme-check` now has a `--require-excerpts` flag to require that any Dart
|
||||
code blocks be managed by `code_excerpter`.
|
||||
|
||||
## 0.8.3
|
||||
|
||||
- Adds a new `update-excerpts` command to maintain README files using the
|
||||
|
@ -26,7 +26,12 @@ class ReadmeCheckCommand extends PackageLoopingCommand {
|
||||
processRunner: processRunner,
|
||||
platform: platform,
|
||||
gitDir: gitDir,
|
||||
);
|
||||
) {
|
||||
argParser.addFlag(_requireExcerptsArg,
|
||||
help: 'Require that Dart code blocks be managed by code-excerpt.');
|
||||
}
|
||||
|
||||
static const String _requireExcerptsArg = 'require-excerpts';
|
||||
|
||||
// Standardized capitalizations for platforms that a plugin can support.
|
||||
static const Map<String, String> _standardPlatformNames = <String, String>{
|
||||
@ -61,8 +66,15 @@ class ReadmeCheckCommand extends PackageLoopingCommand {
|
||||
final Pubspec pubspec = package.parsePubspec();
|
||||
final bool isPlugin = pubspec.flutter?['plugin'] != null;
|
||||
|
||||
final List<String> readmeLines = package.readmeFile.readAsLinesSync();
|
||||
|
||||
final String? blockValidationError = _validateCodeBlocks(readmeLines);
|
||||
if (blockValidationError != null) {
|
||||
errors.add(blockValidationError);
|
||||
}
|
||||
|
||||
if (isPlugin && (!package.isFederated || package.isAppFacing)) {
|
||||
final String? error = _validateSupportedPlatforms(package, pubspec);
|
||||
final String? error = _validateSupportedPlatforms(readmeLines, pubspec);
|
||||
if (error != null) {
|
||||
errors.add(error);
|
||||
}
|
||||
@ -73,23 +85,86 @@ class ReadmeCheckCommand extends PackageLoopingCommand {
|
||||
: PackageResult.fail(errors);
|
||||
}
|
||||
|
||||
/// Validates that code blocks (``` ... ```) follow repository standards.
|
||||
String? _validateCodeBlocks(List<String> readmeLines) {
|
||||
final RegExp codeBlockDelimiterPattern = RegExp(r'^\s*```\s*([^ ]*)\s*');
|
||||
final List<int> missingLanguageLines = <int>[];
|
||||
final List<int> missingExcerptLines = <int>[];
|
||||
bool inBlock = false;
|
||||
for (int i = 0; i < readmeLines.length; ++i) {
|
||||
final RegExpMatch? match =
|
||||
codeBlockDelimiterPattern.firstMatch(readmeLines[i]);
|
||||
if (match == null) {
|
||||
continue;
|
||||
}
|
||||
if (inBlock) {
|
||||
inBlock = false;
|
||||
continue;
|
||||
}
|
||||
inBlock = true;
|
||||
|
||||
final int humanReadableLineNumber = i + 1;
|
||||
|
||||
// Ensure that there's a language tag.
|
||||
final String infoString = match[1] ?? '';
|
||||
if (infoString.isEmpty) {
|
||||
missingLanguageLines.add(humanReadableLineNumber);
|
||||
continue;
|
||||
}
|
||||
|
||||
// Check for code-excerpt usage if requested.
|
||||
if (getBoolArg(_requireExcerptsArg) && infoString == 'dart') {
|
||||
const String excerptTagStart = '<?code-excerpt ';
|
||||
if (i == 0 || !readmeLines[i - 1].trim().startsWith(excerptTagStart)) {
|
||||
missingExcerptLines.add(humanReadableLineNumber);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
String? errorSummary;
|
||||
|
||||
if (missingLanguageLines.isNotEmpty) {
|
||||
for (final int lineNumber in missingLanguageLines) {
|
||||
printError('${indentation}Code block at line $lineNumber is missing '
|
||||
'a language identifier.');
|
||||
}
|
||||
printError(
|
||||
'\n${indentation}For each block listed above, add a language tag to '
|
||||
'the opening block. For instance, for Dart code, use:\n'
|
||||
'${indentation * 2}```dart\n');
|
||||
errorSummary = 'Missing language identifier for code block';
|
||||
}
|
||||
|
||||
if (missingExcerptLines.isNotEmpty) {
|
||||
for (final int lineNumber in missingExcerptLines) {
|
||||
printError('${indentation}Dart code block at line $lineNumber is not '
|
||||
'managed by code-excerpt.');
|
||||
}
|
||||
printError(
|
||||
'\n${indentation}For each block listed above, add <?code-excerpt ...> '
|
||||
'tag on the previous line, and ensure that a build.excerpt.yaml is '
|
||||
'configured for the source example.\n');
|
||||
errorSummary ??= 'Missing code-excerpt management for code block';
|
||||
}
|
||||
|
||||
return errorSummary;
|
||||
}
|
||||
|
||||
/// Validates that the plugin has a supported platforms table following the
|
||||
/// expected format, returning an error string if any issues are found.
|
||||
String? _validateSupportedPlatforms(
|
||||
RepositoryPackage package, Pubspec pubspec) {
|
||||
final List<String> contents = package.readmeFile.readAsLinesSync();
|
||||
|
||||
List<String> readmeLines, Pubspec pubspec) {
|
||||
// Example table following expected format:
|
||||
// | | Android | iOS | Web |
|
||||
// |----------------|---------|----------|------------------------|
|
||||
// | **Support** | SDK 21+ | iOS 10+* | [See `camera_web `][1] |
|
||||
final int detailsLineNumber =
|
||||
contents.indexWhere((String line) => line.startsWith('| **Support**'));
|
||||
final int detailsLineNumber = readmeLines
|
||||
.indexWhere((String line) => line.startsWith('| **Support**'));
|
||||
if (detailsLineNumber == -1) {
|
||||
return 'No OS support table found';
|
||||
}
|
||||
final int osLineNumber = detailsLineNumber - 2;
|
||||
if (osLineNumber < 0 || !contents[osLineNumber].startsWith('|')) {
|
||||
if (osLineNumber < 0 || !readmeLines[osLineNumber].startsWith('|')) {
|
||||
return 'OS support table does not have the expected header format';
|
||||
}
|
||||
|
||||
@ -111,7 +186,7 @@ class ReadmeCheckCommand extends PackageLoopingCommand {
|
||||
final YamlMap platformSupportMaps = platformsEntry as YamlMap;
|
||||
final Set<String> actuallySupportedPlatform =
|
||||
platformSupportMaps.keys.toSet().cast<String>();
|
||||
final Iterable<String> documentedPlatforms = contents[osLineNumber]
|
||||
final Iterable<String> documentedPlatforms = readmeLines[osLineNumber]
|
||||
.split('|')
|
||||
.map((String entry) => entry.trim())
|
||||
.where((String entry) => entry.isNotEmpty);
|
||||
|
@ -1,7 +1,7 @@
|
||||
name: flutter_plugin_tools
|
||||
description: Productivity utils for flutter/plugins and flutter/packages
|
||||
repository: https://github.com/flutter/plugins/tree/main/script/tool
|
||||
version: 0.8.3
|
||||
version: 0.8.4
|
||||
|
||||
dependencies:
|
||||
args: ^2.1.0
|
||||
|
@ -275,4 +275,135 @@ A very useful plugin.
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
group('code blocks', () {
|
||||
test('fails on missing info string', () async {
|
||||
final Directory packageDir = createFakePackage('a_package', packagesDir);
|
||||
|
||||
packageDir.childFile('README.md').writeAsStringSync('''
|
||||
Example:
|
||||
|
||||
```
|
||||
void main() {
|
||||
// ...
|
||||
}
|
||||
```
|
||||
''');
|
||||
|
||||
Error? commandError;
|
||||
final List<String> output = await runCapturingPrint(
|
||||
runner, <String>['readme-check'], errorHandler: (Error e) {
|
||||
commandError = e;
|
||||
});
|
||||
|
||||
expect(commandError, isA<ToolExit>());
|
||||
expect(
|
||||
output,
|
||||
containsAllInOrder(<Matcher>[
|
||||
contains('Code block at line 3 is missing a language identifier.'),
|
||||
contains('Missing language identifier for code block'),
|
||||
]),
|
||||
);
|
||||
});
|
||||
|
||||
test('allows unknown info strings', () async {
|
||||
final Directory packageDir = createFakePackage('a_package', packagesDir);
|
||||
|
||||
packageDir.childFile('README.md').writeAsStringSync('''
|
||||
Example:
|
||||
|
||||
```someunknowninfotag
|
||||
A B C
|
||||
```
|
||||
''');
|
||||
|
||||
final List<String> output = await runCapturingPrint(runner, <String>[
|
||||
'readme-check',
|
||||
]);
|
||||
|
||||
expect(
|
||||
output,
|
||||
containsAll(<Matcher>[
|
||||
contains('Running for a_package...'),
|
||||
contains('No issues found!'),
|
||||
]),
|
||||
);
|
||||
});
|
||||
|
||||
test('allows space around info strings', () async {
|
||||
final Directory packageDir = createFakePackage('a_package', packagesDir);
|
||||
|
||||
packageDir.childFile('README.md').writeAsStringSync('''
|
||||
Example:
|
||||
|
||||
``` dart
|
||||
A B C
|
||||
```
|
||||
''');
|
||||
|
||||
final List<String> output = await runCapturingPrint(runner, <String>[
|
||||
'readme-check',
|
||||
]);
|
||||
|
||||
expect(
|
||||
output,
|
||||
containsAll(<Matcher>[
|
||||
contains('Running for a_package...'),
|
||||
contains('No issues found!'),
|
||||
]),
|
||||
);
|
||||
});
|
||||
|
||||
test('passes when excerpt requirement is met', () async {
|
||||
final Directory packageDir = createFakePackage('a_package', packagesDir);
|
||||
|
||||
packageDir.childFile('README.md').writeAsStringSync('''
|
||||
Example:
|
||||
|
||||
<?code-excerpt "main.dart (SomeSection)"?>
|
||||
```dart
|
||||
A B C
|
||||
```
|
||||
''');
|
||||
|
||||
final List<String> output = await runCapturingPrint(
|
||||
runner, <String>['readme-check', '--require-excerpts']);
|
||||
|
||||
expect(
|
||||
output,
|
||||
containsAll(<Matcher>[
|
||||
contains('Running for a_package...'),
|
||||
contains('No issues found!'),
|
||||
]),
|
||||
);
|
||||
});
|
||||
|
||||
test('fails on missing excerpt tag when requested', () async {
|
||||
final Directory packageDir = createFakePackage('a_package', packagesDir);
|
||||
|
||||
packageDir.childFile('README.md').writeAsStringSync('''
|
||||
Example:
|
||||
|
||||
```dart
|
||||
A B C
|
||||
```
|
||||
''');
|
||||
|
||||
Error? commandError;
|
||||
final List<String> output = await runCapturingPrint(
|
||||
runner, <String>['readme-check', '--require-excerpts'],
|
||||
errorHandler: (Error e) {
|
||||
commandError = e;
|
||||
});
|
||||
|
||||
expect(commandError, isA<ToolExit>());
|
||||
expect(
|
||||
output,
|
||||
containsAllInOrder(<Matcher>[
|
||||
contains('Dart code block at line 3 is not managed by code-excerpt.'),
|
||||
contains('Missing code-excerpt management for code block'),
|
||||
]),
|
||||
);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
Reference in New Issue
Block a user