diff --git a/.cirrus.yml b/.cirrus.yml index c256ab19426e..fe5a18097c80 100644 --- a/.cirrus.yml +++ b/.cirrus.yml @@ -101,7 +101,13 @@ task: always: format_script: ./script/tool_runner.sh format --fail-on-change pubspec_script: ./script/tool_runner.sh pubspec-check - readme_script: ./script/tool_runner.sh readme-check + readme_script: + - ./script/tool_runner.sh readme-check + # Re-run with --require-excerpts, skipping packages that still need + # to be converted. Once https://github.com/flutter/flutter/issues/102679 + # has been fixed, this can be removed and there can just be a single + # run with --require-excerpts and no exclusions. + - ./script/tool_runner.sh readme-check --require-excerpts --exclude=script/configs/temp_exclude_excerpt.yaml license_script: dart $PLUGIN_TOOL license-check - name: federated_safety # This check is only meaningful for PRs, as it validates changes diff --git a/packages/camera/camera/README.md b/packages/camera/camera/README.md index a1c60a08950d..97b16d20f48a 100644 --- a/packages/camera/camera/README.md +++ b/packages/camera/camera/README.md @@ -46,7 +46,7 @@ If editing `Info.plist` as text, add: Change the minimum Android sdk version to 21 (or higher) in your `android/app/build.gradle` file. -``` +```groovy minSdkVersion 21 ``` diff --git a/packages/espresso/README.md b/packages/espresso/README.md index 68c3b55089ca..6d66bfbe85b5 100644 --- a/packages/espresso/README.md +++ b/packages/espresso/README.md @@ -85,13 +85,13 @@ void main() { The following command line command runs the test locally: -``` +```sh ./gradlew app:connectedAndroidTest -Ptarget=`pwd`/../test_driver/example.dart ``` Espresso tests can also be run on [Firebase Test Lab](https://firebase.google.com/docs/test-lab): -``` +```sh ./gradlew app:assembleAndroidTest ./gradlew app:assembleDebug -Ptarget=.dart gcloud auth activate-service-account --key-file= diff --git a/packages/file_selector/file_selector/README.md b/packages/file_selector/file_selector/README.md index 544cde8218bd..89cac1e6fd5f 100644 --- a/packages/file_selector/file_selector/README.md +++ b/packages/file_selector/file_selector/README.md @@ -14,12 +14,12 @@ To use this plugin, add `file_selector` as a [dependency in your pubspec.yaml fi ### macOS You will need to [add an entitlement][entitlement] for either read-only access: -``` +```xml com.apple.security.files.user-selected.read-only ``` or read/write access: -``` +```xml com.apple.security.files.user-selected.read-write ``` diff --git a/packages/file_selector/file_selector_macos/README.md b/packages/file_selector/file_selector_macos/README.md index efa5272149be..3241b21d1e18 100644 --- a/packages/file_selector/file_selector_macos/README.md +++ b/packages/file_selector/file_selector_macos/README.md @@ -17,12 +17,12 @@ APIs directly. ### Entitlements You will need to [add an entitlement][4] for either read-only access: -``` +```xml com.apple.security.files.user-selected.read-only ``` or read/write access: -``` +```xml com.apple.security.files.user-selected.read-write ``` diff --git a/packages/google_sign_in/google_sign_in_web/README.md b/packages/google_sign_in/google_sign_in_web/README.md index 4ee1a2956b45..463603e73e54 100644 --- a/packages/google_sign_in/google_sign_in_web/README.md +++ b/packages/google_sign_in/google_sign_in_web/README.md @@ -37,7 +37,7 @@ Normally `flutter run` starts in a random port. In the case where you need to de You can tell `flutter run` to listen for requests in a specific host and port with the following: -``` +```sh flutter run -d chrome --web-hostname localhost --web-port 7357 ``` diff --git a/packages/url_launcher/url_launcher/README.md b/packages/url_launcher/url_launcher/README.md index 0cdbe1b9859e..9c9f0b57e667 100644 --- a/packages/url_launcher/url_launcher/README.md +++ b/packages/url_launcher/url_launcher/README.md @@ -46,7 +46,7 @@ See the example app for more complex examples. Add any URL schemes passed to `canLaunchUrl` as `LSApplicationQueriesSchemes` entries in your Info.plist file. Example: -``` +```xml LSApplicationQueriesSchemes https diff --git a/script/configs/temp_exclude_excerpt.yaml b/script/configs/temp_exclude_excerpt.yaml new file mode 100644 index 000000000000..fc8454d75a0c --- /dev/null +++ b/script/configs/temp_exclude_excerpt.yaml @@ -0,0 +1,27 @@ +# Packages that have not yet adopted code-excerpt. +# +# This only exists to allow incrementally adopting the new requirement. +# Packages shoud never be added to this list. + +# TODO(ecosystem): Remove everything from this list. See +# https://github.com/flutter/flutter/issues/102679 +- camera_web +- espresso +- file_selector/file_selector +- google_maps_flutter/google_maps_flutter +- google_sign_in/google_sign_in +- google_sign_in_web +- image_picker/image_picker +- image_picker_for_web +- in_app_purchase/in_app_purchase +- ios_platform_images +- local_auth/local_auth +- path_provider/path_provider +- plugin_platform_interface +- quick_actions/quick_actions +- shared_preferences/shared_preferences +- url_launcher/url_launcher +- video_player/video_player +- webview_flutter/webview_flutter +- webview_flutter_android +- webview_flutter_web diff --git a/script/tool/CHANGELOG.md b/script/tool/CHANGELOG.md index 7587ff33f027..1bce029a559f 100644 --- a/script/tool/CHANGELOG.md +++ b/script/tool/CHANGELOG.md @@ -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 diff --git a/script/tool/lib/src/readme_check_command.dart b/script/tool/lib/src/readme_check_command.dart index 99e271c73388..9432c4b7fa40 100644 --- a/script/tool/lib/src/readme_check_command.dart +++ b/script/tool/lib/src/readme_check_command.dart @@ -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 _standardPlatformNames = { @@ -61,8 +66,15 @@ class ReadmeCheckCommand extends PackageLoopingCommand { final Pubspec pubspec = package.parsePubspec(); final bool isPlugin = pubspec.flutter?['plugin'] != null; + final List 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 readmeLines) { + final RegExp codeBlockDelimiterPattern = RegExp(r'^\s*```\s*([^ ]*)\s*'); + final List missingLanguageLines = []; + final List missingExcerptLines = []; + 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 = ' ' + '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 contents = package.readmeFile.readAsLinesSync(); - + List 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 actuallySupportedPlatform = platformSupportMaps.keys.toSet().cast(); - final Iterable documentedPlatforms = contents[osLineNumber] + final Iterable documentedPlatforms = readmeLines[osLineNumber] .split('|') .map((String entry) => entry.trim()) .where((String entry) => entry.isNotEmpty); diff --git a/script/tool/pubspec.yaml b/script/tool/pubspec.yaml index 9f9910f934f7..af38193294a5 100644 --- a/script/tool/pubspec.yaml +++ b/script/tool/pubspec.yaml @@ -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 diff --git a/script/tool/test/readme_check_command_test.dart b/script/tool/test/readme_check_command_test.dart index aec2fa078454..b6e016dccab4 100644 --- a/script/tool/test/readme_check_command_test.dart +++ b/script/tool/test/readme_check_command_test.dart @@ -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 output = await runCapturingPrint( + runner, ['readme-check'], errorHandler: (Error e) { + commandError = e; + }); + + expect(commandError, isA()); + expect( + output, + containsAllInOrder([ + 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 output = await runCapturingPrint(runner, [ + 'readme-check', + ]); + + expect( + output, + containsAll([ + 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 output = await runCapturingPrint(runner, [ + 'readme-check', + ]); + + expect( + output, + containsAll([ + 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: + + +```dart +A B C +``` +'''); + + final List output = await runCapturingPrint( + runner, ['readme-check', '--require-excerpts']); + + expect( + output, + containsAll([ + 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 output = await runCapturingPrint( + runner, ['readme-check', '--require-excerpts'], + errorHandler: (Error e) { + commandError = e; + }); + + expect(commandError, isA()); + expect( + output, + containsAllInOrder([ + contains('Dart code block at line 3 is not managed by code-excerpt.'), + contains('Missing code-excerpt management for code block'), + ]), + ); + }); + }); }