diff --git a/sdk/lib/collection/hash_map.dart b/sdk/lib/collection/hash_map.dart index 9bc1661ee94..e8a5a9bfefa 100644 --- a/sdk/lib/collection/hash_map.dart +++ b/sdk/lib/collection/hash_map.dart @@ -43,7 +43,7 @@ typedef _Hasher = int Function(K object); /// final Map 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 = 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 = 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 = 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 implements Map { /// Example: /// ```dart template:expression /// HashMap(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 diff --git a/sdk/lib/collection/hash_set.dart b/sdk/lib/collection/hash_set.dart index 29ef55b2a01..bd8860c08bc 100644 --- a/sdk/lib/collection/hash_set.dart +++ b/sdk/lib/collection/hash_set.dart @@ -38,24 +38,24 @@ part of dart.collection; /// final letters = HashSet(); /// ``` /// 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 implements Set { /// instance of [E], which means that: /// ```dart template:expression /// HashSet(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`. diff --git a/sdk/lib/collection/linked_hash_map.dart b/sdk/lib/collection/linked_hash_map.dart index be711181be4..9ee9d3013c7 100644 --- a/sdk/lib/collection/linked_hash_map.dart +++ b/sdk/lib/collection/linked_hash_map.dart @@ -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 diff --git a/sdk/lib/collection/linked_hash_set.dart b/sdk/lib/collection/linked_hash_set.dart index 55349c90ce4..fd217770693 100644 --- a/sdk/lib/collection/linked_hash_set.dart +++ b/sdk/lib/collection/linked_hash_set.dart @@ -44,24 +44,24 @@ part of dart.collection; /// final planets = {}; // 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); // {} diff --git a/sdk/lib/collection/queue.dart b/sdk/lib/collection/queue.dart index 7f1a6cee362..fa8b785c7ed 100644 --- a/sdk/lib/collection/queue.dart +++ b/sdk/lib/collection/queue.dart @@ -532,7 +532,7 @@ class _DoubleLinkedQueueIterator implements Iterator { /// final queue = ListQueue(); /// ``` /// 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 implements Iterator { /// ``` /// 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); // {} diff --git a/sdk/lib/core/annotations.dart b/sdk/lib/core/annotations.dart index 4b7140b8edf..1c2a416183e 100644 --- a/sdk/lib/core/annotations.dart +++ b/sdk/lib/core/annotations.dart @@ -159,7 +159,7 @@ const Null proxy = null; /// /// For example: /// -/// ```dart template:none +/// ```dart template:top /// @pragma('Tool:pragma-name', [param1, param2, ...]) /// class Foo { } /// diff --git a/sdk/lib/ffi/ffi.dart b/sdk/lib/ffi/ffi.dart index d3d19c22945..00eee44966e 100644 --- a/sdk/lib/ffi/ffi.dart +++ b/sdk/lib/ffi/ffi.dart @@ -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('FfiNative_Sum', isLeaf:true) /// external int sum(int a, int b); ///``` diff --git a/sdk/lib/internal/internal.dart b/sdk/lib/internal/internal.dart index fbe663b79ec..5d38bd8917e 100644 --- a/sdk/lib/internal/internal.dart +++ b/sdk/lib/internal/internal.dart @@ -644,7 +644,7 @@ const Object sentinelValue = const SentinelValue(0); /// /// Example: /// -/// ```dart template:none +/// ```dart template:top /// class Two {} /// /// print(extractTypeArguments([], () => new Set())); @@ -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 {} /// class B {} /// diff --git a/sdk/lib/js_util/js_util.dart b/sdk/lib/js_util/js_util.dart index 7842a32284a..fcd668cbe0a 100644 --- a/sdk/lib/js_util/js_util.dart +++ b/sdk/lib/js_util/js_util.dart @@ -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 get threePromise; // Resolves to 3 /// diff --git a/tools/verify_docs/README.md b/tools/verify_docs/README.md index a79aa7c69a1..41be8635eea 100644 --- a/tools/verify_docs/README.md +++ b/tools/verify_docs/README.md @@ -1,19 +1,20 @@ -## Whats' this? +## What’s 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/ +dart tools/verify_docs/bin/verify_docs.dart dart: ``` 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; ``` + diff --git a/tools/verify_docs/bin/verify_docs.dart b/tools/verify_docs/bin/verify_docs.dart old mode 100644 new mode 100755 index 085223fa31a..8cba35fc403 --- a/tools/verify_docs/bin/verify_docs.dart +++ b/tools/verify_docs/bin/verify_docs.dart @@ -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 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 args) async { final coreLibraries = args.isEmpty ? libDir.listSync().whereType().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 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(?)|' + r'''import (['"])(?.*?)\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 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 directives; - final String text; + final List 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().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 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 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 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; +}