[flutter_plugin_tools] Validate code blocks in readme-check (#5436)

This commit is contained in:
stuartmorgan
2022-04-28 22:29:09 -04:00
committed by GitHub
parent 6770bf9245
commit d43fae6fb4
4 changed files with 223 additions and 10 deletions

View File

@ -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

View File

@ -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);

View File

@ -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

View File

@ -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'),
]),
);
});
});
}