Unverified Commit 8b768de1 authored by Greg Spencer's avatar Greg Spencer Committed by GitHub

Add lint check to make sure samples are linked and have tests (#130523)

## Description

This adds a checker that will check all of the API docs examples to make sure that they are linked from at least one source file in the packages.

It also checks to make sure that all of the examples have an associated test. Since there are a large number that don't have tests at the moment, there is also a large exception list that can be burned down (burn down list is in https://github.com/flutter/flutter/issues/130459).

Because there are missing links currently, this PR will only pass after https://github.com/flutter/flutter/pull/130521 is merged.

## Related Issues

- https://github.com/flutter/flutter/issues/129956
- https://github.com/flutter/flutter/issues/130459

## Tests

- Added test for the checker.
parent 3b0b8348
......@@ -187,6 +187,13 @@ Future<void> run(List<String> 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,
<String>['--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.');
......
......@@ -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:
......
This diff is collapsed.
......@@ -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"
......
// 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 = <String>[
'╔═╡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 = <String>[
'╔═╡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<void> Function();
Future<String> 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();
}
}
......@@ -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
......
......@@ -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 <Widget>[
/// 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
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment