diff --git a/dev/bots/analyze.dart b/dev/bots/analyze.dart index 19056c54f3..9e87d94f0e 100644 --- a/dev/bots/analyze.dart +++ b/dev/bots/analyze.dart @@ -187,6 +187,13 @@ Future run(List arguments) async { workingDirectory: flutterRoot, ); + // Make sure that all of the existing samples are linked from at least one API doc comment. + printProgress('Code sample link validation...'); + await runCommand(dart, + ['--enable-asserts', path.join(flutterRoot, 'dev', 'bots', 'check_code_samples.dart')], + workingDirectory: flutterRoot, + ); + // Try analysis against a big version of the gallery; generate into a temporary directory. printProgress('Dart analysis (mega gallery)...'); final Directory outDir = Directory.systemTemp.createTempSync('flutter_mega_gallery.'); diff --git a/dev/bots/analyze_snippet_code.dart b/dev/bots/analyze_snippet_code.dart index 119311239e..5669f0cb40 100644 --- a/dev/bots/analyze_snippet_code.dart +++ b/dev/bots/analyze_snippet_code.dart @@ -5,7 +5,7 @@ // To run this, from the root of the Flutter repository: // bin/cache/dart-sdk/bin/dart --enable-asserts dev/bots/analyze_snippet_code.dart -// In general, please prefer using full inline examples in API docs. +// In general, please prefer using full linked examples in API docs. // // For documentation on creating sample code, see ../../examples/api/README.md // See also our style guide's discussion on documentation and sample code: @@ -13,7 +13,7 @@ // // This tool is used to analyze smaller snippets of code in the API docs. // Such snippets are wrapped in ```dart ... ``` blocks, which may themselves -// be wrapped in {@tool snippet} ... {@endtool} blocks to set them apart +// be wrapped in {@tool snippet} ... {@end-tool} blocks to set them apart // in the rendered output. // // Such snippets: diff --git a/dev/bots/check_code_samples.dart b/dev/bots/check_code_samples.dart new file mode 100644 index 0000000000..0ffd56e510 --- /dev/null +++ b/dev/bots/check_code_samples.dart @@ -0,0 +1,481 @@ +// 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. + +// To run this, from the root of the Flutter repository: +// bin/cache/dart-sdk/bin/dart --enable-asserts dev/bots/check_code_sample_links.dart + +import 'dart:io'; + +import 'package:args/args.dart'; +import 'package:file/file.dart'; +import 'package:file/local.dart'; +import 'package:path/path.dart' as path; + +import 'utils.dart'; + +final String _scriptLocation = path.fromUri(Platform.script); +final String _flutterRoot = path.dirname(path.dirname(path.dirname(_scriptLocation))); +final String _exampleDirectoryPath = path.join(_flutterRoot, 'examples', 'api'); +final String _packageDirectoryPath = path.join(_flutterRoot, 'packages'); +final String _dartUIDirectoryPath = path.join(_flutterRoot, 'bin', 'cache', 'pkg', 'sky_engine', 'lib'); + +final List _knownUnlinkedExamples = [ + // These are template files that aren't expected to be linked. + 'examples/api/lib/sample_templates/cupertino.0.dart', + 'examples/api/lib/sample_templates/widgets.0.dart', + 'examples/api/lib/sample_templates/material.0.dart', +]; + +void main(List args) { + final ArgParser argParser = ArgParser(); + argParser.addFlag( + 'help', + negatable: false, + help: 'Print help for this command.', + ); + argParser.addOption( + 'examples', + valueHelp: 'path', + defaultsTo: _exampleDirectoryPath, + help: 'A location where the API doc examples are found.', + ); + argParser.addOption( + 'packages', + valueHelp: 'path', + defaultsTo: _packageDirectoryPath, + help: 'A location where the source code that should link the API doc examples is found.', + ); + argParser.addOption( + 'dart-ui', + valueHelp: 'path', + defaultsTo: _dartUIDirectoryPath, + help: 'A location where the source code that should link the API doc examples is found.', + ); + argParser.addOption( + 'flutter-root', + valueHelp: 'path', + defaultsTo: _flutterRoot, + help: 'The path to the root of the Flutter repo.', + ); + final ArgResults parsedArgs; + + void usage() { + print('dart --enable-asserts ${path.basename(_scriptLocation)} [options]'); + print(argParser.usage); + } + + try { + parsedArgs = argParser.parse(args); + } on FormatException catch (e) { + print(e.message); + usage(); + exit(1); + } + + if (parsedArgs['help'] as bool) { + usage(); + exit(0); + } + + const FileSystem filesystem = LocalFileSystem(); + final Directory examples = filesystem.directory(parsedArgs['examples']! as String); + final Directory packages = filesystem.directory(parsedArgs['packages']! as String); + final Directory dartUIPath = filesystem.directory(parsedArgs['dart-ui']! as String); + final Directory flutterRoot = filesystem.directory(parsedArgs['flutter-root']! as String); + + final SampleChecker checker = SampleChecker( + examples: examples, + packages: packages, + dartUIPath: dartUIPath, + flutterRoot: flutterRoot, + ); + + if (!checker.checkCodeSamples()) { + reportErrorsAndExit('Some errors were found in the API docs code samples.'); + } + reportSuccessAndExit('All examples are linked and have tests.'); +} + +class SampleChecker { + SampleChecker({ + required this.examples, + required this.packages, + required this.dartUIPath, + required this.flutterRoot, + this.filesystem = const LocalFileSystem(), + }); + + final Directory examples; + final Directory packages; + final Directory dartUIPath; + final Directory flutterRoot; + final FileSystem filesystem; + + bool checkCodeSamples() { + filesystem.currentDirectory = flutterRoot; + + // Get a list of all the filenames in the source directory that end in "[0-9]+.dart". + final List exampleFilenames = getExampleFilenames(examples); + + // Get a list of all the example link paths that appear in the source files. + final Set exampleLinks = getExampleLinks(packages); + + // Also add in any that might be found in the dart:ui directory. + exampleLinks.addAll(getExampleLinks(dartUIPath)); + + // Get a list of the filenames that were not found in the source files. + final List missingFilenames = checkForMissingLinks(exampleFilenames, exampleLinks); + + // Get a list of any tests that are missing, as well as any that used to be + // missing, but have been implemented. + final (List missingTests, List noLongerMissing) = checkForMissingTests(exampleFilenames); + + // Remove any that we know are exceptions (examples that aren't expected to be + // linked into any source files). These are typically template files used to + // generate new examples. + missingFilenames.removeWhere((String file) => _knownUnlinkedExamples.contains(file)); + + if (missingFilenames.isEmpty && missingTests.isEmpty && noLongerMissing.isEmpty) { + return true; + } + + if (noLongerMissing.isNotEmpty) { + final StringBuffer buffer = StringBuffer('The following tests have been implemented! Huzzah!:\n'); + for (final File name in noLongerMissing) { + buffer.writeln(' ${getRelativePath(name)}'); + } + buffer.writeln('However, they now need to be removed from the _knownMissingTests'); + buffer.write('list in the script $_scriptLocation.'); + foundError(buffer.toString().split('\n')); + } + + if (missingTests.isNotEmpty) { + final StringBuffer buffer = StringBuffer('The following example test files are missing:\n'); + for (final File name in missingTests) { + buffer.writeln(' ${getRelativePath(name)}'); + } + foundError(buffer.toString().trimRight().split('\n')); + } + + if (missingFilenames.isNotEmpty) { + final StringBuffer buffer = + StringBuffer('The following examples are not linked from any source file API doc comments:\n'); + for (final String name in missingFilenames) { + buffer.writeln(' $name'); + } + buffer.write('Either link them to a source file API doc comment, or remove them.'); + foundError(buffer.toString().split('\n')); + } + return false; + } + + String getRelativePath(File file, [Directory? root]) { + root ??= flutterRoot; + return path.relative(file.absolute.path, from: root.absolute.path); + } + + List getFiles(Directory directory, [Pattern? filenamePattern]) { + final List filenames = directory + .listSync(recursive: true) + .map((FileSystemEntity entity) { + if (entity is File) { + return entity; + } else { + return null; + } + }) + .where((File? filename) => + filename != null && (filenamePattern == null || filename.absolute.path.contains(filenamePattern))) + .map((File? s) => s!) + .toList(); + return filenames; + } + + List getExampleFilenames(Directory directory) { + return getFiles( + directory.childDirectory('lib'), + RegExp(r'\d+\.dart$'), + ); + } + + Set getExampleLinks(Directory searchDirectory) { + final List files = getFiles(searchDirectory, RegExp(r'\.dart$')); + final Set searchStrings = {}; + final RegExp exampleRe = RegExp(r'\*\* See code in (?.*) \*\*'); + for (final File file in files) { + final String contents = file.readAsStringSync(); + searchStrings.addAll( + contents.split('\n').where((String s) => s.contains(exampleRe)).map( + (String e) { + return exampleRe.firstMatch(e)!.namedGroup('path')!; + }, + ), + ); + } + return searchStrings; + } + + List checkForMissingLinks(List exampleFilenames, Set searchStrings) { + final List missingFilenames = []; + for (final File example in exampleFilenames) { + final String relativePath = getRelativePath(example); + if (!searchStrings.contains(relativePath)) { + missingFilenames.add(relativePath); + } + } + return missingFilenames; + } + + String getTestNameForExample(File example, Directory examples) { + final String testPath = path.dirname( + path.join( + examples.absolute.path, + 'test', + getRelativePath(example, examples.childDirectory('lib')), + ), + ); + return '${path.join(testPath, path.basenameWithoutExtension(example.path))}_test.dart'; + } + + (List, List) checkForMissingTests(List exampleFilenames) { + final List missingTests = []; + final List noLongerMissingTests = []; + for (final File example in exampleFilenames) { + final File testFile = filesystem.file(getTestNameForExample(example, examples)); + final String name = path.relative(testFile.absolute.path, from: flutterRoot.absolute.path); + if (!testFile.existsSync()) { + missingTests.add(testFile); + } else if (_knownMissingTests.contains(name.replaceAll(r'\', '/'))) { + noLongerMissingTests.add(testFile); + } + } + // Skip any that we know are missing. + missingTests.removeWhere( + (File test) { + final String name = path.relative(test.absolute.path, from: flutterRoot.absolute.path).replaceAll(r'\', '/'); + return _knownMissingTests.contains(name); + }, + ); + return (missingTests, noLongerMissingTests); + } +} + +// These tests are known to be missing. They should all eventually be +// implemented, but until they are we allow them, so that we can catch any new +// examples that are added without tests. +// +// TODO(gspencergoog): implement the missing tests. +// See https://github.com/flutter/flutter/issues/130459 +final Set _knownMissingTests = { + 'examples/api/test/cupertino/text_field/cupertino_text_field.0_test.dart', + 'examples/api/test/material/bottom_app_bar/bottom_app_bar.2_test.dart', + 'examples/api/test/material/bottom_app_bar/bottom_app_bar.1_test.dart', + 'examples/api/test/material/theme/theme_extension.1_test.dart', + 'examples/api/test/material/elevated_button/elevated_button.0_test.dart', + 'examples/api/test/material/material_state/material_state_border_side.0_test.dart', + 'examples/api/test/material/material_state/material_state_mouse_cursor.0_test.dart', + 'examples/api/test/material/material_state/material_state_outlined_border.0_test.dart', + 'examples/api/test/material/material_state/material_state_property.0_test.dart', + 'examples/api/test/material/selectable_region/selectable_region.0_test.dart', + 'examples/api/test/material/text_field/text_field.2_test.dart', + 'examples/api/test/material/text_field/text_field.1_test.dart', + 'examples/api/test/material/button_style/button_style.0_test.dart', + 'examples/api/test/material/range_slider/range_slider.0_test.dart', + 'examples/api/test/material/card/card.2_test.dart', + 'examples/api/test/material/card/card.0_test.dart', + 'examples/api/test/material/selection_container/selection_container_disabled.0_test.dart', + 'examples/api/test/material/selection_container/selection_container.0_test.dart', + 'examples/api/test/material/color_scheme/dynamic_content_color.0_test.dart', + 'examples/api/test/material/platform_menu_bar/platform_menu_bar.0_test.dart', + 'examples/api/test/material/menu_anchor/menu_anchor.2_test.dart', + 'examples/api/test/material/stepper/stepper.controls_builder.0_test.dart', + 'examples/api/test/material/stepper/stepper.0_test.dart', + 'examples/api/test/material/flexible_space_bar/flexible_space_bar.0_test.dart', + 'examples/api/test/material/data_table/data_table.1_test.dart', + 'examples/api/test/material/data_table/data_table.0_test.dart', + 'examples/api/test/material/floating_action_button_location/standard_fab_location.0_test.dart', + 'examples/api/test/material/chip/deletable_chip_attributes.on_deleted.0_test.dart', + 'examples/api/test/material/snack_bar/snack_bar.0_test.dart', + 'examples/api/test/material/snack_bar/snack_bar.2_test.dart', + 'examples/api/test/material/snack_bar/snack_bar.1_test.dart', + 'examples/api/test/material/bottom_navigation_bar/bottom_navigation_bar.0_test.dart', + 'examples/api/test/material/bottom_navigation_bar/bottom_navigation_bar.1_test.dart', + 'examples/api/test/material/outlined_button/outlined_button.0_test.dart', + 'examples/api/test/material/icon_button/icon_button.2_test.dart', + 'examples/api/test/material/icon_button/icon_button.3_test.dart', + 'examples/api/test/material/icon_button/icon_button.0_test.dart', + 'examples/api/test/material/icon_button/icon_button.1_test.dart', + 'examples/api/test/material/expansion_panel/expansion_panel_list.0_test.dart', + 'examples/api/test/material/expansion_panel/expansion_panel_list.expansion_panel_list_radio.0_test.dart', + 'examples/api/test/material/input_decorator/input_decoration.1_test.dart', + 'examples/api/test/material/input_decorator/input_decoration.prefix_icon_constraints.0_test.dart', + 'examples/api/test/material/input_decorator/input_decoration.material_state.0_test.dart', + 'examples/api/test/material/input_decorator/input_decoration.2_test.dart', + 'examples/api/test/material/input_decorator/input_decoration.0_test.dart', + 'examples/api/test/material/input_decorator/input_decoration.label.0_test.dart', + 'examples/api/test/material/input_decorator/input_decoration.suffix_icon_constraints.0_test.dart', + 'examples/api/test/material/input_decorator/input_decoration.3_test.dart', + 'examples/api/test/material/input_decorator/input_decoration.material_state.1_test.dart', + 'examples/api/test/material/filled_button/filled_button.0_test.dart', + 'examples/api/test/material/text_form_field/text_form_field.1_test.dart', + 'examples/api/test/material/scrollbar/scrollbar.1_test.dart', + 'examples/api/test/material/scrollbar/scrollbar.0_test.dart', + 'examples/api/test/material/dropdown_menu/dropdown_menu.1_test.dart', + 'examples/api/test/material/dropdown_menu/dropdown_menu.0_test.dart', + 'examples/api/test/material/radio/radio.toggleable.0_test.dart', + 'examples/api/test/material/radio/radio.0_test.dart', + 'examples/api/test/material/search_anchor/search_anchor.0_test.dart', + 'examples/api/test/material/search_anchor/search_anchor.1_test.dart', + 'examples/api/test/material/search_anchor/search_anchor.2_test.dart', + 'examples/api/test/material/about/about_list_tile.0_test.dart', + 'examples/api/test/material/tab_controller/tab_controller.1_test.dart', + 'examples/api/test/material/selection_area/selection_area.0_test.dart', + 'examples/api/test/material/scaffold/scaffold.end_drawer.0_test.dart', + 'examples/api/test/material/scaffold/scaffold.drawer.0_test.dart', + 'examples/api/test/material/scaffold/scaffold.1_test.dart', + 'examples/api/test/material/scaffold/scaffold.of.0_test.dart', + 'examples/api/test/material/scaffold/scaffold_messenger.of.0_test.dart', + 'examples/api/test/material/scaffold/scaffold_messenger.0_test.dart', + 'examples/api/test/material/scaffold/scaffold.0_test.dart', + 'examples/api/test/material/scaffold/scaffold_state.show_bottom_sheet.0_test.dart', + 'examples/api/test/material/scaffold/scaffold.2_test.dart', + 'examples/api/test/material/scaffold/scaffold_messenger_state.show_material_banner.0_test.dart', + 'examples/api/test/material/scaffold/scaffold.of.1_test.dart', + 'examples/api/test/material/scaffold/scaffold_messenger.of.1_test.dart', + 'examples/api/test/material/scaffold/scaffold_messenger_state.show_snack_bar.0_test.dart', + 'examples/api/test/material/segmented_button/segmented_button.0_test.dart', + 'examples/api/test/material/app_bar/app_bar.2_test.dart', + 'examples/api/test/material/app_bar/sliver_app_bar.1_test.dart', + 'examples/api/test/material/app_bar/sliver_app_bar.2_test.dart', + 'examples/api/test/material/app_bar/sliver_app_bar.3_test.dart', + 'examples/api/test/material/app_bar/app_bar.1_test.dart', + 'examples/api/test/material/app_bar/sliver_app_bar.4_test.dart', + 'examples/api/test/material/app_bar/app_bar.3_test.dart', + 'examples/api/test/material/app_bar/app_bar.0_test.dart', + 'examples/api/test/material/ink_well/ink_well.0_test.dart', + 'examples/api/test/material/banner/material_banner.1_test.dart', + 'examples/api/test/material/banner/material_banner.0_test.dart', + 'examples/api/test/material/checkbox/checkbox.1_test.dart', + 'examples/api/test/material/checkbox/checkbox.0_test.dart', + 'examples/api/test/material/navigation_rail/navigation_rail.extended_animation.0_test.dart', + 'examples/api/test/material/text_button/text_button.0_test.dart', + 'examples/api/test/rendering/growth_direction/growth_direction.0_test.dart', + 'examples/api/test/rendering/sliver_grid/sliver_grid_delegate_with_fixed_cross_axis_count.0_test.dart', + 'examples/api/test/rendering/sliver_grid/sliver_grid_delegate_with_fixed_cross_axis_count.1_test.dart', + 'examples/api/test/rendering/scroll_direction/scroll_direction.0_test.dart', + 'examples/api/test/painting/axis_direction/axis_direction.0_test.dart', + 'examples/api/test/painting/linear_border/linear_border.0_test.dart', + 'examples/api/test/painting/gradient/linear_gradient.0_test.dart', + 'examples/api/test/painting/star_border/star_border.0_test.dart', + 'examples/api/test/painting/borders/border_side.stroke_align.0_test.dart', + 'examples/api/test/widgets/autocomplete/raw_autocomplete.focus_node.0_test.dart', + 'examples/api/test/widgets/autocomplete/raw_autocomplete.2_test.dart', + 'examples/api/test/widgets/autocomplete/raw_autocomplete.1_test.dart', + 'examples/api/test/widgets/autocomplete/raw_autocomplete.0_test.dart', + 'examples/api/test/widgets/navigator/navigator.restorable_push_and_remove_until.0_test.dart', + 'examples/api/test/widgets/navigator/navigator.0_test.dart', + 'examples/api/test/widgets/navigator/navigator.restorable_push.0_test.dart', + 'examples/api/test/widgets/navigator/navigator_state.restorable_push_replacement.0_test.dart', + 'examples/api/test/widgets/navigator/navigator_state.restorable_push_and_remove_until.0_test.dart', + 'examples/api/test/widgets/navigator/navigator.restorable_push_replacement.0_test.dart', + 'examples/api/test/widgets/navigator/restorable_route_future.0_test.dart', + 'examples/api/test/widgets/navigator/navigator_state.restorable_push.0_test.dart', + 'examples/api/test/widgets/focus_manager/focus_node.unfocus.0_test.dart', + 'examples/api/test/widgets/focus_manager/focus_node.0_test.dart', + 'examples/api/test/widgets/framework/build_owner.0_test.dart', + 'examples/api/test/widgets/framework/error_widget.0_test.dart', + 'examples/api/test/widgets/inherited_theme/inherited_theme.0_test.dart', + 'examples/api/test/widgets/sliver/decorated_sliver.0_test.dart', + 'examples/api/test/widgets/autofill/autofill_group.0_test.dart', + 'examples/api/test/widgets/drag_target/draggable.0_test.dart', + 'examples/api/test/widgets/shared_app_data/shared_app_data.1_test.dart', + 'examples/api/test/widgets/shared_app_data/shared_app_data.0_test.dart', + 'examples/api/test/widgets/form/form.0_test.dart', + 'examples/api/test/widgets/nested_scroll_view/nested_scroll_view_state.0_test.dart', + 'examples/api/test/widgets/nested_scroll_view/nested_scroll_view.2_test.dart', + 'examples/api/test/widgets/nested_scroll_view/nested_scroll_view.1_test.dart', + 'examples/api/test/widgets/nested_scroll_view/nested_scroll_view.0_test.dart', + 'examples/api/test/widgets/page_view/page_view.0_test.dart', + 'examples/api/test/widgets/scroll_position/scroll_metrics_notification.0_test.dart', + 'examples/api/test/widgets/media_query/media_query_data.system_gesture_insets.0_test.dart', + 'examples/api/test/widgets/async/stream_builder.0_test.dart', + 'examples/api/test/widgets/async/future_builder.0_test.dart', + 'examples/api/test/widgets/restoration_properties/restorable_value.0_test.dart', + 'examples/api/test/widgets/animated_size/animated_size.0_test.dart', + 'examples/api/test/widgets/table/table.0_test.dart', + 'examples/api/test/widgets/animated_switcher/animated_switcher.0_test.dart', + 'examples/api/test/widgets/transitions/relative_positioned_transition.0_test.dart', + 'examples/api/test/widgets/transitions/positioned_transition.0_test.dart', + 'examples/api/test/widgets/transitions/listenable_builder.3_test.dart', + 'examples/api/test/widgets/transitions/sliver_fade_transition.0_test.dart', + 'examples/api/test/widgets/transitions/align_transition.0_test.dart', + 'examples/api/test/widgets/transitions/fade_transition.0_test.dart', + 'examples/api/test/widgets/transitions/animated_builder.0_test.dart', + 'examples/api/test/widgets/transitions/rotation_transition.0_test.dart', + 'examples/api/test/widgets/transitions/animated_widget.0_test.dart', + 'examples/api/test/widgets/transitions/slide_transition.0_test.dart', + 'examples/api/test/widgets/transitions/listenable_builder.2_test.dart', + 'examples/api/test/widgets/transitions/scale_transition.0_test.dart', + 'examples/api/test/widgets/transitions/default_text_style_transition.0_test.dart', + 'examples/api/test/widgets/transitions/decorated_box_transition.0_test.dart', + 'examples/api/test/widgets/transitions/size_transition.0_test.dart', + 'examples/api/test/widgets/animated_list/animated_list.0_test.dart', + 'examples/api/test/widgets/focus_traversal/focus_traversal_group.0_test.dart', + 'examples/api/test/widgets/focus_traversal/ordered_traversal_policy.0_test.dart', + 'examples/api/test/widgets/image/image.error_builder.0_test.dart', + 'examples/api/test/widgets/image/image.frame_builder.0_test.dart', + 'examples/api/test/widgets/image/image.loading_builder.0_test.dart', + 'examples/api/test/widgets/shortcuts/logical_key_set.0_test.dart', + 'examples/api/test/widgets/shortcuts/shortcuts.0_test.dart', + 'examples/api/test/widgets/shortcuts/single_activator.single_activator.0_test.dart', + 'examples/api/test/widgets/shortcuts/shortcuts.1_test.dart', + 'examples/api/test/widgets/shortcuts/character_activator.0_test.dart', + 'examples/api/test/widgets/shortcuts/callback_shortcuts.0_test.dart', + 'examples/api/test/widgets/page_storage/page_storage.0_test.dart', + 'examples/api/test/widgets/scrollbar/raw_scrollbar.1_test.dart', + 'examples/api/test/widgets/scrollbar/raw_scrollbar.2_test.dart', + 'examples/api/test/widgets/scrollbar/raw_scrollbar.desktop.0_test.dart', + 'examples/api/test/widgets/scrollbar/raw_scrollbar.shape.0_test.dart', + 'examples/api/test/widgets/scrollbar/raw_scrollbar.0_test.dart', + 'examples/api/test/widgets/sliver_fill/sliver_fill_remaining.2_test.dart', + 'examples/api/test/widgets/sliver_fill/sliver_fill_remaining.1_test.dart', + 'examples/api/test/widgets/sliver_fill/sliver_fill_remaining.3_test.dart', + 'examples/api/test/widgets/sliver_fill/sliver_fill_remaining.0_test.dart', + 'examples/api/test/widgets/interactive_viewer/interactive_viewer.constrained.0_test.dart', + 'examples/api/test/widgets/interactive_viewer/interactive_viewer.transformation_controller.0_test.dart', + 'examples/api/test/widgets/interactive_viewer/interactive_viewer.0_test.dart', + 'examples/api/test/widgets/notification_listener/notification.0_test.dart', + 'examples/api/test/widgets/gesture_detector/gesture_detector.1_test.dart', + 'examples/api/test/widgets/gesture_detector/gesture_detector.0_test.dart', + 'examples/api/test/widgets/editable_text/text_editing_controller.0_test.dart', + 'examples/api/test/widgets/editable_text/editable_text.on_changed.0_test.dart', + 'examples/api/test/widgets/undo_history/undo_history_controller.0_test.dart', + 'examples/api/test/widgets/overscroll_indicator/glowing_overscroll_indicator.1_test.dart', + 'examples/api/test/widgets/overscroll_indicator/glowing_overscroll_indicator.0_test.dart', + 'examples/api/test/widgets/tween_animation_builder/tween_animation_builder.0_test.dart', + 'examples/api/test/widgets/single_child_scroll_view/single_child_scroll_view.1_test.dart', + 'examples/api/test/widgets/single_child_scroll_view/single_child_scroll_view.0_test.dart', + 'examples/api/test/widgets/overflow_bar/overflow_bar.0_test.dart', + 'examples/api/test/widgets/restoration/restoration_mixin.0_test.dart', + 'examples/api/test/widgets/actions/actions.0_test.dart', + 'examples/api/test/widgets/actions/action_listener.0_test.dart', + 'examples/api/test/widgets/actions/focusable_action_detector.0_test.dart', + 'examples/api/test/widgets/color_filter/color_filtered.0_test.dart', + 'examples/api/test/widgets/focus_scope/focus.2_test.dart', + 'examples/api/test/widgets/focus_scope/focus.0_test.dart', + 'examples/api/test/widgets/focus_scope/focus.1_test.dart', + 'examples/api/test/widgets/focus_scope/focus_scope.0_test.dart', + 'examples/api/test/widgets/implicit_animations/animated_fractionally_sized_box.0_test.dart', + 'examples/api/test/widgets/implicit_animations/animated_align.0_test.dart', + 'examples/api/test/widgets/implicit_animations/animated_positioned.0_test.dart', + 'examples/api/test/widgets/implicit_animations/animated_padding.0_test.dart', + 'examples/api/test/widgets/implicit_animations/sliver_animated_opacity.0_test.dart', + 'examples/api/test/widgets/implicit_animations/animated_container.0_test.dart', + 'examples/api/test/widgets/dismissible/dismissible.0_test.dart', + 'examples/api/test/widgets/scroll_view/custom_scroll_view.1_test.dart', + 'examples/api/test/widgets/preferred_size/preferred_size.0_test.dart', + 'examples/api/test/widgets/inherited_notifier/inherited_notifier.0_test.dart', + 'examples/api/test/animation/curves/curve2_d.0_test.dart', + 'examples/api/test/gestures/pointer_signal_resolver/pointer_signal_resolver.0_test.dart', +}; diff --git a/dev/bots/pubspec.yaml b/dev/bots/pubspec.yaml index 970d1e1a6e..c992ede277 100644 --- a/dev/bots/pubspec.yaml +++ b/dev/bots/pubspec.yaml @@ -8,6 +8,7 @@ dependencies: args: 2.4.2 crypto: 3.0.3 intl: 0.18.1 + file: 6.1.4 flutter_devicelab: path: ../devicelab http_parser: 4.0.2 @@ -29,7 +30,6 @@ dependencies: convert: 3.1.1 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" coverage: 1.6.3 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" dart_internal: 0.2.8 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" - file: 6.1.4 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" frontend_server_client: 3.2.0 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" gcloud: 0.8.9 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" glob: 2.1.2 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" diff --git a/dev/bots/test/check_code_samples_test.dart b/dev/bots/test/check_code_samples_test.dart new file mode 100644 index 0000000000..2fcae906f3 --- /dev/null +++ b/dev/bots/test/check_code_samples_test.dart @@ -0,0 +1,190 @@ +// 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'; + +import 'package:file/file.dart'; +import 'package:file/memory.dart'; +import 'package:path/path.dart' as path; + +import '../check_code_samples.dart'; +import '../utils.dart'; +import 'common.dart'; + +void main() { + late SampleChecker checker; + late FileSystem fs; + late Directory examples; + late Directory packages; + late Directory dartUIPath; + late Directory flutterRoot; + + String getRelativePath(File file, [Directory? from]) { + from ??= flutterRoot; + return path.relative(file.absolute.path, from: flutterRoot.absolute.path); + } + + void writeLink({required File source, required File example}) { + source + ..createSync(recursive: true) + ..writeAsStringSync(''' +/// Class documentation +/// +/// {@tool dartpad} +/// Example description +/// +/// ** See code in ${getRelativePath(example)} ** +/// {@end-tool} +'''); + } + + void buildTestFiles({bool missingLinks = false, bool missingTests = false}) { + final Directory examplesLib = examples.childDirectory('lib').childDirectory('layer')..createSync(recursive: true); + final File fooExample = examplesLib.childFile('foo_example.0.dart') + ..createSync(recursive: true) + ..writeAsStringSync('// Example for foo'); + final File barExample = examplesLib.childFile('bar_example.0.dart') + ..createSync(recursive: true) + ..writeAsStringSync('// Example for bar'); + final File curvesExample = + examples.childDirectory('lib').childDirectory('animation').childDirectory('curves').childFile('curve2_d.0.dart') + ..createSync(recursive: true) + ..writeAsStringSync('// Missing a test, but OK'); + if (missingLinks) { + examplesLib.childFile('missing_example.0.dart') + ..createSync(recursive: true) + ..writeAsStringSync('// Example that is not linked'); + } + final Directory examplesTests = examples.childDirectory('test').childDirectory('layer') + ..createSync(recursive: true); + examplesTests.childFile('foo_example.0_test.dart') + ..createSync(recursive: true) + ..writeAsStringSync('// test for foo example'); + if (!missingTests) { + examplesTests.childFile('bar_example.0_test.dart') + ..createSync(recursive: true) + ..writeAsStringSync('// test for bar example'); + } + if (missingLinks) { + examplesTests.childFile('missing_example.0_test.dart') + ..createSync(recursive: true) + ..writeAsStringSync('// test for foo example'); + } + final Directory flutterPackage = packages.childDirectory('flutter').childDirectory('lib').childDirectory('src') + ..createSync(recursive: true); + writeLink(source: flutterPackage.childDirectory('layer').childFile('foo.dart'), example: fooExample); + writeLink(source: flutterPackage.childDirectory('layer').childFile('bar.dart'), example: barExample); + writeLink(source: flutterPackage.childDirectory('animation').childFile('curves.dart'), example: curvesExample); + } + + setUp(() { + fs = MemoryFileSystem(style: Platform.isWindows ? FileSystemStyle.windows : FileSystemStyle.posix); + // Get the root prefix of the current directory so that on Windows we get a + // correct root prefix. + flutterRoot = fs.directory(path.join(path.rootPrefix(fs.currentDirectory.absolute.path), 'flutter sdk'))..createSync(recursive: true); + fs.currentDirectory = flutterRoot; + examples = flutterRoot.childDirectory('examples').childDirectory('api')..createSync(recursive: true); + packages = flutterRoot.childDirectory('packages')..createSync(recursive: true); + dartUIPath = flutterRoot + .childDirectory('bin') + .childDirectory('cache') + .childDirectory('pkg') + .childDirectory('sky_engine') + .childDirectory('lib') + ..createSync(recursive: true); + checker = SampleChecker( + examples: examples, + packages: packages, + dartUIPath: dartUIPath, + flutterRoot: flutterRoot, + filesystem: fs, + ); + }); + + test('check_code_samples.dart - checkCodeSamples catches missing links', () async { + buildTestFiles(missingLinks: true); + bool? success; + final String result = await capture( + () async { + success = checker.checkCodeSamples(); + }, + shouldHaveErrors: true, + ); + final String lines = [ + '╔═╡ERROR╞═══════════════════════════════════════════════════════════════════════', + '║ The following examples are not linked from any source file API doc comments:', + '║ examples/api/lib/layer/missing_example.0.dart', + '║ Either link them to a source file API doc comment, or remove them.', + '╚═══════════════════════════════════════════════════════════════════════════════', + ].map((String line) { + return line.replaceAll('/', Platform.isWindows ? r'\' : '/'); + }).join('\n'); + expect(result, equals('$lines\n')); + expect(success, equals(false)); + }); + + test('check_code_samples.dart - checkCodeSamples catches missing tests', () async { + buildTestFiles(missingTests: true); + bool? success; + final String result = await capture( + () async { + success = checker.checkCodeSamples(); + }, + shouldHaveErrors: true, + ); + final String lines = [ + '╔═╡ERROR╞═══════════════════════════════════════════════════════════════════════', + '║ The following example test files are missing:', + '║ examples/api/test/layer/bar_example.0_test.dart', + '╚═══════════════════════════════════════════════════════════════════════════════', + ].map((String line) { + return line.replaceAll('/', Platform.isWindows ? r'\' : '/'); + }).join('\n'); + expect(result, equals('$lines\n')); + expect(success, equals(false)); + }); + + test('check_code_samples.dart - checkCodeSamples succeeds', () async { + buildTestFiles(); + bool? success; + final String result = await capture( + () async { + success = checker.checkCodeSamples(); + }, + ); + expect(result, isEmpty); + expect(success, equals(true)); + }); +} + +typedef AsyncVoidCallback = Future Function(); + +Future capture(AsyncVoidCallback callback, {bool shouldHaveErrors = false}) async { + final StringBuffer buffer = StringBuffer(); + final PrintCallback oldPrint = print; + try { + print = (Object? line) { + buffer.writeln(line); + }; + await callback(); + expect( + hasError, + shouldHaveErrors, + reason: buffer.isEmpty + ? '(No output to report.)' + : hasError + ? 'Unexpected errors:\n$buffer' + : 'Unexpected success:\n$buffer', + ); + } finally { + print = oldPrint; + resetErrorStatus(); + } + if (stdout.supportsAnsiEscapes) { + // Remove ANSI escapes when this test is running on a terminal. + return buffer.toString().replaceAll(RegExp(r'(\x9B|\x1B\[)[0-?]{1,3}[ -/]*[@-~]'), ''); + } else { + return buffer.toString(); + } +} diff --git a/packages/flutter/lib/src/material/checkbox.dart b/packages/flutter/lib/src/material/checkbox.dart index bfead13fe1..518351ae9c 100644 --- a/packages/flutter/lib/src/material/checkbox.dart +++ b/packages/flutter/lib/src/material/checkbox.dart @@ -44,6 +44,12 @@ enum _CheckboxType { material, adaptive } /// ** See code in examples/api/lib/material/checkbox/checkbox.0.dart ** /// {@end-tool} /// +/// {@tool dartpad} +/// This example shows what the checkbox error state looks like. +/// +/// ** See code in examples/api/lib/material/checkbox/checkbox.1.dart ** +/// {@end-tool} +/// /// See also: /// /// * [CheckboxListTile], which combines this widget with a [ListTile] so that diff --git a/packages/flutter/lib/src/material/drawer.dart b/packages/flutter/lib/src/material/drawer.dart index ecaecb2c0d..c2b78ce097 100644 --- a/packages/flutter/lib/src/material/drawer.dart +++ b/packages/flutter/lib/src/material/drawer.dart @@ -90,50 +90,13 @@ const Duration _kBaseSettleDuration = Duration(milliseconds: 246); /// The [Drawer] displays the four items using a [ListView], which allows the /// user to scroll through the items if need be. /// -/// ```dart -/// Scaffold( -/// appBar: AppBar( -/// title: const Text('Drawer Demo'), -/// ), -/// drawer: Drawer( -/// child: ListView( -/// padding: EdgeInsets.zero, -/// children: const [ -/// DrawerHeader( -/// decoration: BoxDecoration( -/// color: Colors.blue, -/// ), -/// child: Text( -/// 'Drawer Header', -/// style: TextStyle( -/// color: Colors.white, -/// fontSize: 24, -/// ), -/// ), -/// ), -/// ListTile( -/// leading: Icon(Icons.message), -/// title: Text('Messages'), -/// ), -/// ListTile( -/// leading: Icon(Icons.account_circle), -/// title: Text('Profile'), -/// ), -/// ListTile( -/// leading: Icon(Icons.settings), -/// title: Text('Settings'), -/// ), -/// ], -/// ), -/// ), -/// ) -/// ``` +/// ** See code in examples/api/lib/material/drawer/drawer.0.dart ** /// {@end-tool} /// /// {@tool snippet} /// This example shows how to migrate the above [Drawer] to a [NavigationDrawer]. /// -/// See code in examples/api/lib/material/navigation_drawer/navigation_drawer.1.dart ** +/// ** See code in examples/api/lib/material/navigation_drawer/navigation_drawer.1.dart ** /// {@end-tool} /// /// An open drawer may be closed with a swipe to close gesture, pressing the