Allow code samples to *continue* a prior sample.

We have added more examples, and some of them uses the structure of:
````dart
/// ```dart
/// var x = something;
/// ```
/// and then you can also floo the thing
/// ```
/// x.floo(...);
/// ```
````

The following chunks of the same example can now be written as:

````dart
/// ```dart continued
/// x.floo(...);
/// ```
````

Change handling of imports,
and introduce a `top` template different from `none`.

The `none` template gets nothing for free.
The sample must be completely self-contained.
Is triggered by the sample containing a `library` declaration,
because we can't add anything before that.

The `top` template allows top-level declarations,
but does introduce automatic imports and "samples can expect"
code if the sample doesn't contain `import`s.
Is triggered by top-level declarations other than `library`.

Also some restructuring of the code to make this feature easier
to implement.

Change-Id: If2288147face01efad2ad656aa52183cb4c8b3bf
Reviewed-on: https://dart-review.googlesource.com/c/sdk/+/221343
Commit-Queue: Lasse Nielsen <lrn@google.com>
Reviewed-by: Devon Carew <devoncarew@google.com>
This commit is contained in:
Lasse R.H. Nielsen 2021-12-02 16:37:57 +00:00 committed by Commit Bot
parent 65a3e2eb4c
commit 44baaf13b6
11 changed files with 311 additions and 150 deletions

View file

@ -43,7 +43,7 @@ typedef _Hasher<K> = int Function(K object);
/// final Map<int, String> planets = HashMap(); // Is a HashMap
/// ```
/// To add data to a map, use [operator[]=], [addAll] or [addEntries].
/// ```
/// ```dart continued
/// planets[3] = 'Earth';
/// planets.addAll({4: 'Mars'});
/// final gasGiants = {6: 'Jupiter', 5: 'Saturn'};
@ -52,12 +52,12 @@ typedef _Hasher<K> = int Function(K object);
/// ```
/// To check if the map is empty, use [isEmpty] or [isNotEmpty].
/// To find the number of map entries, use [length].
/// ```
/// ```dart continued
/// final isEmpty = planets.isEmpty; // false
/// final length = planets.length; // 4
/// ```
/// The [forEach] iterates through all entries of a map.
/// ```
/// ```dart continued
/// planets.forEach((key, value) {
/// print('$key \t $value');
/// // 5 Saturn
@ -67,32 +67,32 @@ typedef _Hasher<K> = int Function(K object);
/// });
/// ```
/// To check whether the map has an entry with a specific key, use [containsKey].
/// ```
/// ```dart continued
/// final keyOneExists = planets.containsKey(4); // true
/// final keyFiveExists = planets.containsKey(1); // false
/// ```
/// ```dart continued
/// To check whether the map has an entry with a specific value,
/// use [containsValue].
/// ```
/// ```dart continued
/// final marsExists = planets.containsValue('Mars'); // true
/// final venusExists = planets.containsValue('Venus'); // false
/// ```
/// To remove an entry with a specific key, use [remove].
/// ```
/// ```dart continued
/// final removeValue = planets.remove(5);
/// print(removeValue); // Jupiter
/// print(planets); // fx {4: Mars, 3: Earth, 5: Saturn}
/// ```
/// To remove multiple entries at the same time, based on their keys and values,
/// use [removeWhere].
/// ```
/// ```dart continued
/// planets.removeWhere((key, value) => key == 5);
/// print(planets); // fx {3: Earth, 4: Mars}
/// ```
/// To conditionally add or modify a value for a specific key, depending on
/// whether there already is an entry with that key,
/// use [putIfAbsent] or [update].
/// ```
/// ```dart continued
/// planets.update(4, (v) => 'Saturn');
/// planets.update(8, (v) => '', ifAbsent: () => 'Neptune');
/// planets.putIfAbsent(4, () => 'Another Saturn');
@ -100,12 +100,12 @@ typedef _Hasher<K> = int Function(K object);
/// ```
/// To update the values of all keys, based on the existing key and value,
/// use [updateAll].
/// ```
/// ```dart continued
/// planets.updateAll((key, value) => 'X');
/// print(planets); // fx {8: X, 3: X, 4: X}
/// ```
/// To remove all entries and empty the map, use [clear].
/// ```
/// ```dart continued
/// planets.clear();
/// print(planets); // {}
/// print(planets.isEmpty); // true
@ -152,7 +152,7 @@ abstract class HashMap<K, V> implements Map<K, V> {
/// Example:
/// ```dart template:expression
/// HashMap<int,int>(equals: (int a, int b) => (b - a) % 5 == 0,
/// hashCode: (int e) => e % 5);
/// hashCode: (int e) => e % 5)
/// ```
/// This example map does not need an `isValidKey` function to be passed.
/// The default function accepts precisely `int` values, which can safely be

View file

@ -38,24 +38,24 @@ part of dart.collection;
/// final letters = HashSet<String>();
/// ```
/// To add data to a set, use [add] or [addAll].
/// ```
/// ```dart continued
/// letters.add('A');
/// letters.addAll({'B', 'C', 'D'});
/// ```
/// To check if the set is empty, use [isEmpty] or [isNotEmpty].
/// To find the number of elements in the set, use [length].
/// ```
/// ```dart continued
/// print(letters.isEmpty); // false
/// print(letters.length); // 4
/// print(letters); // fx {A, D, C, B}
/// ```
/// To check whether the set has an element with a specific value,
/// use [contains].
/// ```
/// ```dart continued
/// final bExists = letters.contains('B'); // true
/// ```
/// The [forEach] method calls a function with each element of the set.
/// ```
/// ```dart continued
/// letters.forEach(print);
/// // A
/// // D
@ -63,29 +63,29 @@ part of dart.collection;
/// // B
/// ```
/// To make a copy of the set, use [toSet].
/// ```
/// ```dart continued
/// final anotherSet = letters.toSet();
/// print(anotherSet); // fx {A, C, D, B}
/// ```
/// To remove an element, use [remove].
/// ```
/// ```dart continued
/// final removedValue = letters.remove('A'); // true
/// print(letters); // fx {B, C, D}
/// ```
/// To remove multiple elements at the same time, use [removeWhere] or
/// [removeAll].
/// ```
/// ```dart continued
/// letters.removeWhere((element) => element.startsWith('B'));
/// print(letters); // fx {D, C}
/// ```
/// To removes all elements in this set that do not meet a condition,
/// use [retainWhere].
/// ```
/// ```dart continued
/// letters.retainWhere((element) => element.contains('C'));
/// print(letters); // {C}
/// ```
/// To remove all elements and empty the set, use [clear].
/// ```
/// ```dart continued
/// letters.clear();
/// print(letters.isEmpty); // true
/// print(letters); // {}
@ -118,7 +118,7 @@ abstract class HashSet<E> implements Set<E> {
/// instance of [E], which means that:
/// ```dart template:expression
/// HashSet<int>(equals: (int e1, int e2) => (e1 - e2) % 5 == 0,
/// hashCode: (int e) => e % 5);
/// hashCode: (int e) => e % 5)
/// ```
/// does not need an `isValidKey` argument because it defaults to only
/// accepting `int` values which are accepted by both `equals` and `hashCode`.

View file

@ -41,20 +41,20 @@ part of dart.collection;
/// final planetsByDiameter = {0.949: 'Venus'}; // A new LinkedHashMap
/// ```
/// To add data to a map, use [operator[]=], [addAll] or [addEntries].
/// ```
/// ```dart continued
/// planetsByDiameter[1] = 'Earth';
/// planetsByDiameter.addAll({0.532: 'Mars', 11.209: 'Jupiter'});
/// ```
/// To check if the map is empty, use [isEmpty] or [isNotEmpty].
/// To find the number of map entries, use [length].
/// ```
/// ```dart continued
/// print(planetsByDiameter.isEmpty); // false
/// print(planetsByDiameter.length); // 4
/// print(planetsByDiameter);
/// // {0.949: Venus, 1.0: Earth, 0.532: Mars, 11.209: Jupiter}
/// ```
/// The [forEach] method calls a function for each key/value entry of the map.
/// ```
/// ```dart continued
/// planetsByDiameter.forEach((key, value) {
/// print('$key \t $value');
/// // 0.949 Venus
@ -64,44 +64,44 @@ part of dart.collection;
/// });
/// ```
/// To check whether the map has an entry with a specific key, use [containsKey].
/// ```
/// ```dart continued
/// final keyOneExists = planetsByDiameter.containsKey(1); // true
/// final keyFiveExists = planetsByDiameter.containsKey(5); // false
/// ```
/// To check whether the map has an entry with a specific value,
/// use [containsValue].
/// ```
/// ```dart continued
/// final earthExists = planetsByDiameter.containsValue('Earth'); // true
/// final saturnExists = planetsByDiameter.containsValue('Saturn'); // false
/// ```
/// To remove an entry with a specific key, use [remove].
/// ```
/// ```dart continued
/// final removedValue = planetsByDiameter.remove(1);
/// print(removedValue); // Earth
/// print(planetsByDiameter); // {0.949: Venus, 0.532: Mars, 11.209: Jupiter}
/// ```
/// To remove multiple entries at the same time, based on their keys and values,
/// use [removeWhere].
/// ```
/// ```dart continued
/// planetsByDiameter.removeWhere((key, value) => key == 0.949);
/// print(planetsByDiameter); // {0.532: Mars, 11.209: Jupiter}
/// ```
/// To conditionally add or modify a value for a specific key, depending on
/// whether there already is an entry with that key,
/// use [putIfAbsent] or [update].
/// ```
/// ```dart continued
/// planetsByDiameter.update(0.949, (v) => 'Venus', ifAbsent: () => 'Venus');
/// planetsByDiameter.putIfAbsent(0.532, () => "Another Mars if needed");
/// print(planetsByDiameter); // {0.532: Mars, 11.209: Jupiter, 0.949: Venus}
/// ```
/// To update the values of all keys, based on the existing key and value,
/// use [updateAll].
/// ```
/// ```dart continued
/// planetsByDiameter.updateAll((key, value) => 'X');
/// print(planetsByDiameter); // {0.532: X, 11.209: X, 0.949: X}
/// ```
/// To remove all entries and empty the map, use [clear].
/// ```
/// ```dart continued
/// planetsByDiameter.clear();
/// print(planetsByDiameter); // {}
/// print(planetsByDiameter.isEmpty); // true

View file

@ -44,24 +44,24 @@ part of dart.collection;
/// final planets = <String>{}; // LinkedHashSet
/// ```
/// To add data to a set, use [add] or [addAll].
/// ```
/// ```dart continued
/// final uranusAdded = planets.add('Uranus'); // true
/// planets.addAll({'Venus', 'Mars', 'Earth', 'Jupiter'});
/// print(planets); // {Uranus, Venus, Mars, Earth, Jupiter}
/// ```
/// To check if the set is empty, use [isEmpty] or [isNotEmpty].
/// To find the number of elements in the set, use [length].
/// ```
/// ```dart continued
/// print(planets.isEmpty); // false
/// print(planets.length); // 5
/// ```
/// To check whether the set has an element with a specific value,
/// use [contains].
/// ```
/// ```dart continued
/// final marsExists = planets.contains('Mars'); // true
/// ```
/// The [forEach] method calls a function with each element of the set.
/// ```
/// ```dart continued
/// planets.forEach(print);
/// // Uranus
/// // Venus
@ -71,29 +71,29 @@ part of dart.collection;
/// ```
///
/// To make a copy of the set, use [toSet].
/// ```
/// ```dart continued
/// final copySet = planets.toSet();
/// print(copySet); // {Uranus, Venus, Mars, Earth, Jupiter}
/// ```
/// To remove an element, use [remove].
/// ```
/// ```dart continued
/// final removedValue = planets.remove('Mars'); // Mars
/// print(planets); // {Uranus, Venus, Earth, Jupiter}
/// ```
/// To remove multiple elements at the same time, use [removeWhere] or
/// [removeAll].
/// ```
/// ```dart continued
/// planets.removeWhere((element) => element.startsWith('E'));
/// print(planets); // {Uranus, Venus, Jupiter}
/// ```
/// To removes all elements in this set that do not meet a condition,
/// use [retainWhere].
/// ```
/// ```dart continued
/// planets.retainWhere((element) => element.contains('Jupiter'));
/// print(planets); // {Jupiter}
/// ```
/// ```dart continued
/// To remove all elements and empty the set, use [clear].
/// ```
/// ```dart continued
/// planets.clear();
/// print(planets.isEmpty); // true
/// print(planets); // {}

View file

@ -532,7 +532,7 @@ class _DoubleLinkedQueueIterator<E> implements Iterator<E> {
/// final queue = ListQueue<int>();
/// ```
/// To add objects to a queue, use [add], [addAll], [addFirst] or[addLast].
/// ```
/// ```dart continued
/// queue.add(5);
/// queue.addFirst(0);
/// queue.addLast(10);
@ -541,44 +541,44 @@ class _DoubleLinkedQueueIterator<E> implements Iterator<E> {
/// ```
/// To check if the queue is empty, use [isEmpty] or [isNotEmpty].
/// To find the number of queue entries, use [length].
/// ```
/// ```dart continued
/// final isEmpty = queue.isEmpty; // false
/// final queueSize = queue.length; // 6
/// ```
/// To get first or last item from queue, use [first] or [last].
/// ```
/// ```dart continued
/// final first = queue.first; // 0
/// final last = queue.last; // 3
/// ```
/// To get item value using index, use [elementAt].
/// ```
/// ```dart continued
/// final itemAt = queue.elementAt(2); // 10
/// ```
/// To convert queue to list, call [toList].
/// ```
/// ```dart continued
/// final numbers = queue.toList();
/// print(numbers); // [0, 5, 10, 1, 2, 3]
/// ```
/// To remove item from queue, call [remove], [removeFirst] or [removeLast].
/// ```
/// ```dart continued
/// queue.remove(10);
/// queue.removeFirst();
/// queue.removeLast();
/// print(queue); // {5, 1, 2}
/// ```
/// To remove multiple elements at the same time, use [removeWhere].
/// ```
/// ```dart continued
/// queue.removeWhere((element) => element == 1);
/// print(queue); // {5, 2}
/// ```
/// To remove all elements in this queue that do not meet a condition,
/// use [retainWhere].
/// ```
/// ```dart continued
/// queue.retainWhere((element) => element < 4);
/// print(queue); // {2}
/// ```
/// To remove all items and empty the set, use [clear].
/// ```
/// ```dart continued
/// queue.clear();
/// print(queue.isEmpty); // true
/// print(queue); // {}

View file

@ -159,7 +159,7 @@ const Null proxy = null;
///
/// For example:
///
/// ```dart template:none
/// ```dart template:top
/// @pragma('Tool:pragma-name', [param1, param2, ...])
/// class Foo { }
///

View file

@ -827,7 +827,7 @@ abstract class NativeApi {
/// Annotation to be used for marking an external function as FFI native.
///
/// Example:
///```dart template:none
///```dart template:top
/// @FfiNative<Int64 Function(Int64, Int64)>('FfiNative_Sum', isLeaf:true)
/// external int sum(int a, int b);
///```

View file

@ -644,7 +644,7 @@ const Object sentinelValue = const SentinelValue(0);
///
/// Example:
///
/// ```dart template:none
/// ```dart template:top
/// class Two<A, B> {}
///
/// print(extractTypeArguments<List>(<int>[], <T>() => new Set<T>()));
@ -658,7 +658,7 @@ const Object sentinelValue = const SentinelValue(0);
/// The type argument T is important to choose which specific type parameter
/// list in [instance]'s type hierarchy is being extracted. Consider:
///
/// ```dart template:none
/// ```dart template:top
/// class A<T> {}
/// class B<T> {}
///

View file

@ -245,7 +245,7 @@ class NullRejectionException implements Exception {
/// Converts a JavaScript Promise to a Dart [Future].
///
/// ```dart template:none
/// ```dart template:top
/// @JS()
/// external Promise<num> get threePromise; // Resolves to 3
///

View file

@ -1,19 +1,20 @@
## Whats' this?
## Whats this?
A tool to validate the documentation comments for the `dart:` libraries.
## Running the tool
To validate all the dart: libraries, run:
To validate all the `dart:` libraries, run:
```
dart tools/verify_docs/bin/verify_docs.dart
```
Or to validate an individual library (async, collection, js_util, ...), run:
Or to validate an individual library (async, collection, js_util, ...), run either of:
```
dart tools/verify_docs/bin/verify_docs.dart sdk/lib/<lib-name>
dart tools/verify_docs/bin/verify_docs.dart dart:<lib-name>
```
The tool should be run from the root of the sdk repository.
@ -22,68 +23,104 @@ The tool should be run from the root of the sdk repository.
### What gets analyzed
This tool will walk all dartdoc api docs looking for code samples in doc comments.
This tool will walk all DartDoc API docs looking for code samples in doc comments.
It will analyze any code sample in a `dart` code fence. For example:
> ```dart
> print('hello world!');
> ```
> ````dart
> /// ```dart
> /// print('hello world!');
> /// ```
> ````
By default, an import for that library is added to the sample being analyzed (i.e.,
`import 'dart:async";`).
By default, an import for that library is added to the sample being analyzed, e.g., `import 'dart:async";`.
### Excluding code samples from analysis
In order to exclude a code sample from analysis, change it to a plain code fence style:
> ```
> print("I'm not analyzed :(");
> ```
### Specifying additional imports
In order to reference code from other Dart core libraries, you can either explicitly add
the import to the code sample - in-line in the sample - or use a directive on the same
line as the code fence. The directive style looks like:
> ```dart import:async
> print('hello world ${Timer()}');
> ```
Multiple imports can be specified like this if desired (i.e., "```dart import:async import:convert").
> ````dart
> /// ```
> /// print("I'm not analyzed :(");
> /// ```
> ````
### Specifying templates
The analysis tool can inject the code sample into a template before analyzing the
sample. This allows the author to focus on the import parts of the API being
sample. This allows the author to focus on the important parts of the API being
documented with less boilerplate in the generated docs.
The template includes an automatic import of the library containing the example, so an example in, say, the documentation of `StreamController.add` would have `dart:async` imported automatically.
The tool will try and automatically detect the right template to use based on
code patterns within the sample itself. In order to explicitly indicate which template
to use, you can specify it as part of the code fence line. For example:
> ```dart template:main
> print('hello world ${Timer()}');
> ```dart
> /// ```dart template:main
> /// print('hello world ${Timer()}');
> /// ```
> ```
The three current templates are:
- `none`: do not wrap the code sample in any template
- `main`: wrap the code sample in a simple main() method
- `expression`: wrap the code sample in a statement within a main() method
The current templates are:
For most code sample, the auto-detection code will select `template:main` or
- `none`: Do not wrap the code sample in any template, including no imports.
- `top`: The code sample is top level code, preceded only by imports.
- `main`: The code sample is one or more statements in a simple asynchronous `main()` function.
- `expression`: The code sample is an expression within a simple asynchronous `main()` method.
For most code samples, the auto-detection code will select `template:main` or
`template:expression`.
If the example contains any `library` declarations, the template becomes `none`.
### Specifying additional imports
If your example contains any `library`, the default import of the current library is omitted. To avoid that, you can declare extra automatic imports in the code fence like:
> ````dart
> /// ```dart import:async
> /// print('hello world ${Timer()}');
> /// ```
> ````
Multiple imports can be specified like this if desired, e.g., "```` ```dart import:async import:convert````".
Does not work if combined with `template:none`, whether the `none` template is specified explicitly or auto-detected.
### Splitting examples
Some examples may be split into separate code blocks, but should be seen
as continuing the same running example.
If the following code blocks are marked as `continued` as shown below, they
are included into the previous code block instead of being treated as a new
example.
> ````dart
> /// ```dart
> /// var list = [1, 2, 3];
> /// ```
> /// And then you can also do the following:
> /// ```dart continued
> /// list.forEach(print);
> /// ```
> ````
A `continued` code block cannot have any other flags in the fence.
### Including additional code for analysis
You can declare code that should be included in the analysis but not shown in
the API docs by adding a comment "// Examples can assume:" to the file (usually
at the top of the file, after the imports), following by one or more
commented-out lines of code. That code is included verbatim in the analysis. For
example:
commented-out lines of code. That code is included verbatim in the analysis, at top-level after the automatic imports. Does not work with `template:none`.
For example:
```dart
// Examples can assume:
// final BuildContext context;
// final String userAvatarUrl;
```

244
tools/verify_docs/bin/verify_docs.dart Normal file → Executable file
View file

@ -2,6 +2,8 @@
// for details. All rights reserved. Use of this source code is governed by a
// BSD-style license that can be found in the LICENSE file.
// Read the ../README.md file for the recognized syntax.
import 'dart:collection';
import 'dart:io';
@ -20,10 +22,10 @@ import 'package:analyzer/src/error/codes.dart';
import 'package:analyzer/src/util/comment.dart';
import 'package:path/path.dart' as path;
final libDir = Directory(path.join('sdk', 'lib'));
void main(List<String> args) async {
final libDir = Directory('sdk/lib');
if (!libDir.existsSync()) {
print('Please run this tool from the root of the sdk repo.');
print('Please run this tool from the root of the sdk repository.');
exit(1);
}
@ -37,7 +39,7 @@ void main(List<String> args) async {
final coreLibraries = args.isEmpty
? libDir.listSync().whereType<Directory>().toList()
: args.map((arg) => Directory(arg)).toList();
: args.map(parseArg).toList();
coreLibraries.sort((a, b) => a.path.compareTo(b.path));
// Skip some dart: libraries.
@ -172,9 +174,9 @@ class ValidateCommentCodeSamplesVisitor extends GeneralizingAstVisitor {
var offset = text.indexOf(sampleStart);
while (offset != -1) {
// Collect template directives, like "```dart import:async".
final codeFenceSuffix = text.substring(
offset + sampleStart.length, text.indexOf('\n', offset));
final directives = Set.unmodifiable(codeFenceSuffix.trim().split(' '));
final codeFenceSuffix = text
.substring(offset + sampleStart.length, text.indexOf('\n', offset))
.trim();
offset = text.indexOf('\n', offset) + 1;
final end = text.indexOf(sampleEnd, offset);
@ -183,73 +185,128 @@ class ValidateCommentCodeSamplesVisitor extends GeneralizingAstVisitor {
snippet = snippet.substring(0, snippet.lastIndexOf('\n'));
List<String> lines = snippet.split('\n');
samples.add(
CodeSample(
lines.map((e) => ' ${cleanDocLine(e)}').join('\n'),
coreLibName: coreLibName,
directives: directives,
lineStartOffset: commentLineStart +
text.substring(0, offset - 1).split('\n').length -
1,
),
);
var startLineNumber = commentLineStart +
text.substring(0, offset - 1).split('\n').length -
1;
if (codeFenceSuffix == "continued") {
if (samples.isEmpty) {
throw "Continued code block without previous code";
}
samples.last = samples.last.append(lines, startLineNumber);
} else {
final directives = Set.unmodifiable(codeFenceSuffix.split(' '));
samples.add(
CodeSample(
[for (var e in lines) ' ${cleanDocLine(e)}'],
coreLibName: coreLibName,
directives: directives,
lineStartOffset: startLineNumber,
),
);
}
offset = text.indexOf(sampleStart, offset);
}
}
Future validateCodeSample(CodeSample sample) async {
var text = sample.text;
final lines = sample.text.split('\n').map((l) => l.trim()).toList();
// RegExp detecting various top-level declarations or `main(`.
//
// If the top-level declaration is `library` or `import`,
// then match 1 (`libdecl`) will be non-null.
// This is a sign that no auto-imports should be added.
//
// If an import declaration is included in the sample, no
// assumed-declarations are added.
// Use the `import:foo` template to import other `dart:` libraries
// instead of writing them explicitly to.
//
// Captures:
// 1/libdecl: Non-null if mathcing a `library` declaration.
// 2: Internal use, quote around import URI.
// 3/importuri: Import URI.
final _toplevelDeclarationRE = RegExp(r'^\s*(?:'
r'library\b(?<libdecl>)|'
r'''import (['"])(?<importuri>.*?)\2|'''
r'class\b|mixin\b|enum\b|extension\b|typedef\b|.*\bmain\('
r')');
final hasImports = text.contains("import '") || text.contains('import "');
validateCodeSample(CodeSample sample) async {
final lines = sample.lines;
// One of 'none', 'main', or 'expression'.
String? template;
// The default imports includes the library itself
// and any import directives.
Set<String> autoImports = sample.imports;
if (sample.hasTemplateDirective) {
template = sample.templateDirective;
// One of 'none', 'top, 'main', or 'expression'.
String template;
bool hasImport = false;
final templateDirective = sample.templateDirective;
if (templateDirective != null) {
template = templateDirective;
} else {
// If there's no explicit template, auto-detect one.
if (lines.any((line) =>
line.startsWith('class ') ||
line.startsWith('enum ') ||
line.startsWith('extension '))) {
template = 'none';
} else if (lines
.any((line) => line.startsWith('main(') || line.contains(' main('))) {
// Scan lines for top-level declarations.
bool hasTopDeclaration = false;
bool hasLibraryDeclaration = false;
for (var line in lines) {
var topDeclaration = _toplevelDeclarationRE.firstMatch(line);
if (topDeclaration != null) {
hasTopDeclaration = true;
hasLibraryDeclaration |=
(topDeclaration.namedGroup("libdecl") != null);
var importDecl = topDeclaration.namedGroup("importuri");
if (importDecl != null) {
hasImport = true;
if (importDecl.startsWith('dart:')) {
// Remove explicit imports from automatic imports
// to avoid duplicate import warnings.
autoImports.remove(importDecl.substring('dart:'.length));
}
}
}
}
if (hasLibraryDeclaration) {
template = 'none';
} else if (hasTopDeclaration) {
template = 'top';
} else if (lines.length == 1 && !lines.first.trim().endsWith(';')) {
// If single line with no trailing `;`, assume expression.
template = 'expression';
} else {
// Otherwise default to `main`.
template = 'main';
}
}
final assumptions = sampleAssumptions ?? '';
var buffer = StringBuffer();
if (!hasImports) {
if (template == 'none') {
// just use the sample text as is
} else if (template == 'main') {
text = "${assumptions}main() async {\n${text.trimRight()}\n}\n";
} else if (template == 'expression') {
text = "${assumptions}main() async {\n${text.trimRight()}\n;\n}\n";
} else {
throw 'unexpected template directive: $template';
if (template != 'none') {
for (var library in autoImports) {
buffer.writeln("import 'dart:$library';");
}
for (final directive
in sample.directives.where((str) => str.startsWith('import:'))) {
final libName = directive.substring('import:'.length);
text = "import 'dart:$libName';\n$text";
}
if (sample.coreLibName != 'internal') {
text = "import 'dart:${sample.coreLibName}';\n$text";
if (!hasImport) {
buffer.write(sampleAssumptions ?? '');
}
}
if (template == 'none' || template == 'top') {
buffer.writeAllLines(lines);
} else if (template == 'main') {
buffer
..writeln('void main() async {')
..writeAllLines(lines)
..writeln('}');
} else if (template == 'expression') {
assert(lines.length >= 1);
buffer
..writeln('void main() async =>')
..writeAllLines(lines.take(lines.length - 1))
..writeln("${lines.last.trimRight()};");
} else {
throw 'unexpected template directive: $template';
}
final text = buffer.toString();
final result = await analysisHelper.resolveFile(text);
@ -311,8 +368,7 @@ class ValidateCommentCodeSamplesVisitor extends GeneralizingAstVisitor {
print('');
// Print out the code sample.
print(sample.text
.split('\n')
print(sample.lines
.map((line) =>
' >${line.length >= 5 ? line.substring(5) : line.trimLeft()}')
.join('\n'));
@ -337,27 +393,63 @@ String cleanDocLine(String line) {
}
class CodeSample {
/// Currently valid template names.
static const validTemplates = ['none', 'top', 'main', 'expression'];
final String coreLibName;
final Set<String> directives;
final String text;
final List<String> lines;
final int lineStartOffset;
CodeSample(
this.text, {
this.lines, {
required this.coreLibName,
this.directives = const {},
required this.lineStartOffset,
});
String get text => lines.join('\n');
bool get hasTemplateDirective => templateDirective != null;
/// The specified template, or `null` if no template is specified.
///
/// A specified template must be of [validTemplates].
String? get templateDirective {
const prefix = 'template:';
String? match = directives.cast<String?>().firstWhere(
(directive) => directive!.startsWith(prefix),
orElse: () => null);
return match == null ? match : match.substring(prefix.length);
for (var directive in directives) {
if (directive.startsWith(prefix)) {
var result = directive.substring(prefix.length);
if (!validTemplates.contains(result)) {
throw "Invalid template name: $result";
}
return result;
}
}
return null;
}
/// The implicit or explicitly requested imports.
Set<String> get imports => {
if (coreLibName != 'internal' && coreLibName != 'core') coreLibName,
for (var directive in directives)
if (directive.startsWith('import:'))
directive.substring('import:'.length)
};
/// Creates a new code sample by appending [lines] to this sample.
///
/// The new sample only differs from this sample in that it has
/// more lines appended, first `this.lines`, then a gap of ` //` lines
/// and then [lines].
CodeSample append(List<String> lines, int lineStartOffset) {
var gapSize = lineStartOffset - (this.lineStartOffset + this.lines.length);
return CodeSample(
[...this.lines, for (var i = 0; i < gapSize; i++) " //", ...lines],
coreLibName: coreLibName,
directives: directives,
lineStartOffset: this.lineStartOffset);
}
}
@ -422,3 +514,35 @@ class AnalysisHelper {
return await analysisSession.getResolvedUnit(samplePath);
}
}
// Helper function to make things easier to read.
extension on StringBuffer {
/// Write every line, right-trimmed, of [lines] with a newline after.
void writeAllLines(Iterable<String> lines) {
for (var line in lines) {
this.writeln(line.trimRight());
}
}
}
/// Interprets [arg] as directory containing a platform library.
///
/// If [arg] is `dart:foo`, the directory is the default directory for
/// the `dart:foo` library source.
/// Otherwise, if [arg] is a directory (relative to the current directory)
/// which exists, that is the result.
/// Otherwise, if [arg] is the name of a platform library,
/// like `foo` where `dart:foo` is a platform library,
/// the result is the default directory for that library's source.
/// Otherwise it's treated as a directory relative to the current directory,
/// which doesn't exist (but that's what the error will refer to).
Directory parseArg(String arg) {
if (arg.startsWith('dart:')) {
return Directory(path.join(libDir.path, arg.substring('dart:'.length)));
}
var dir = Directory(arg);
if (dir.existsSync()) return dir;
var relDir = Directory(path.join(libDir.path, arg));
if (relDir.existsSync()) return relDir;
return dir;
}