Fix document generation, eliminate template support from snippets tool. (#147893)

## Description

This fixes the API doc generation that I broke when I moved the snippets tool into the framework.

It removes the last of the template support (properly this time), and makes sure all of the tests pass.

## Related Issues
 - https://github.com/flutter/flutter/issues/144408
 - https://github.com/flutter/flutter/issues/147609
 - https://github.com/flutter/flutter/pull/147645

## Tests
 - Fixed tests, including smoke test of doc generation.
This commit is contained in:
Greg Spencer 2024-05-06 19:13:54 -07:00 committed by GitHub
parent 91f2350a07
commit 8a7c18c12a
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
11 changed files with 70 additions and 266 deletions

View File

@ -113,33 +113,6 @@ demonstrate the API's functionality in a sample application, or used with the
`dartpad` samples are embedded into the API docs web page and are live
applications in the API documentation.
```dart
/// {@tool sample --template=stateless_widget_material}
/// This example shows how to make a simple [FloatingActionButton] in a
/// [Scaffold], with a pink [backgroundColor] and a thumbs up [Icon].
///
/// ```dart
/// Widget build(BuildContext context) {
/// return Scaffold(
/// appBar: AppBar(
/// title: Text('Floating Action Button Sample'),
/// ),
/// body: Center(
/// child: Text('Press the button below!')
/// ),
/// floatingActionButton: FloatingActionButton(
/// onPressed: () {
/// // Add your onPressed code here!
/// },
/// child: Icon(Icons.thumb_up),
/// backgroundColor: Colors.pink,
/// ),
/// );
/// }
/// ```
/// {@end-tool}
```
This uses the skeleton for [application](https://github.com/flutter/flutter/blob/main/dev/snippets/config/skeletons/sample.html)
snippets in the Flutter repo.

View File

@ -21,7 +21,6 @@ const String _kOutputDirectoryOption = 'output-directory';
const String _kOutputOption = 'output';
const String _kPackageOption = 'package';
const String _kSerialOption = 'serial';
const String _kTemplateOption = 'template';
const String _kTypeOption = 'type';
class GitStatusFailed implements Exception {
@ -126,22 +125,14 @@ void main(List<String> argList) {
allowed: sampleTypes,
allowedHelp: <String, String>{
'dartpad':
'Produce a code sample application complete with embedding the sample in an '
'application template for using in Dartpad.',
'Produce a code sample application for using in Dartpad.',
'sample':
'Produce a code sample application complete with embedding the sample in an '
'application template.',
'Produce a code sample application.',
'snippet':
'Produce a nicely formatted piece of sample code. Does not embed the '
'sample into an application template.',
'Produce a nicely formatted piece of sample code.',
},
help: 'The type of snippet to produce.',
);
// TODO(goderbauer): Remove template support, this is no longer used.
parser.addOption(
_kTemplateOption,
help: 'The name of the template to inject the code into.',
);
parser.addOption(
_kOutputOption,
help: 'The output name for the generated sample application. Overrides '

View File

@ -11,7 +11,6 @@ class SnippetConfiguration {
const SnippetConfiguration({
required this.configDirectory,
required this.skeletonsDirectory,
required this.templatesDirectory,
this.filesystem = const LocalFileSystem(),
});
@ -25,10 +24,6 @@ class SnippetConfiguration {
/// and returned to dartdoc for insertion in the output.
final Directory skeletonsDirectory;
/// The directory containing the code templates that can be referenced by the
/// dartdoc.
final Directory templatesDirectory;
/// Gets the skeleton file to use for the given [SampleType] and DartPad
/// preference.
File getHtmlSkeletonFile(String type) {
@ -47,11 +42,6 @@ class FlutterRepoSnippetConfiguration extends SnippetConfiguration {
const <String>['dev', 'snippets', 'config']),
skeletonsDirectory: _underRoot(filesystem, flutterRoot,
const <String>['dev', 'snippets', 'config', 'skeletons']),
templatesDirectory: _underRoot(
filesystem,
flutterRoot,
const <String>['dev', 'snippets', 'config', 'templates'],
),
);
final Directory flutterRoot;

View File

@ -58,8 +58,8 @@ class SourceLine {
/// A class containing the name and contents associated with a code block inside of a
/// code sample, for named injection into a template.
class TemplateInjection {
TemplateInjection(this.name, this.contents, {this.language = ''});
class SkeletonInjection {
SkeletonInjection(this.name, this.contents, {this.language = ''});
final String name;
final List<SourceLine> contents;
final String language;
@ -137,7 +137,7 @@ abstract class CodeSample {
String get element => start.element ?? '';
String output = '';
Map<String, Object?> metadata = <String, Object?>{};
List<TemplateInjection> parts = <TemplateInjection>[];
List<SkeletonInjection> parts = <SkeletonInjection>[];
SourceLine get start => input.isEmpty ? _lineProto : input.first;
String get template {

View File

@ -356,7 +356,7 @@ class _DirectiveInfo implements Comparable<_DirectiveInfo> {
if (priority == other.priority) {
return _compareUri(uri, other.uri);
}
return priority.ordinal - other.priority.ordinal;
return priority.index - other.priority.index;
}
@override
@ -387,23 +387,15 @@ class _DirectiveInfo implements Comparable<_DirectiveInfo> {
}
enum _DirectivePriority {
IMPORT_SDK('IMPORT_SDK', 0),
IMPORT_PKG('IMPORT_PKG', 1),
IMPORT_OTHER('IMPORT_OTHER', 2),
IMPORT_REL('IMPORT_REL', 3),
EXPORT_SDK('EXPORT_SDK', 4),
EXPORT_PKG('EXPORT_PKG', 5),
EXPORT_OTHER('EXPORT_OTHER', 6),
EXPORT_REL('EXPORT_REL', 7),
PART('PART', 8);
const _DirectivePriority(this.name, this.ordinal);
final String name;
final int ordinal;
@override
String toString() => name;
IMPORT_SDK,
IMPORT_PKG,
IMPORT_OTHER,
IMPORT_REL,
EXPORT_SDK,
EXPORT_PKG,
EXPORT_OTHER,
EXPORT_REL,
PART
}
/// SourceEdit

View File

@ -43,30 +43,9 @@ class SnippetGenerator {
static DartFormatter formatter =
DartFormatter(pageWidth: 80, fixes: StyleFix.all);
/// Gets the path to the template file requested.
File? getTemplatePath(String templateName, {Directory? templatesDir}) {
final Directory templateDir =
templatesDir ?? configuration.templatesDirectory;
final File templateFile = configuration.filesystem
.file(path.join(templateDir.path, '$templateName.tmpl'));
return templateFile.existsSync() ? templateFile : null;
}
/// Returns an iterable over the template files available in the templates
/// directory in the configuration.
Iterable<File> getAvailableTemplates() sync* {
final Directory templatesDir = configuration.templatesDirectory;
for (final File file in templatesDir.listSync().whereType<File>()) {
if (file.basename.endsWith('.tmpl')) {
yield file;
}
}
}
/// Interpolates the [injections] into an HTML skeleton file.
///
/// Similar to interpolateTemplate, but we are only looking for `code-`
/// components, and we care about the order of the injections.
/// The order of the injections is important.
///
/// Takes into account the [type] and doesn't substitute in the id and the app
/// if not a [SnippetType.sample] snippet.
@ -77,7 +56,7 @@ class SnippetGenerator {
final List<String> codeParts = <String>[];
const HtmlEscape htmlEscape = HtmlEscape();
String? language;
for (final TemplateInjection injection in sample.parts) {
for (final SkeletonInjection injection in sample.parts) {
if (!injection.name.startsWith('code')) {
continue;
}
@ -165,7 +144,7 @@ class SnippetGenerator {
/// into valid Dart code.
List<SourceLine> _processBlocks(CodeSample sample) {
final List<SourceLine> block = sample.parts
.expand<SourceLine>((TemplateInjection injection) => injection.contents)
.expand<SourceLine>((SkeletonInjection injection) => injection.contents)
.toList();
if (block.isEmpty) {
return <SourceLine>[];
@ -233,11 +212,11 @@ class SnippetGenerator {
}
/// Parses the input for the various code and description segments, and
/// returns a set of template injections in the order found.
List<TemplateInjection> parseInput(CodeSample sample) {
/// returns a set of skeleton injections in the order found.
List<SkeletonInjection> parseInput(CodeSample sample) {
bool inCodeBlock = false;
final List<SourceLine> description = <SourceLine>[];
final List<TemplateInjection> components = <TemplateInjection>[];
final List<SkeletonInjection> components = <SkeletonInjection>[];
String? language;
final RegExp codeStartEnd =
RegExp(r'^\s*```(?<language>[-\w]+|[-\w]+ (?<section>[-\w]+))?\s*$');
@ -249,12 +228,12 @@ class SnippetGenerator {
if (match.namedGroup('language') != null) {
language = match[1];
if (match.namedGroup('section') != null) {
components.add(TemplateInjection(
components.add(SkeletonInjection(
'code-${match.namedGroup('section')}', <SourceLine>[],
language: language!));
} else {
components.add(
TemplateInjection('code', <SourceLine>[], language: language!));
SkeletonInjection('code', <SourceLine>[], language: language!));
}
} else {
language = null;
@ -286,9 +265,9 @@ class SnippetGenerator {
lastWasWhitespace = onlyWhitespace;
}
sample.description = descriptionLines.join('\n').trimRight();
sample.parts = <TemplateInjection>[
sample.parts = <SkeletonInjection>[
if (sample is SnippetSample)
TemplateInjection('#assumptions', sample.assumptions),
SkeletonInjection('#assumptions', sample.assumptions),
...components,
];
return sample.parts;
@ -327,10 +306,6 @@ class SnippetGenerator {
///
/// The optional `output` is the file to write the generated sample code to.
///
/// If `addSectionMarkers` is true, then markers will be added before and
/// after each template section in the output. This is intended to facilitate
/// editing of the sample during the authoring process.
///
/// If `includeAssumptions` is true, then the block in the "Examples can
/// assume:" block will also be included in the output.
///
@ -341,58 +316,16 @@ class SnippetGenerator {
String? copyright,
String? description,
bool formatOutput = true,
bool addSectionMarkers = false,
bool includeAssumptions = false,
}) {
sample.metadata['copyright'] ??= copyright;
final List<TemplateInjection> snippetData = parseInput(sample);
final List<SkeletonInjection> snippetData = parseInput(sample);
sample.description = description ?? sample.description;
sample.metadata['description'] = _getDescription(sample);
switch (sample.runtimeType) {
switch (sample) {
case DartpadSample _:
case ApplicationSample _:
String app;
if (sample.sourceFile == null) {
final String templateName = sample.template;
if (templateName.isEmpty) {
io.stderr
.writeln('Non-linked samples must have a --template argument.');
io.exit(1);
}
final Directory templatesDir = configuration.templatesDirectory;
File? templateFile;
templateFile =
getTemplatePath(templateName, templatesDir: templatesDir);
if (templateFile == null) {
io.stderr.writeln(
'The template $templateName was not found in the templates '
'directory ${templatesDir.path}');
io.exit(1);
}
final String templateContents = _loadFileAsUtf8(templateFile);
final String templateRelativePath =
templateFile.absolute.path.contains(flutterRoot.absolute.path)
? path.relative(templateFile.absolute.path,
from: flutterRoot.absolute.path)
: templateFile.absolute.path;
final String templateHeader = '''
// Template: $templateRelativePath
//
// Comment lines marked with "▼▼▼" and "▲▲▲" are used for authoring
// of samples, and may be ignored if you are just exploring the sample.
''';
app = interpolateTemplate(
snippetData,
addSectionMarkers
? '$templateHeader\n$templateContents'
: templateContents,
sample.metadata,
addSectionMarkers: addSectionMarkers,
addCopyright: copyright != null,
);
} else {
app = sample.sourceFileContents;
}
final String app = sample.sourceFileContents;
sample.output = app;
if (formatOutput) {
final DartFormatter formatter =
@ -421,13 +354,14 @@ class SnippetGenerator {
metadataFile.writeAsStringSync(jsonEncoder.convert(metadata));
}
case SnippetSample _:
if (sample is SnippetSample) {
String app;
if (sample.sourceFile == null) {
String templateContents;
if (includeAssumptions) {
templateContents =
'${headers.map<String>((SourceLine line) => line.text).join('\n')}\n{{#assumptions}}\n{{description}}\n{{code}}';
'${headers.map<String>((SourceLine line) {
return line.text;
}).join('\n')}\n{{#assumptions}}\n{{description}}\n{{code}}';
} else {
templateContents = '{{description}}\n{{code}}';
}
@ -435,14 +369,12 @@ class SnippetGenerator {
snippetData,
templateContents,
sample.metadata,
addSectionMarkers: addSectionMarkers,
addCopyright: copyright != null,
);
} else {
app = sample.inputAsString;
}
sample.output = app;
}
}
return sample.output;
}

View File

@ -176,7 +176,7 @@ class SnippetDartdocParser {
}
parseComment(element);
for (final CodeSample sample in element.samples) {
switch (sample.runtimeType) {
switch (sample) {
case DartpadSample _:
dartpadCount++;
case ApplicationSample _:

View File

@ -140,28 +140,12 @@ class FlutterInformation {
}
}
/// Returns a marker with section arrows surrounding the given string.
///
/// Specifying `start` as false returns an ending marker instead of a starting
/// marker.
String sectionArrows(String name, {bool start = true}) {
const int markerArrows = 8;
final String arrows =
(start ? '\u25bc' /* ▼ */ : '\u25b2' /* ▲ */) * markerArrows;
final String marker =
'//* $arrows $name $arrows (do not modify or remove section marker)';
return '${start ? '\n//*${'*' * marker.length}\n' : '\n'}'
'$marker'
'${!start ? '\n//*${'*' * marker.length}\n' : '\n'}';
}
/// Injects the [injections] into the [template], while turning the
/// "description" injection into a comment.
String interpolateTemplate(
List<TemplateInjection> injections,
List<SkeletonInjection> injections,
String template,
Map<String, Object?> metadata, {
bool addSectionMarkers = false,
bool addCopyright = false,
}) {
String wrapSectionMarker(Iterable<String> contents, {required String name}) {
@ -170,13 +154,8 @@ String interpolateTemplate(
return '';
}
// We don't wrap some sections, because otherwise they generate invalid files.
const Set<String> skippedSections = <String>{'element', 'copyright'};
final bool addMarkers =
addSectionMarkers && !skippedSections.contains(name);
final String result = <String>[
if (addMarkers) sectionArrows(name),
...contents,
if (addMarkers) sectionArrows(name, start: false),
].join('\n');
final RegExp wrappingNewlines = RegExp(r'^\n*(.*)\n*$', dotAll: true);
return result.replaceAllMapped(
@ -187,7 +166,7 @@ String interpolateTemplate(
.replaceAllMapped(RegExp(r'{{([^}]+)}}'), (Match match) {
final String name = match[1]!;
final int componentIndex = injections
.indexWhere((TemplateInjection injection) => injection.name == name);
.indexWhere((SkeletonInjection injection) => injection.name == name);
if (metadata[name] != null && componentIndex == -1) {
// If the match isn't found in the injections, then just return the
// metadata entry.

View File

@ -27,12 +27,6 @@ void main() {
matches(RegExp(
r'[/\\]flutter sdk[/\\]dev[/\\]snippets[/\\]config[/\\]skeletons')));
});
test('templates directory is correct', () async {
expect(
config.templatesDirectory.path,
matches(RegExp(
r'[/\\]flutter sdk[/\\]dev[/\\]snippets[/\\]config[/\\]templates')));
});
test('html skeleton file for sample is correct', () async {
expect(
config.getHtmlSkeletonFile('snippet').path,

View File

@ -37,7 +37,6 @@ void main() {
late FlutterRepoSnippetConfiguration configuration;
late SnippetGenerator generator;
late Directory tmpDir;
late File template;
late Directory flutterRoot;
void writeSkeleton(String type) {
@ -69,25 +68,13 @@ void main() {
.directory(path.join(tmpDir.absolute.path, 'flutter'));
configuration = FlutterRepoSnippetConfiguration(
flutterRoot: flutterRoot, filesystem: memoryFileSystem);
configuration.templatesDirectory.createSync(recursive: true);
configuration.skeletonsDirectory.createSync(recursive: true);
template = memoryFileSystem.file(
path.join(configuration.templatesDirectory.path, 'template.tmpl'));
template.writeAsStringSync('''
// Flutter code sample for {{element}}
{{description}}
{{code-my-preamble}}
{{code}}
''');
<String>['dartpad', 'sample', 'snippet'].forEach(writeSkeleton);
FlutterInformation.instance = FakeFlutterInformation(flutterRoot);
generator = SnippetGenerator(
configuration: configuration,
filesystem: memoryFileSystem,
flutterRoot: configuration.templatesDirectory.parent);
flutterRoot: configuration.skeletonsDirectory.parent);
});
test('parses from comments', () async {
@ -124,35 +111,6 @@ void main() {
}
expect(sampleCount, equals(8));
});
test('parses dartpad samples from comments', () async {
final File inputFile =
_createDartpadSourceFile(tmpDir, memoryFileSystem, flutterRoot);
final Iterable<SourceElement> elements = getFileElements(inputFile,
resourceProvider: FileSystemResourceProvider(memoryFileSystem));
expect(elements, isNotEmpty);
final SnippetDartdocParser sampleParser =
SnippetDartdocParser(memoryFileSystem);
sampleParser.parseFromComments(elements);
expect(elements.length, equals(1));
int sampleCount = 0;
for (final SourceElement element in elements) {
expect(element.samples.length, greaterThanOrEqualTo(1));
sampleCount += element.samples.length;
final String code = generator.generateCode(element.samples.first);
expect(code, contains('// Description'));
expect(
code,
contains(RegExp('^void ${element.name}Sample\\(\\) \\{.*\$',
multiLine: true)));
final String html = generator.generateHtml(element.samples.first);
expect(
html,
contains(RegExp(
'''^<iframe class="snippet-dartpad" src="https://dartpad.dev/.*sample_id=${element.name}.0.*></iframe>.*\$''',
multiLine: true)));
}
expect(sampleCount, equals(1));
});
test('parses dartpad samples from linked file', () async {
final File inputFile = _createDartpadSourceFile(
tmpDir, memoryFileSystem, flutterRoot,

View File

@ -41,7 +41,6 @@ void main() {
late FlutterRepoSnippetConfiguration configuration;
late SnippetGenerator generator;
late Directory tmpDir;
late File template;
void writeSkeleton(String type) {
switch (type) {
@ -72,26 +71,14 @@ void main() {
flutterRoot: memoryFileSystem
.directory(path.join(tmpDir.absolute.path, 'flutter')),
filesystem: memoryFileSystem);
configuration.templatesDirectory.createSync(recursive: true);
configuration.skeletonsDirectory.createSync(recursive: true);
template = memoryFileSystem.file(
path.join(configuration.templatesDirectory.path, 'template.tmpl'));
template.writeAsStringSync('''
// Flutter code sample for {{element}}
{{description}}
{{code-my-preamble}}
{{code}}
''');
<String>['dartpad', 'sample', 'snippet'].forEach(writeSkeleton);
FlutterInformation.instance =
FakeFlutterInformation(configuration.flutterRoot);
generator = SnippetGenerator(
configuration: configuration,
filesystem: memoryFileSystem,
flutterRoot: configuration.templatesDirectory.parent);
flutterRoot: configuration.skeletonsDirectory.parent);
});
test('generates samples', () async {
@ -99,20 +86,24 @@ void main() {
.file(path.join(tmpDir.absolute.path, 'snippet_in.txt'))
..createSync(recursive: true)
..writeAsStringSync(r'''
A description of the snippet.
A description of the sample.
On several lines.
```my-dart_language my-preamble
const String name = 'snippet';
```
```dart
void main() {
print('The actual $name.');
}
```
** See code in examples/api/widgets/foo/foo_example.0.dart **
''');
final String examplePath = path.join(configuration.flutterRoot.path, 'examples/api/widgets/foo/foo_example.0.dart');
memoryFileSystem.file(examplePath)
..create(recursive: true)
..writeAsStringSync('''
// Copyright
// Flutter code sample for [MyElement].
void main() {
runApp(MaterialApp(title: 'foo'));
}\n'''
);
final File outputFile = memoryFileSystem
.file(path.join(tmpDir.absolute.path, 'snippet_out.txt'));
final SnippetDartdocParser sampleParser =
@ -135,27 +126,23 @@ void main() {
element.samples.first,
output: outputFile,
);
expect(code, contains('// Flutter code sample for MyElement'));
expect(code, contains("runApp(MaterialApp(title: 'foo'));"));
final String html = generator.generateHtml(
element.samples.first,
);
expect(html, contains('<div>HTML Bits</div>'));
expect(html, contains('<div>More HTML Bits</div>'));
expect(html, contains(r'print(&#39;The actual $name.&#39;);'));
expect(html, contains('A description of the snippet.\n'));
expect(html, contains(r'''runApp(MaterialApp(title: &#39;foo&#39;));'''));
expect(html, isNot(contains('sample_channel=stable')));
expect(
html,
contains('A description of the snippet.\n'
contains('A description of the sample.\n'
'\n'
'On several lines.{@inject-html}</div>'));
expect(html, contains('void main() {'));
final String outputContents = outputFile.readAsStringSync();
expect(outputContents, contains('// Flutter code sample for MyElement'));
expect(outputContents, contains('A description of the snippet.'));
expect(outputContents, contains('void main() {'));
expect(outputContents, contains("const String name = 'snippet';"));
});
test('generates snippets', () async {
@ -212,12 +199,20 @@ A description of the snippet.
On several lines.
```code
void main() {
print('The actual $name.');
}
```
** See code in examples/api/widgets/foo/foo_example.0.dart **
''');
final String examplePath = path.join(configuration.flutterRoot.path, 'examples/api/widgets/foo/foo_example.0.dart');
memoryFileSystem.file(examplePath)
..create(recursive: true)
..writeAsStringSync('''
// Copyright
// Flutter code sample for [MyElement].
void main() {
runApp(MaterialApp(title: 'foo'));
}\n'''
);
final SnippetDartdocParser sampleParser =
SnippetDartdocParser(memoryFileSystem);
@ -235,7 +230,7 @@ void main() {
'channel': 'stable',
});
final String code = generator.generateCode(element.samples.first);
expect(code, contains('// Flutter code sample for MyElement'));
expect(code, contains("runApp(MaterialApp(title: 'foo'));"));
final String html = generator.generateHtml(element.samples.first);
expect(html, contains('<div>HTML Bits (DartPad-style)</div>'));
expect(html, contains('<div>More HTML Bits</div>'));
@ -350,7 +345,7 @@ void main() {
.file(tmpDir.childFile('input.snippet'))
..writeAsString('/// Test file');
snippets_main.main(
<String>['--input=${input.absolute.path}', '--template=template']);
<String>['--input=${input.absolute.path}']);
final Map<String, dynamic> metadata =
mockSnippetGenerator.sample.metadata;
@ -393,7 +388,7 @@ void main() {
input.deleteSync();
snippets_main.main(
<String>['--input=${input.absolute.path}', '--template=template']);
<String>['--input=${input.absolute.path}']);
expect(errorMessage,
equals('The input file ${input.absolute.path} does not exist.'));
errorMessage = '';