mirror of
https://github.com/flutter/flutter.git
synced 2025-06-03 00:51:18 +00:00
[flutter_releases] Flutter stable 3.22.0 Framework Cherrypicks (#147950)
# Flutter stable 3.22.0 Framework ## Scheduled Cherrypicks
This commit is contained in:
parent
87b652410d
commit
5dcb86f68f
@ -1 +1 @@
|
||||
b4bfd459865a8d636f26aca0d330ae297c006c3c
|
||||
f6344b75dcf861d8bf1f1322780b8811f982e31a
|
||||
|
@ -5,13 +5,13 @@ dartdoc:
|
||||
# The dev/bots/docs.sh script does this automatically.
|
||||
tools:
|
||||
snippet:
|
||||
command: ["bin/cache/dart-sdk/bin/dart", "pub", "global", "run", "snippets", "--output-directory=doc/snippets", "--type=snippet"]
|
||||
command: ["bin/cache/artifacts/snippets/snippets", "--output-directory=doc/snippets", "--type=snippet"]
|
||||
description: "Creates sample code documentation output from embedded documentation samples."
|
||||
sample:
|
||||
command: ["bin/cache/dart-sdk/bin/dart", "pub", "global", "run", "snippets", "--output-directory=doc/snippets", "--type=sample"]
|
||||
command: ["bin/cache/artifacts/snippets/snippets", "--output-directory=doc/snippets", "--type=sample"]
|
||||
description: "Creates full application sample code documentation output from embedded documentation samples."
|
||||
dartpad:
|
||||
command: ["bin/cache/dart-sdk/bin/dart", "pub", "global", "run", "snippets", "--output-directory=doc/snippets", "--type=dartpad"]
|
||||
command: ["bin/cache/artifacts/snippets/snippets", "--output-directory=doc/snippets", "--type=dartpad"]
|
||||
description: "Creates full application sample code documentation output from embedded documentation samples and displays it in an embedded DartPad."
|
||||
errors:
|
||||
## Default errors of dartdoc:
|
||||
|
@ -107,16 +107,25 @@ function parse_args() {
|
||||
fi
|
||||
}
|
||||
|
||||
function build_snippets_tool() (
|
||||
local snippets_dir="$FLUTTER_ROOT/dev/snippets"
|
||||
local output_dir="$FLUTTER_BIN/cache/artifacts/snippets"
|
||||
echo "Building snippets tool executable."
|
||||
command cd "$snippets_dir"
|
||||
mkdir -p "$output_dir"
|
||||
dart pub get
|
||||
dart compile exe -o "$output_dir/snippets" bin/snippets.dart
|
||||
)
|
||||
|
||||
function generate_docs() {
|
||||
# Install and activate dartdoc.
|
||||
# When updating to a new dartdoc version, please also update
|
||||
# `dartdoc_options.yaml` to include newly introduced error and warning types.
|
||||
"$DART" pub global activate dartdoc 8.0.6
|
||||
|
||||
# Install and activate the snippets tool, which resides in the
|
||||
# assets-for-api-docs repo:
|
||||
# https://github.com/flutter/assets-for-api-docs/tree/main/packages/snippets
|
||||
"$DART" pub global activate snippets 0.4.3
|
||||
# Build and install the snippets tool, which resides in
|
||||
# the dev/docs/snippets directory.
|
||||
build_snippets_tool
|
||||
|
||||
# This script generates a unified doc set, and creates
|
||||
# a custom index.html, placing everything into DOC_DIR.
|
||||
|
204
dev/snippets/README.md
Normal file
204
dev/snippets/README.md
Normal file
@ -0,0 +1,204 @@
|
||||
# Dartdoc Sample Generation
|
||||
|
||||
The Flutter API documentation contains code blocks that help provide context or
|
||||
a good starting point when learning to use any of Flutter's APIs.
|
||||
|
||||
To generate these code blocks, Flutter uses dartdoc tools to turn documentation
|
||||
in the source code into API documentation, as seen on [https://api.flutter.dev/]
|
||||
|
||||
## Table of Contents
|
||||
|
||||
- [Types of code blocks](#types-of-code-blocks)
|
||||
- [Snippet tool](#snippet-tool)
|
||||
- [Sample tool](#sample-tool)
|
||||
- [Skeletons](#skeletons)
|
||||
- [Test Doc Generation Workflow](#test-doc-generation-workflow)
|
||||
|
||||
## Types of code blocks
|
||||
|
||||
There are three kinds of code blocks.
|
||||
|
||||
- A `snippet`, which is a more or less context-free code snippet that we
|
||||
magically determine how to analyze.
|
||||
|
||||
- A `dartpad` sample, which gets placed into a full-fledged application, and can
|
||||
be executed inline in the documentation on the web page using
|
||||
DartPad.
|
||||
|
||||
- A `sample`, which gets placed into a full-fledged application, but isn't
|
||||
placed into DartPad in the documentation because it doesn't make sense to do
|
||||
so.
|
||||
|
||||
Ideally, every sample is a DartPad sample, but some samples don't have any visual
|
||||
representation and some just don't make sense that way (for example, sample
|
||||
code for setting the system UI's notification area color on Android won't do
|
||||
anything on the web).
|
||||
|
||||
### Snippet Tool
|
||||
|
||||

|
||||
|
||||
The code `snippet` tool generates a block containing a description and example
|
||||
code. Here is an example of the code `snippet` tool in use:
|
||||
|
||||
```dart
|
||||
/// {@tool snippet}
|
||||
///
|
||||
/// If the avatar is to have an image, the image should be specified in the
|
||||
/// [backgroundImage] property:
|
||||
///
|
||||
/// ```dart
|
||||
/// CircleAvatar(
|
||||
/// backgroundImage: NetworkImage(userAvatarUrl),
|
||||
/// )
|
||||
/// ```
|
||||
/// {@end-tool}
|
||||
```
|
||||
|
||||
This will generate sample code that can be copied to the clipboard and added to
|
||||
existing applications.
|
||||
|
||||
This uses the skeleton for `snippet` snippets when generating the HTML to put
|
||||
into the Dart docs. You can find this [template in the Flutter
|
||||
repo](https://github.com/flutter/flutter/blob/main/dev/snippets/config/skeletons/snippet.html).
|
||||
|
||||
#### Analysis
|
||||
|
||||
The
|
||||
[`analyze_sample_code.dart`](https://github.com/flutter/flutter/blob/main/dev/bots/analyze_sample_code.dart)
|
||||
script finds code inside the `@tool
|
||||
snippet` sections and uses the Dart analyzer to check them.
|
||||
|
||||
There are several kinds of sample code you can specify:
|
||||
|
||||
- Constructor calls, typically showing what might exist in a build method. These
|
||||
will be inserted into an assignment expression assigning to a variable of type
|
||||
"dynamic" and followed by a semicolon, for analysis.
|
||||
|
||||
- Class definitions. These start with "class", and are analyzed verbatim.
|
||||
|
||||
- Other code. It gets included verbatim, though any line that says `// ...` is
|
||||
considered to separate the block into multiple blocks to be processed
|
||||
individually.
|
||||
|
||||
The above means that it's tricky to include verbatim imperative code (e.g. a
|
||||
call to a method) since it won't be valid to have such code at the top level.
|
||||
Instead, wrap it in a function or even a whole class, or make it a valid
|
||||
variable declaration.
|
||||
|
||||
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:
|
||||
|
||||
```dart
|
||||
// Examples can assume:
|
||||
// final BuildContext context;
|
||||
// final String userAvatarUrl;
|
||||
```
|
||||
|
||||
You can assume that the entire Flutter framework and most common
|
||||
`dart:*` packages are imported and in scope; `dart:math` as `math` and
|
||||
`dart:ui` as `ui`.
|
||||
|
||||
### Sample Tool
|
||||
|
||||

|
||||
|
||||
The code `sample` and `dartpad` tools can expand sample code into full Flutter
|
||||
applications. These sample applications can be directly copied and used to
|
||||
demonstrate the API's functionality in a sample application, or used with the
|
||||
`flutter create` command to create a local project with the sample code. The
|
||||
`dartpad` samples are embedded into the API docs web page and are live
|
||||
applications in the API documentation.
|
||||
|
||||
This uses the skeleton for [application](https://github.com/flutter/flutter/blob/main/dev/snippets/config/skeletons/sample.html)
|
||||
snippets in the Flutter repo.
|
||||
|
||||
The `sample` and `dartpad` tools also allow for quick Flutter app generation
|
||||
using the following command:
|
||||
|
||||
```bash
|
||||
flutter create --sample=[directory.File.sampleNumber] [name_of_project_directory]
|
||||
```
|
||||
|
||||
This command is displayed as part of the sample in the API docs.
|
||||
|
||||
#### Sample Analysis
|
||||
|
||||
The [`../bots/analyze_sample_code.dart`](../bots/analyze_sample_code.dart)
|
||||
script finds code inside the `@tool sample` sections and uses the Dart analyzer
|
||||
to check the sample code.
|
||||
|
||||
## Skeletons
|
||||
|
||||
A skeleton (concerning this tool) is an HTML template into which the Dart
|
||||
code blocks and descriptions are interpolated.
|
||||
|
||||
There is currently one skeleton for
|
||||
[application](https://github.com/flutter/flutter/blob/main/dev/snippets/config/skeletons/sample.html)
|
||||
samples, one for
|
||||
[dartpad](https://github.com/flutter/flutter/blob/main/dev/snippets/config/skeletons/dartpad-sample.html),
|
||||
and one for
|
||||
[snippet](https://github.com/flutter/flutter/blob/main/dev/snippets/config/skeletons/snippet.html)
|
||||
code samples, but there could be more.
|
||||
|
||||
Skeletons use mustache notation (e.g. `{{code}}`) to mark where components will
|
||||
be interpolated into the template. It doesn't use the mustache
|
||||
package since these are simple string substitutions, but it uses the same
|
||||
syntax.
|
||||
|
||||
The code block generation tools that process the source input and emit HTML for
|
||||
output, which dartdoc places back into the documentation. Any options given to
|
||||
the `{@tool ...}` directive are passed on verbatim to the tool.
|
||||
|
||||
The `snippets` tool renders these examples through a combination of markdown
|
||||
and HTML using the `{@inject-html}` dartdoc directive.
|
||||
|
||||
## Test Doc Generation Workflow
|
||||
|
||||
If you are making changes to an existing code block or are creating a new code
|
||||
block, follow these steps to generate a local copy of the API docs and verify
|
||||
that your code blocks are showing up correctly:
|
||||
|
||||
1. Make an update to a code block or create a new code block.
|
||||
2. From the root directory, run `./dev/bots/docs.sh`. This should start
|
||||
generating a local copy of the API documentation.
|
||||
Supplying the "--output" argument allows you to specify the output zip file
|
||||
for the completed documentation. Defaults to `api_docs.zip`` in the current
|
||||
directory.
|
||||
3. Once complete, unzip the files to the desired location and open the `index.html`
|
||||
within.
|
||||
|
||||
Note that generating the sample output will not allow you to run your code in
|
||||
DartPad, because DartPad pulls the code it runs from the appropriate docs server
|
||||
(main or stable).
|
||||
|
||||
Copy the generated code and paste it into a regular DartPad instance to test if
|
||||
it runs in DartPad. To get the code that will be produced by your documentation
|
||||
changes, run sample analysis locally (see the next section) and paste the output
|
||||
into a DartPad at [https://dartpad.dartlang.org].
|
||||
|
||||
## Running sample analysis locally
|
||||
|
||||
If all you want to do is analyze the sample code you have written locally, then
|
||||
generating the entire docs output takes a long time.
|
||||
|
||||
Instead, you can run the analysis locally with this command from the Flutter root:
|
||||
|
||||
```bash
|
||||
TMPDIR=/tmp bin/cache/dart-sdk/bin/dart dev/bots/analyze_sample_code.dart --temp=samples
|
||||
```
|
||||
|
||||
This will analyze the samples, and leave the generated files in `/tmp/samples`
|
||||
|
||||
You can find the sample you are working on in `/tmp/samples`. It is named using the
|
||||
path to the file it is in, and the line of the file that the `{@tool ...}` directive
|
||||
is on.
|
||||
|
||||
For example, the file `sample.src.widgets.animated_list.52.dart` points to the sample
|
||||
in `packages/flutter/src/widgets/animated_list.dart` at line 52. You can then take the
|
||||
contents of that file, and paste it into [Dartpad](https://dartpad.dev) and see if it
|
||||
works. If the sample relies on new features that have just landed, it may not work
|
||||
until the features make it into the `dev` branch.
|
286
dev/snippets/bin/snippets.dart
Normal file
286
dev/snippets/bin/snippets.dart
Normal file
@ -0,0 +1,286 @@
|
||||
// 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' show ProcessResult, exitCode, stderr;
|
||||
|
||||
import 'package:args/args.dart';
|
||||
import 'package:file/file.dart';
|
||||
import 'package:file/local.dart';
|
||||
import 'package:path/path.dart' as path;
|
||||
import 'package:platform/platform.dart';
|
||||
import 'package:process/process.dart';
|
||||
import 'package:snippets/snippets.dart';
|
||||
|
||||
const String _kElementOption = 'element';
|
||||
const String _kFormatOutputOption = 'format-output';
|
||||
const String _kHelpOption = 'help';
|
||||
const String _kInputOption = 'input';
|
||||
const String _kLibraryOption = 'library';
|
||||
const String _kOutputDirectoryOption = 'output-directory';
|
||||
const String _kOutputOption = 'output';
|
||||
const String _kPackageOption = 'package';
|
||||
const String _kSerialOption = 'serial';
|
||||
const String _kTypeOption = 'type';
|
||||
|
||||
class GitStatusFailed implements Exception {
|
||||
GitStatusFailed(this.gitResult);
|
||||
|
||||
final ProcessResult gitResult;
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return 'git status exited with a non-zero exit code: '
|
||||
'${gitResult.exitCode}:\n${gitResult.stderr}\n${gitResult.stdout}';
|
||||
}
|
||||
}
|
||||
|
||||
/// A singleton filesystem that can be set by tests to a memory filesystem.
|
||||
FileSystem filesystem = const LocalFileSystem();
|
||||
|
||||
/// A singleton snippet generator that can be set by tests to a mock, so that
|
||||
/// we can test the command line parsing.
|
||||
SnippetGenerator snippetGenerator = SnippetGenerator();
|
||||
|
||||
/// A singleton platform that can be set by tests for use in testing command line
|
||||
/// parsing.
|
||||
Platform platform = const LocalPlatform();
|
||||
|
||||
/// A singleton process manager that can be set by tests for use in testing.
|
||||
ProcessManager processManager = const LocalProcessManager();
|
||||
|
||||
/// Get the name of the channel these docs are from.
|
||||
///
|
||||
/// First check env variable LUCI_BRANCH, then refer to the currently
|
||||
/// checked out git branch.
|
||||
String getChannelName({
|
||||
Platform platform = const LocalPlatform(),
|
||||
ProcessManager processManager = const LocalProcessManager(),
|
||||
}) {
|
||||
final String? envReleaseChannel = platform.environment['LUCI_BRANCH']?.trim();
|
||||
if (<String>['master', 'stable', 'main'].contains(envReleaseChannel)) {
|
||||
// Backward compatibility: Still support running on "master", but pretend it is "main".
|
||||
if (envReleaseChannel == 'master') {
|
||||
return 'main';
|
||||
}
|
||||
return envReleaseChannel!;
|
||||
}
|
||||
|
||||
final RegExp gitBranchRegexp = RegExp(r'^## (?<branch>.*)');
|
||||
final ProcessResult gitResult = processManager.runSync(
|
||||
<String>['git', 'status', '-b', '--porcelain'],
|
||||
// Use the FLUTTER_ROOT, if defined.
|
||||
workingDirectory: platform.environment['FLUTTER_ROOT']?.trim() ??
|
||||
filesystem.currentDirectory.path,
|
||||
// Adding extra debugging output to help debug why git status inexplicably fails
|
||||
// (random non-zero error code) about 2% of the time.
|
||||
environment: <String, String>{'GIT_TRACE': '2', 'GIT_TRACE_SETUP': '2'});
|
||||
if (gitResult.exitCode != 0) {
|
||||
throw GitStatusFailed(gitResult);
|
||||
}
|
||||
|
||||
final RegExpMatch? gitBranchMatch = gitBranchRegexp
|
||||
.firstMatch((gitResult.stdout as String).trim().split('\n').first);
|
||||
return gitBranchMatch == null
|
||||
? '<unknown>'
|
||||
: gitBranchMatch.namedGroup('branch')!.split('...').first;
|
||||
}
|
||||
|
||||
const List<String> sampleTypes = <String>[
|
||||
'snippet',
|
||||
'sample',
|
||||
'dartpad',
|
||||
];
|
||||
|
||||
// This is a hack to workaround the fact that git status inexplicably fails
|
||||
// (with random non-zero error code) about 2% of the time.
|
||||
String getChannelNameWithRetries({
|
||||
Platform platform = const LocalPlatform(),
|
||||
ProcessManager processManager = const LocalProcessManager(),
|
||||
}) {
|
||||
int retryCount = 0;
|
||||
|
||||
while (retryCount < 2) {
|
||||
try {
|
||||
return getChannelName(platform: platform, processManager: processManager);
|
||||
} on GitStatusFailed catch (e) {
|
||||
retryCount += 1;
|
||||
stderr.write(
|
||||
'git status failed, retrying ($retryCount)\nError report:\n$e');
|
||||
}
|
||||
}
|
||||
|
||||
return getChannelName(platform: platform, processManager: processManager);
|
||||
}
|
||||
|
||||
/// Generates snippet dartdoc output for a given input, and creates any sample
|
||||
/// applications needed by the snippet.
|
||||
void main(List<String> argList) {
|
||||
final Map<String, String> environment = platform.environment;
|
||||
final ArgParser parser = ArgParser();
|
||||
|
||||
parser.addOption(
|
||||
_kTypeOption,
|
||||
defaultsTo: 'dartpad',
|
||||
allowed: sampleTypes,
|
||||
allowedHelp: <String, String>{
|
||||
'dartpad':
|
||||
'Produce a code sample application for using in Dartpad.',
|
||||
'sample':
|
||||
'Produce a code sample application.',
|
||||
'snippet':
|
||||
'Produce a nicely formatted piece of sample code.',
|
||||
},
|
||||
help: 'The type of snippet to produce.',
|
||||
);
|
||||
parser.addOption(
|
||||
_kOutputOption,
|
||||
help: 'The output name for the generated sample application. Overrides '
|
||||
'the naming generated by the --$_kPackageOption/--$_kLibraryOption/--$_kElementOption '
|
||||
'arguments. Metadata will be written alongside in a .json file. '
|
||||
'The basename of this argument is used as the ID. If this is a '
|
||||
'relative path, will be placed under the --$_kOutputDirectoryOption location.',
|
||||
);
|
||||
parser.addOption(
|
||||
_kOutputDirectoryOption,
|
||||
defaultsTo: '.',
|
||||
help: 'The output path for the generated sample application.',
|
||||
);
|
||||
parser.addOption(
|
||||
_kInputOption,
|
||||
defaultsTo: environment['INPUT'],
|
||||
help: 'The input file containing the sample code to inject.',
|
||||
);
|
||||
parser.addOption(
|
||||
_kPackageOption,
|
||||
defaultsTo: environment['PACKAGE_NAME'],
|
||||
help: 'The name of the package that this sample belongs to.',
|
||||
);
|
||||
parser.addOption(
|
||||
_kLibraryOption,
|
||||
defaultsTo: environment['LIBRARY_NAME'],
|
||||
help: 'The name of the library that this sample belongs to.',
|
||||
);
|
||||
parser.addOption(
|
||||
_kElementOption,
|
||||
defaultsTo: environment['ELEMENT_NAME'],
|
||||
help: 'The name of the element that this sample belongs to.',
|
||||
);
|
||||
parser.addOption(
|
||||
_kSerialOption,
|
||||
defaultsTo: environment['INVOCATION_INDEX'],
|
||||
help: 'A unique serial number for this snippet tool invocation.',
|
||||
);
|
||||
parser.addFlag(
|
||||
_kFormatOutputOption,
|
||||
defaultsTo: true,
|
||||
help: 'Applies the Dart formatter to the published/extracted sample code.',
|
||||
);
|
||||
parser.addFlag(
|
||||
_kHelpOption,
|
||||
negatable: false,
|
||||
help: 'Prints help documentation for this command',
|
||||
);
|
||||
|
||||
final ArgResults args = parser.parse(argList);
|
||||
|
||||
if (args[_kHelpOption]! as bool) {
|
||||
stderr.writeln(parser.usage);
|
||||
exitCode = 0;
|
||||
return;
|
||||
}
|
||||
|
||||
final String sampleType = args[_kTypeOption]! as String;
|
||||
|
||||
if (args[_kInputOption] == null) {
|
||||
stderr.writeln(parser.usage);
|
||||
errorExit(
|
||||
'The --$_kInputOption option must be specified, either on the command '
|
||||
'line, or in the INPUT environment variable.');
|
||||
return;
|
||||
}
|
||||
|
||||
final File input = filesystem.file(args['input']! as String);
|
||||
if (!input.existsSync()) {
|
||||
errorExit('The input file ${input.path} does not exist.');
|
||||
return;
|
||||
}
|
||||
|
||||
final bool formatOutput = args[_kFormatOutputOption]! as bool;
|
||||
final String packageName = args[_kPackageOption] as String? ?? '';
|
||||
final String libraryName = args[_kLibraryOption] as String? ?? '';
|
||||
final String elementName = args[_kElementOption] as String? ?? '';
|
||||
final String serial = args[_kSerialOption] as String? ?? '';
|
||||
late String id;
|
||||
File? output;
|
||||
final Directory outputDirectory =
|
||||
filesystem.directory(args[_kOutputDirectoryOption]! as String).absolute;
|
||||
|
||||
if (args[_kOutputOption] != null) {
|
||||
id = path.basenameWithoutExtension(args[_kOutputOption]! as String);
|
||||
final File outputPath = filesystem.file(args[_kOutputOption]! as String);
|
||||
if (outputPath.isAbsolute) {
|
||||
output = outputPath;
|
||||
} else {
|
||||
output =
|
||||
filesystem.file(path.join(outputDirectory.path, outputPath.path));
|
||||
}
|
||||
} else {
|
||||
final List<String> idParts = <String>[];
|
||||
if (packageName.isNotEmpty && packageName != 'flutter') {
|
||||
idParts.add(packageName.replaceAll(RegExp(r'\W'), '_').toLowerCase());
|
||||
}
|
||||
if (libraryName.isNotEmpty) {
|
||||
idParts.add(libraryName.replaceAll(RegExp(r'\W'), '_').toLowerCase());
|
||||
}
|
||||
if (elementName.isNotEmpty) {
|
||||
idParts.add(elementName);
|
||||
}
|
||||
if (serial.isNotEmpty) {
|
||||
idParts.add(serial);
|
||||
}
|
||||
if (idParts.isEmpty) {
|
||||
errorExit('Unable to determine ID. At least one of --$_kPackageOption, '
|
||||
'--$_kLibraryOption, --$_kElementOption, -$_kSerialOption, or the environment variables '
|
||||
'PACKAGE_NAME, LIBRARY_NAME, ELEMENT_NAME, or INVOCATION_INDEX must be non-empty.');
|
||||
return;
|
||||
}
|
||||
id = idParts.join('.');
|
||||
output = outputDirectory.childFile('$id.dart');
|
||||
}
|
||||
output.parent.createSync(recursive: true);
|
||||
|
||||
final int? sourceLine = environment['SOURCE_LINE'] != null
|
||||
? int.tryParse(environment['SOURCE_LINE']!)
|
||||
: null;
|
||||
final String sourcePath = environment['SOURCE_PATH'] ?? 'unknown.dart';
|
||||
final SnippetDartdocParser sampleParser = SnippetDartdocParser(filesystem);
|
||||
final SourceElement element = sampleParser.parseFromDartdocToolFile(
|
||||
input,
|
||||
startLine: sourceLine,
|
||||
element: elementName,
|
||||
sourceFile: filesystem.file(sourcePath),
|
||||
type: sampleType,
|
||||
);
|
||||
final Map<String, Object?> metadata = <String, Object?>{
|
||||
'channel': getChannelNameWithRetries(
|
||||
platform: platform, processManager: processManager),
|
||||
'serial': serial,
|
||||
'id': id,
|
||||
'package': packageName,
|
||||
'library': libraryName,
|
||||
'element': elementName,
|
||||
};
|
||||
|
||||
for (final CodeSample sample in element.samples) {
|
||||
sample.metadata.addAll(metadata);
|
||||
snippetGenerator.generateCode(
|
||||
sample,
|
||||
output: output,
|
||||
formatOutput: formatOutput,
|
||||
);
|
||||
print(snippetGenerator.generateHtml(sample));
|
||||
}
|
||||
|
||||
exitCode = 0;
|
||||
}
|
11
dev/snippets/lib/snippets.dart
Normal file
11
dev/snippets/lib/snippets.dart
Normal file
@ -0,0 +1,11 @@
|
||||
// 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.
|
||||
|
||||
export 'src/analysis.dart';
|
||||
export 'src/configuration.dart';
|
||||
export 'src/data_types.dart';
|
||||
export 'src/import_sorter.dart';
|
||||
export 'src/snippet_generator.dart';
|
||||
export 'src/snippet_parser.dart';
|
||||
export 'src/util.dart';
|
361
dev/snippets/lib/src/analysis.dart
Normal file
361
dev/snippets/lib/src/analysis.dart
Normal file
@ -0,0 +1,361 @@
|
||||
// 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 'package:analyzer/dart/analysis/features.dart';
|
||||
import 'package:analyzer/dart/analysis/results.dart';
|
||||
import 'package:analyzer/dart/analysis/utilities.dart';
|
||||
import 'package:analyzer/dart/ast/ast.dart';
|
||||
import 'package:analyzer/dart/ast/token.dart';
|
||||
import 'package:analyzer/dart/ast/visitor.dart';
|
||||
import 'package:analyzer/file_system/file_system.dart' as afs;
|
||||
import 'package:analyzer/file_system/physical_file_system.dart' as afs;
|
||||
import 'package:analyzer/source/line_info.dart';
|
||||
import 'package:file/file.dart';
|
||||
|
||||
import 'data_types.dart';
|
||||
import 'util.dart';
|
||||
|
||||
/// Gets an iterable over all of the blocks of documentation comments in a file
|
||||
/// using the analyzer.
|
||||
///
|
||||
/// Each entry in the list is a list of source lines corresponding to the
|
||||
/// documentation comment block.
|
||||
Iterable<List<SourceLine>> getFileDocumentationComments(File file) {
|
||||
return getDocumentationComments(getFileElements(file));
|
||||
}
|
||||
|
||||
/// Gets an iterable over all of the blocks of documentation comments from an
|
||||
/// iterable over the [SourceElement]s involved.
|
||||
Iterable<List<SourceLine>> getDocumentationComments(
|
||||
Iterable<SourceElement> elements) {
|
||||
return elements
|
||||
.where((SourceElement element) => element.comment.isNotEmpty)
|
||||
.map<List<SourceLine>>((SourceElement element) => element.comment);
|
||||
}
|
||||
|
||||
/// Gets an iterable over the comment [SourceElement]s in a file.
|
||||
Iterable<SourceElement> getFileCommentElements(File file) {
|
||||
return getCommentElements(getFileElements(file));
|
||||
}
|
||||
|
||||
/// Filters the source `elements` to only return the comment elements.
|
||||
Iterable<SourceElement> getCommentElements(Iterable<SourceElement> elements) {
|
||||
return elements.where((SourceElement element) => element.comment.isNotEmpty);
|
||||
}
|
||||
|
||||
/// Reads the file content from a string, to avoid having to read the file more
|
||||
/// than once if the caller already has the content in memory.
|
||||
///
|
||||
/// The `file` argument is used to tag the lines with a filename that they came from.
|
||||
Iterable<SourceElement> getElementsFromString(String content, File file) {
|
||||
final ParseStringResult parseResult = parseString(
|
||||
featureSet: FeatureSet.fromEnableFlags2(
|
||||
sdkLanguageVersion: FlutterInformation.instance.getDartSdkVersion(),
|
||||
flags: <String>[],
|
||||
),
|
||||
content: content);
|
||||
final _SourceVisitor<CompilationUnit> visitor =
|
||||
_SourceVisitor<CompilationUnit>(file);
|
||||
visitor.visitCompilationUnit(parseResult.unit);
|
||||
visitor.assignLineNumbers();
|
||||
return visitor.elements;
|
||||
}
|
||||
|
||||
/// Gets an iterable over the [SourceElement]s in the given `file`.
|
||||
///
|
||||
/// Takes an optional [ResourceProvider] to allow reading from a memory
|
||||
/// filesystem.
|
||||
Iterable<SourceElement> getFileElements(File file,
|
||||
{afs.ResourceProvider? resourceProvider}) {
|
||||
resourceProvider ??= afs.PhysicalResourceProvider.INSTANCE;
|
||||
final ParseStringResult parseResult = parseFile(
|
||||
featureSet: FeatureSet.fromEnableFlags2(
|
||||
sdkLanguageVersion: FlutterInformation.instance.getDartSdkVersion(),
|
||||
flags: <String>[],
|
||||
),
|
||||
path: file.absolute.path,
|
||||
resourceProvider: resourceProvider);
|
||||
final _SourceVisitor<CompilationUnit> visitor =
|
||||
_SourceVisitor<CompilationUnit>(file);
|
||||
visitor.visitCompilationUnit(parseResult.unit);
|
||||
visitor.assignLineNumbers();
|
||||
return visitor.elements;
|
||||
}
|
||||
|
||||
class _SourceVisitor<T> extends RecursiveAstVisitor<T> {
|
||||
_SourceVisitor(this.file) : elements = <SourceElement>{};
|
||||
|
||||
final Set<SourceElement> elements;
|
||||
String enclosingClass = '';
|
||||
|
||||
File file;
|
||||
|
||||
void assignLineNumbers() {
|
||||
final String contents = file.readAsStringSync();
|
||||
final LineInfo lineInfo = LineInfo.fromContent(contents);
|
||||
|
||||
final Set<SourceElement> removedElements = <SourceElement>{};
|
||||
final Set<SourceElement> replacedElements = <SourceElement>{};
|
||||
for (final SourceElement element in elements) {
|
||||
final List<SourceLine> newLines = <SourceLine>[];
|
||||
for (final SourceLine line in element.comment) {
|
||||
final CharacterLocation intervalLine =
|
||||
lineInfo.getLocation(line.startChar);
|
||||
newLines.add(line.copyWith(line: intervalLine.lineNumber));
|
||||
}
|
||||
final int elementLine = lineInfo.getLocation(element.startPos).lineNumber;
|
||||
replacedElements
|
||||
.add(element.copyWith(comment: newLines, startLine: elementLine));
|
||||
removedElements.add(element);
|
||||
}
|
||||
elements.removeAll(removedElements);
|
||||
elements.addAll(replacedElements);
|
||||
}
|
||||
|
||||
List<SourceLine> _processComment(String element, Comment comment) {
|
||||
final List<SourceLine> result = <SourceLine>[];
|
||||
if (comment.tokens.isNotEmpty) {
|
||||
for (final Token token in comment.tokens) {
|
||||
result.add(SourceLine(
|
||||
token.toString(),
|
||||
element: element,
|
||||
file: file,
|
||||
startChar: token.charOffset,
|
||||
endChar: token.charEnd,
|
||||
));
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
@override
|
||||
T? visitCompilationUnit(CompilationUnit node) {
|
||||
elements.clear();
|
||||
return super.visitCompilationUnit(node);
|
||||
}
|
||||
|
||||
static bool isPublic(String name) {
|
||||
return !name.startsWith('_');
|
||||
}
|
||||
|
||||
static bool isInsideMethod(AstNode startNode) {
|
||||
AstNode? node = startNode.parent;
|
||||
while (node != null) {
|
||||
if (node is MethodDeclaration) {
|
||||
return true;
|
||||
}
|
||||
node = node.parent;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
@override
|
||||
T? visitTopLevelVariableDeclaration(TopLevelVariableDeclaration node) {
|
||||
for (final VariableDeclaration declaration in node.variables.variables) {
|
||||
if (!isPublic(declaration.name.lexeme)) {
|
||||
continue;
|
||||
}
|
||||
List<SourceLine> comment = <SourceLine>[];
|
||||
if (node.documentationComment != null &&
|
||||
node.documentationComment!.tokens.isNotEmpty) {
|
||||
comment = _processComment(
|
||||
declaration.name.lexeme, node.documentationComment!);
|
||||
}
|
||||
elements.add(
|
||||
SourceElement(
|
||||
SourceElementType.topLevelVariableType,
|
||||
declaration.name.lexeme,
|
||||
node.beginToken.charOffset,
|
||||
file: file,
|
||||
className: enclosingClass,
|
||||
comment: comment,
|
||||
),
|
||||
);
|
||||
}
|
||||
return super.visitTopLevelVariableDeclaration(node);
|
||||
}
|
||||
|
||||
@override
|
||||
T? visitGenericTypeAlias(GenericTypeAlias node) {
|
||||
if (isPublic(node.name.lexeme)) {
|
||||
List<SourceLine> comment = <SourceLine>[];
|
||||
if (node.documentationComment != null &&
|
||||
node.documentationComment!.tokens.isNotEmpty) {
|
||||
comment = _processComment(node.name.lexeme, node.documentationComment!);
|
||||
}
|
||||
elements.add(
|
||||
SourceElement(
|
||||
SourceElementType.typedefType,
|
||||
node.name.lexeme,
|
||||
node.beginToken.charOffset,
|
||||
file: file,
|
||||
comment: comment,
|
||||
),
|
||||
);
|
||||
}
|
||||
return super.visitGenericTypeAlias(node);
|
||||
}
|
||||
|
||||
@override
|
||||
T? visitFieldDeclaration(FieldDeclaration node) {
|
||||
for (final VariableDeclaration declaration in node.fields.variables) {
|
||||
if (!isPublic(declaration.name.lexeme) || !isPublic(enclosingClass)) {
|
||||
continue;
|
||||
}
|
||||
List<SourceLine> comment = <SourceLine>[];
|
||||
if (node.documentationComment != null &&
|
||||
node.documentationComment!.tokens.isNotEmpty) {
|
||||
assert(enclosingClass.isNotEmpty);
|
||||
comment = _processComment('$enclosingClass.${declaration.name.lexeme}',
|
||||
node.documentationComment!);
|
||||
}
|
||||
elements.add(
|
||||
SourceElement(
|
||||
SourceElementType.fieldType,
|
||||
declaration.name.lexeme,
|
||||
node.beginToken.charOffset,
|
||||
file: file,
|
||||
className: enclosingClass,
|
||||
comment: comment,
|
||||
override: _isOverridden(node),
|
||||
),
|
||||
);
|
||||
return super.visitFieldDeclaration(node);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
@override
|
||||
T? visitConstructorDeclaration(ConstructorDeclaration node) {
|
||||
final String fullName =
|
||||
'$enclosingClass${node.name == null ? '' : '.${node.name}'}';
|
||||
if (isPublic(enclosingClass) &&
|
||||
(node.name == null || isPublic(node.name!.lexeme))) {
|
||||
List<SourceLine> comment = <SourceLine>[];
|
||||
if (node.documentationComment != null &&
|
||||
node.documentationComment!.tokens.isNotEmpty) {
|
||||
comment = _processComment(
|
||||
'$enclosingClass.$fullName', node.documentationComment!);
|
||||
}
|
||||
elements.add(
|
||||
SourceElement(
|
||||
SourceElementType.constructorType,
|
||||
fullName,
|
||||
node.beginToken.charOffset,
|
||||
file: file,
|
||||
className: enclosingClass,
|
||||
comment: comment,
|
||||
),
|
||||
);
|
||||
}
|
||||
return super.visitConstructorDeclaration(node);
|
||||
}
|
||||
|
||||
@override
|
||||
T? visitFunctionDeclaration(FunctionDeclaration node) {
|
||||
if (isPublic(node.name.lexeme)) {
|
||||
List<SourceLine> comment = <SourceLine>[];
|
||||
// Skip functions that are defined inside of methods.
|
||||
if (!isInsideMethod(node)) {
|
||||
if (node.documentationComment != null &&
|
||||
node.documentationComment!.tokens.isNotEmpty) {
|
||||
comment =
|
||||
_processComment(node.name.lexeme, node.documentationComment!);
|
||||
}
|
||||
elements.add(
|
||||
SourceElement(
|
||||
SourceElementType.functionType,
|
||||
node.name.lexeme,
|
||||
node.beginToken.charOffset,
|
||||
file: file,
|
||||
comment: comment,
|
||||
override: _isOverridden(node),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
return super.visitFunctionDeclaration(node);
|
||||
}
|
||||
|
||||
@override
|
||||
T? visitMethodDeclaration(MethodDeclaration node) {
|
||||
if (isPublic(node.name.lexeme) && isPublic(enclosingClass)) {
|
||||
List<SourceLine> comment = <SourceLine>[];
|
||||
if (node.documentationComment != null &&
|
||||
node.documentationComment!.tokens.isNotEmpty) {
|
||||
assert(enclosingClass.isNotEmpty);
|
||||
comment = _processComment(
|
||||
'$enclosingClass.${node.name.lexeme}', node.documentationComment!);
|
||||
}
|
||||
elements.add(
|
||||
SourceElement(
|
||||
SourceElementType.methodType,
|
||||
node.name.lexeme,
|
||||
node.beginToken.charOffset,
|
||||
file: file,
|
||||
className: enclosingClass,
|
||||
comment: comment,
|
||||
override: _isOverridden(node),
|
||||
),
|
||||
);
|
||||
}
|
||||
return super.visitMethodDeclaration(node);
|
||||
}
|
||||
|
||||
bool _isOverridden(AnnotatedNode node) {
|
||||
return node.metadata.where((Annotation annotation) {
|
||||
return annotation.name.name == 'override';
|
||||
}).isNotEmpty;
|
||||
}
|
||||
|
||||
@override
|
||||
T? visitMixinDeclaration(MixinDeclaration node) {
|
||||
enclosingClass = node.name.lexeme;
|
||||
if (!node.name.lexeme.startsWith('_')) {
|
||||
enclosingClass = node.name.lexeme;
|
||||
List<SourceLine> comment = <SourceLine>[];
|
||||
if (node.documentationComment != null &&
|
||||
node.documentationComment!.tokens.isNotEmpty) {
|
||||
comment = _processComment(node.name.lexeme, node.documentationComment!);
|
||||
}
|
||||
elements.add(
|
||||
SourceElement(
|
||||
SourceElementType.classType,
|
||||
node.name.lexeme,
|
||||
node.beginToken.charOffset,
|
||||
file: file,
|
||||
comment: comment,
|
||||
),
|
||||
);
|
||||
}
|
||||
final T? result = super.visitMixinDeclaration(node);
|
||||
enclosingClass = '';
|
||||
return result;
|
||||
}
|
||||
|
||||
@override
|
||||
T? visitClassDeclaration(ClassDeclaration node) {
|
||||
enclosingClass = node.name.lexeme;
|
||||
if (!node.name.lexeme.startsWith('_')) {
|
||||
enclosingClass = node.name.lexeme;
|
||||
List<SourceLine> comment = <SourceLine>[];
|
||||
if (node.documentationComment != null &&
|
||||
node.documentationComment!.tokens.isNotEmpty) {
|
||||
comment = _processComment(node.name.lexeme, node.documentationComment!);
|
||||
}
|
||||
elements.add(
|
||||
SourceElement(
|
||||
SourceElementType.classType,
|
||||
node.name.lexeme,
|
||||
node.beginToken.charOffset,
|
||||
file: file,
|
||||
comment: comment,
|
||||
),
|
||||
);
|
||||
}
|
||||
final T? result = super.visitClassDeclaration(node);
|
||||
enclosingClass = '';
|
||||
return result;
|
||||
}
|
||||
}
|
53
dev/snippets/lib/src/configuration.dart
Normal file
53
dev/snippets/lib/src/configuration.dart
Normal file
@ -0,0 +1,53 @@
|
||||
// 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 'package:file/file.dart';
|
||||
import 'package:file/local.dart';
|
||||
import 'package:path/path.dart' as path;
|
||||
|
||||
// Represents the locations of all of the data for snippets.
|
||||
class SnippetConfiguration {
|
||||
const SnippetConfiguration({
|
||||
required this.configDirectory,
|
||||
required this.skeletonsDirectory,
|
||||
this.filesystem = const LocalFileSystem(),
|
||||
});
|
||||
|
||||
final FileSystem filesystem;
|
||||
|
||||
/// This is the configuration directory for the snippets system, containing
|
||||
/// the skeletons and templates.
|
||||
final Directory configDirectory;
|
||||
|
||||
/// The directory containing the HTML skeletons to be filled out with metadata
|
||||
/// and returned to dartdoc for insertion in the output.
|
||||
final Directory skeletonsDirectory;
|
||||
|
||||
/// Gets the skeleton file to use for the given [SampleType] and DartPad
|
||||
/// preference.
|
||||
File getHtmlSkeletonFile(String type) {
|
||||
final String filename =
|
||||
type == 'dartpad' ? 'dartpad-sample.html' : '$type.html';
|
||||
return filesystem.file(path.join(skeletonsDirectory.path, filename));
|
||||
}
|
||||
}
|
||||
|
||||
/// A class to compute the configuration of the snippets input and output
|
||||
/// locations based in the current location of the snippets main.dart.
|
||||
class FlutterRepoSnippetConfiguration extends SnippetConfiguration {
|
||||
FlutterRepoSnippetConfiguration({required this.flutterRoot, super.filesystem})
|
||||
: super(
|
||||
configDirectory: _underRoot(filesystem, flutterRoot,
|
||||
const <String>['dev', 'snippets', 'config']),
|
||||
skeletonsDirectory: _underRoot(filesystem, flutterRoot,
|
||||
const <String>['dev', 'snippets', 'config', 'skeletons']),
|
||||
);
|
||||
|
||||
final Directory flutterRoot;
|
||||
|
||||
static Directory _underRoot(
|
||||
FileSystem fs, Directory flutterRoot, List<String> dirs) =>
|
||||
fs.directory(path.canonicalize(
|
||||
path.joinAll(<String>[flutterRoot.absolute.path, ...dirs])));
|
||||
}
|
567
dev/snippets/lib/src/data_types.dart
Normal file
567
dev/snippets/lib/src/data_types.dart
Normal file
@ -0,0 +1,567 @@
|
||||
// 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 'package:args/args.dart';
|
||||
import 'package:file/file.dart';
|
||||
|
||||
import 'util.dart';
|
||||
|
||||
/// A class to represent a line of input code, with associated line number, file
|
||||
/// and element name.
|
||||
class SourceLine {
|
||||
const SourceLine(
|
||||
this.text, {
|
||||
this.file,
|
||||
this.element,
|
||||
this.line = -1,
|
||||
this.startChar = -1,
|
||||
this.endChar = -1,
|
||||
this.indent = 0,
|
||||
});
|
||||
final File? file;
|
||||
final String? element;
|
||||
final int line;
|
||||
final int startChar;
|
||||
final int endChar;
|
||||
final int indent;
|
||||
final String text;
|
||||
|
||||
String toStringWithColumn(int column) =>
|
||||
'$file:$line:${column + indent}: $text';
|
||||
|
||||
SourceLine copyWith({
|
||||
String? element,
|
||||
String? text,
|
||||
File? file,
|
||||
int? line,
|
||||
int? startChar,
|
||||
int? endChar,
|
||||
int? indent,
|
||||
}) {
|
||||
return SourceLine(
|
||||
text ?? this.text,
|
||||
element: element ?? this.element,
|
||||
file: file ?? this.file,
|
||||
line: line ?? this.line,
|
||||
startChar: startChar ?? this.startChar,
|
||||
endChar: endChar ?? this.endChar,
|
||||
indent: indent ?? this.indent,
|
||||
);
|
||||
}
|
||||
|
||||
bool get hasFile => file != null;
|
||||
|
||||
@override
|
||||
String toString() => '$file:${line == -1 ? '??' : line}: $text';
|
||||
}
|
||||
|
||||
/// A class containing the name and contents associated with a code block inside of a
|
||||
/// code sample, for named injection into a template.
|
||||
class SkeletonInjection {
|
||||
SkeletonInjection(this.name, this.contents, {this.language = ''});
|
||||
final String name;
|
||||
final List<SourceLine> contents;
|
||||
final String language;
|
||||
Iterable<String> get stringContents =>
|
||||
contents.map<String>((SourceLine line) => line.text.trimRight());
|
||||
String get mergedContent => stringContents.join('\n');
|
||||
}
|
||||
|
||||
/// A base class to represent a block of any kind of sample code, marked by
|
||||
/// "{@tool (snippet|sample|dartdoc) ...}...{@end-tool}".
|
||||
abstract class CodeSample {
|
||||
CodeSample(
|
||||
this.args,
|
||||
this.input, {
|
||||
required this.index,
|
||||
required SourceLine lineProto,
|
||||
}) : assert(args.isNotEmpty),
|
||||
_lineProto = lineProto,
|
||||
sourceFile = null;
|
||||
|
||||
CodeSample.fromFile(
|
||||
this.args,
|
||||
this.input,
|
||||
this.sourceFile, {
|
||||
required this.index,
|
||||
required SourceLine lineProto,
|
||||
}) : assert(args.isNotEmpty),
|
||||
_lineProto = lineProto;
|
||||
|
||||
final File? sourceFile;
|
||||
final List<String> args;
|
||||
final List<SourceLine> input;
|
||||
final SourceLine _lineProto;
|
||||
String? _sourceFileContents;
|
||||
String get sourceFileContents {
|
||||
if (sourceFile != null && _sourceFileContents == null) {
|
||||
// Strip lines until the first non-comment line. This gets rid of the
|
||||
// copyright and comment directing the reader to the original source file.
|
||||
final List<String> stripped = <String>[];
|
||||
bool doneStrippingHeaders = false;
|
||||
try {
|
||||
for (final String line in sourceFile!.readAsLinesSync()) {
|
||||
if (!doneStrippingHeaders &&
|
||||
RegExp(r'^\s*(\/\/.*)?$').hasMatch(line)) {
|
||||
continue;
|
||||
}
|
||||
// Stop skipping lines after the first line that isn't stripped.
|
||||
doneStrippingHeaders = true;
|
||||
stripped.add(line);
|
||||
}
|
||||
} on FileSystemException catch (e) {
|
||||
throw SnippetException(
|
||||
'Unable to read linked source file ${sourceFile!}: $e',
|
||||
file: _lineProto.file?.absolute.path,
|
||||
);
|
||||
}
|
||||
// Remove any section markers
|
||||
final RegExp sectionMarkerRegExp = RegExp(
|
||||
r'(\/\/\*\*+\n)?\/\/\* [▼▲]+.*$(\n\/\/\*\*+)?\n\n?',
|
||||
multiLine: true,
|
||||
);
|
||||
_sourceFileContents =
|
||||
stripped.join('\n').replaceAll(sectionMarkerRegExp, '');
|
||||
}
|
||||
return _sourceFileContents ?? '';
|
||||
}
|
||||
|
||||
Iterable<String> get inputStrings =>
|
||||
input.map<String>((SourceLine line) => line.text);
|
||||
String get inputAsString => inputStrings.join('\n');
|
||||
|
||||
/// The index of this sample within the dartdoc comment it came from.
|
||||
final int index;
|
||||
String description = '';
|
||||
String get element => start.element ?? '';
|
||||
String output = '';
|
||||
Map<String, Object?> metadata = <String, Object?>{};
|
||||
List<SkeletonInjection> parts = <SkeletonInjection>[];
|
||||
SourceLine get start => input.isEmpty ? _lineProto : input.first;
|
||||
|
||||
String get template {
|
||||
final ArgParser parser = ArgParser();
|
||||
parser.addOption('template', defaultsTo: '');
|
||||
final ArgResults parsedArgs = parser.parse(args);
|
||||
return parsedArgs['template']! as String;
|
||||
}
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
final StringBuffer buf = StringBuffer('${args.join(' ')}:\n');
|
||||
for (final SourceLine line in input) {
|
||||
buf.writeln(
|
||||
'${(line.line == -1 ? '??' : line.line).toString().padLeft(4)}: ${line.text} ',
|
||||
);
|
||||
}
|
||||
return buf.toString();
|
||||
}
|
||||
|
||||
String get type;
|
||||
}
|
||||
|
||||
/// A class to represent a snippet of sample code, marked by "{@tool
|
||||
/// snippet}...{@end-tool}".
|
||||
///
|
||||
/// Snippets are code that is not meant to be run as a complete application, but
|
||||
/// rather as a code usage example.
|
||||
class SnippetSample extends CodeSample {
|
||||
SnippetSample(
|
||||
List<SourceLine> input, {
|
||||
required int index,
|
||||
required SourceLine lineProto,
|
||||
}) : assumptions = <SourceLine>[],
|
||||
super(
|
||||
<String>['snippet'],
|
||||
input,
|
||||
index: index,
|
||||
lineProto: lineProto,
|
||||
);
|
||||
|
||||
factory SnippetSample.combine(
|
||||
List<SnippetSample> sections, {
|
||||
required int index,
|
||||
required SourceLine lineProto,
|
||||
}) {
|
||||
final List<SourceLine> code =
|
||||
sections.expand((SnippetSample section) => section.input).toList();
|
||||
return SnippetSample(code, index: index, lineProto: lineProto);
|
||||
}
|
||||
|
||||
factory SnippetSample.fromStrings(SourceLine firstLine, List<String> code,
|
||||
{required int index}) {
|
||||
final List<SourceLine> codeLines = <SourceLine>[];
|
||||
int startPos = firstLine.startChar;
|
||||
for (int i = 0; i < code.length; ++i) {
|
||||
codeLines.add(
|
||||
firstLine.copyWith(
|
||||
text: code[i],
|
||||
line: firstLine.line + i,
|
||||
startChar: startPos,
|
||||
),
|
||||
);
|
||||
startPos += code[i].length + 1;
|
||||
}
|
||||
return SnippetSample(
|
||||
codeLines,
|
||||
index: index,
|
||||
lineProto: firstLine,
|
||||
);
|
||||
}
|
||||
|
||||
factory SnippetSample.surround(
|
||||
String prefix,
|
||||
List<SourceLine> code,
|
||||
String postfix, {
|
||||
required int index,
|
||||
}) {
|
||||
return SnippetSample(
|
||||
<SourceLine>[
|
||||
if (prefix.isNotEmpty) SourceLine(prefix),
|
||||
...code,
|
||||
if (postfix.isNotEmpty) SourceLine(postfix),
|
||||
],
|
||||
index: index,
|
||||
lineProto: code.first,
|
||||
);
|
||||
}
|
||||
|
||||
List<SourceLine> assumptions;
|
||||
|
||||
@override
|
||||
String get template => '';
|
||||
|
||||
@override
|
||||
SourceLine get start =>
|
||||
input.firstWhere((SourceLine line) => line.file != null);
|
||||
|
||||
@override
|
||||
String get type => 'snippet';
|
||||
}
|
||||
|
||||
/// A class to represent a plain application sample in the dartdoc comments,
|
||||
/// marked by `{@tool sample ...}...{@end-tool}`.
|
||||
///
|
||||
/// Application samples are processed separately from [SnippetSample]s, because
|
||||
/// they must be injected into templates in order to be analyzed. Each
|
||||
/// [ApplicationSample] represents one `{@tool sample ...}...{@end-tool}` block
|
||||
/// in the source file.
|
||||
class ApplicationSample extends CodeSample {
|
||||
ApplicationSample({
|
||||
List<SourceLine> input = const <SourceLine>[],
|
||||
required List<String> args,
|
||||
required int index,
|
||||
required SourceLine lineProto,
|
||||
}) : assert(args.isNotEmpty),
|
||||
super(args, input, index: index, lineProto: lineProto);
|
||||
|
||||
ApplicationSample.fromFile({
|
||||
List<SourceLine> input = const <SourceLine>[],
|
||||
required List<String> args,
|
||||
required File sourceFile,
|
||||
required int index,
|
||||
required SourceLine lineProto,
|
||||
}) : assert(args.isNotEmpty),
|
||||
super.fromFile(args, input, sourceFile,
|
||||
index: index, lineProto: lineProto);
|
||||
|
||||
@override
|
||||
String get type => 'sample';
|
||||
}
|
||||
|
||||
/// A class to represent a Dartpad application sample in the dartdoc comments,
|
||||
/// marked by `{@tool dartpad ...}...{@end-tool}`.
|
||||
///
|
||||
/// Dartpad samples are processed separately from [SnippetSample]s, because they
|
||||
/// must be injected into templates in order to be analyzed. Each
|
||||
/// [DartpadSample] represents one `{@tool dartpad ...}...{@end-tool}` block in
|
||||
/// the source file.
|
||||
class DartpadSample extends ApplicationSample {
|
||||
DartpadSample({
|
||||
super.input,
|
||||
required super.args,
|
||||
required super.index,
|
||||
required super.lineProto,
|
||||
}) : assert(args.isNotEmpty);
|
||||
|
||||
DartpadSample.fromFile({
|
||||
super.input,
|
||||
required super.args,
|
||||
required super.sourceFile,
|
||||
required super.index,
|
||||
required super.lineProto,
|
||||
}) : assert(args.isNotEmpty),
|
||||
super.fromFile();
|
||||
|
||||
@override
|
||||
String get type => 'dartpad';
|
||||
}
|
||||
|
||||
/// The different types of Dart [SourceElement]s that can be found in a source file.
|
||||
enum SourceElementType {
|
||||
/// A class
|
||||
classType,
|
||||
|
||||
/// A field variable of a class.
|
||||
fieldType,
|
||||
|
||||
/// A constructor for a class.
|
||||
constructorType,
|
||||
|
||||
/// A method of a class.
|
||||
methodType,
|
||||
|
||||
/// A function typedef
|
||||
typedefType,
|
||||
|
||||
/// A top level (non-class) variable.
|
||||
topLevelVariableType,
|
||||
|
||||
/// A function, either top level, or embedded in another function.
|
||||
functionType,
|
||||
|
||||
/// An unknown type used for initialization.
|
||||
unknownType,
|
||||
}
|
||||
|
||||
/// Converts the enun type [SourceElementType] to a human readable string.
|
||||
String sourceElementTypeAsString(SourceElementType type) {
|
||||
switch (type) {
|
||||
case SourceElementType.classType:
|
||||
return 'class';
|
||||
case SourceElementType.fieldType:
|
||||
return 'field';
|
||||
case SourceElementType.methodType:
|
||||
return 'method';
|
||||
case SourceElementType.constructorType:
|
||||
return 'constructor';
|
||||
case SourceElementType.typedefType:
|
||||
return 'typedef';
|
||||
case SourceElementType.topLevelVariableType:
|
||||
return 'variable';
|
||||
case SourceElementType.functionType:
|
||||
return 'function';
|
||||
case SourceElementType.unknownType:
|
||||
return 'unknown';
|
||||
}
|
||||
}
|
||||
|
||||
/// A class that represents a Dart element in a source file.
|
||||
///
|
||||
/// The element is one of the types in [SourceElementType].
|
||||
class SourceElement {
|
||||
/// A factory constructor for SourceElements.
|
||||
///
|
||||
/// This uses a factory so that the default for the `comment` and `samples`
|
||||
/// lists can be modifiable lists.
|
||||
factory SourceElement(
|
||||
SourceElementType type,
|
||||
String name,
|
||||
int startPos, {
|
||||
required File file,
|
||||
String className = '',
|
||||
List<SourceLine>? comment,
|
||||
int startLine = -1,
|
||||
List<CodeSample>? samples,
|
||||
bool override = false,
|
||||
}) {
|
||||
comment ??= <SourceLine>[];
|
||||
samples ??= <CodeSample>[];
|
||||
final List<String> commentLines =
|
||||
comment.map<String>((SourceLine line) => line.text).toList();
|
||||
final String commentString = commentLines.join('\n');
|
||||
return SourceElement._(
|
||||
type,
|
||||
name,
|
||||
startPos,
|
||||
file: file,
|
||||
className: className,
|
||||
comment: comment,
|
||||
startLine: startLine,
|
||||
samples: samples,
|
||||
override: override,
|
||||
commentString: commentString,
|
||||
commentStringWithoutTools: _getCommentStringWithoutTools(commentString),
|
||||
commentStringWithoutCode: _getCommentStringWithoutCode(commentString),
|
||||
commentLines: commentLines,
|
||||
);
|
||||
}
|
||||
|
||||
const SourceElement._(
|
||||
this.type,
|
||||
this.name,
|
||||
this.startPos, {
|
||||
required this.file,
|
||||
this.className = '',
|
||||
this.comment = const <SourceLine>[],
|
||||
this.startLine = -1,
|
||||
this.samples = const <CodeSample>[],
|
||||
this.override = false,
|
||||
String commentString = '',
|
||||
String commentStringWithoutTools = '',
|
||||
String commentStringWithoutCode = '',
|
||||
List<String> commentLines = const <String>[],
|
||||
}) : _commentString = commentString,
|
||||
_commentStringWithoutTools = commentStringWithoutTools,
|
||||
_commentStringWithoutCode = commentStringWithoutCode,
|
||||
_commentLines = commentLines;
|
||||
|
||||
final String _commentString;
|
||||
final String _commentStringWithoutTools;
|
||||
final String _commentStringWithoutCode;
|
||||
final List<String> _commentLines;
|
||||
|
||||
// Does not include the description of the sample code, just the text outside
|
||||
// of any dartdoc tools.
|
||||
static String _getCommentStringWithoutTools(String string) {
|
||||
return string.replaceAll(
|
||||
RegExp(r'(\{@tool ([^}]*)\}.*?\{@end-tool\}|/// ?)', dotAll: true), '');
|
||||
}
|
||||
|
||||
// Includes the description text inside of an "@tool"-based sample, but not
|
||||
// the code itself, or any dartdoc tags.
|
||||
static String _getCommentStringWithoutCode(String string) {
|
||||
return string.replaceAll(
|
||||
RegExp(r'([`]{3}.*?[`]{3}|\{@\w+[^}]*\}|/// ?)', dotAll: true), '');
|
||||
}
|
||||
|
||||
/// The type of the element
|
||||
final SourceElementType type;
|
||||
|
||||
/// The name of the element.
|
||||
///
|
||||
/// For example, a method called "doSomething" that is part of the class
|
||||
/// "MyClass" would have "doSomething" as its name.
|
||||
final String name;
|
||||
|
||||
/// The name of the class the element belongs to, if any.
|
||||
///
|
||||
/// This is the empty string if it isn't part of a class.
|
||||
///
|
||||
/// For example, a method called "doSomething" that is part of the class
|
||||
/// "MyClass" would have "MyClass" as its `className`.
|
||||
final String className;
|
||||
|
||||
/// Whether or not this element has the "@override" annotation attached to it.
|
||||
final bool override;
|
||||
|
||||
/// The file that this [SourceElement] was parsed from.
|
||||
final File file;
|
||||
|
||||
/// The character position in the file that this [SourceElement] starts at.
|
||||
final int startPos;
|
||||
|
||||
/// The line in the file that the first position of [SourceElement] is on.
|
||||
final int startLine;
|
||||
|
||||
/// The list of [SourceLine]s that make up the documentation comment for this
|
||||
/// [SourceElement].
|
||||
final List<SourceLine> comment;
|
||||
|
||||
/// The list of [CodeSample]s that are in the documentation comment for this
|
||||
/// [SourceElement].
|
||||
///
|
||||
/// This field will be populated by calling [replaceSamples].
|
||||
final List<CodeSample> samples;
|
||||
|
||||
/// Get the comments as an iterable of lines.
|
||||
Iterable<String> get commentLines => _commentLines;
|
||||
|
||||
/// Get the comments as a single string.
|
||||
String get commentString => _commentString;
|
||||
|
||||
/// Does not include the description of the sample code, just the text outside of any dartdoc tools.
|
||||
String get commentStringWithoutTools => _commentStringWithoutTools;
|
||||
|
||||
/// Includes the description text inside of an "@tool"-based sample, but not
|
||||
/// the code itself, or any dartdoc tags.
|
||||
String get commentStringWithoutCode => _commentStringWithoutCode;
|
||||
|
||||
/// The number of samples in the dartdoc comment for this element.
|
||||
int get sampleCount => samples.length;
|
||||
|
||||
/// The number of [DartpadSample]s in the dartdoc comment for this element.
|
||||
int get dartpadSampleCount => samples.whereType<DartpadSample>().length;
|
||||
|
||||
/// The number of [ApplicationSample]s in the dartdoc comment for this element.
|
||||
int get applicationSampleCount => samples.where((CodeSample sample) {
|
||||
return sample is ApplicationSample && sample is! DartpadSample;
|
||||
}).length;
|
||||
|
||||
/// The number of [SnippetSample]s in the dartdoc comment for this element.
|
||||
int get snippetCount => samples.whereType<SnippetSample>().length;
|
||||
|
||||
/// Count of comment lines, not including lines of code in the comment.
|
||||
int get lineCount => commentStringWithoutCode.split('\n').length;
|
||||
|
||||
/// Count of comment words, not including words in any code in the comment.
|
||||
int get wordCount {
|
||||
return commentStringWithoutCode.split(RegExp(r'\s+')).length;
|
||||
}
|
||||
|
||||
/// Count of comment characters, not including any code samples in the
|
||||
/// comment, after collapsing each run of whitespace to a single space.
|
||||
int get charCount =>
|
||||
commentStringWithoutCode.replaceAll(RegExp(r'\s+'), ' ').length;
|
||||
|
||||
/// Whether or not this element's documentation has a "See also:" section in it.
|
||||
bool get hasSeeAlso => commentStringWithoutTools.contains('See also:');
|
||||
|
||||
int get referenceCount {
|
||||
final RegExp regex = RegExp(r'\[[. \w]*\](?!\(.*\))');
|
||||
return regex.allMatches(commentStringWithoutCode).length;
|
||||
}
|
||||
|
||||
int get linkCount {
|
||||
final RegExp regex = RegExp(r'\[[. \w]*\]\(.*\)');
|
||||
return regex.allMatches(commentStringWithoutCode).length;
|
||||
}
|
||||
|
||||
/// Returns the fully qualified name of this element.
|
||||
///
|
||||
/// For example, a method called "doSomething" that is part of the class
|
||||
/// "MyClass" would have "MyClass.doSomething" as its `elementName`.
|
||||
String get elementName {
|
||||
if (type == SourceElementType.constructorType) {
|
||||
// Constructors already have the name of the class in them.
|
||||
return name;
|
||||
}
|
||||
return className.isEmpty ? name : '$className.$name';
|
||||
}
|
||||
|
||||
/// Returns the type of this element as a [String].
|
||||
String get typeAsString {
|
||||
return '${override ? 'overridden ' : ''}${sourceElementTypeAsString(type)}';
|
||||
}
|
||||
|
||||
void replaceSamples(Iterable<CodeSample> samples) {
|
||||
this.samples.clear();
|
||||
this.samples.addAll(samples);
|
||||
}
|
||||
|
||||
/// Copy the source element, with some attributes optionally replaced.
|
||||
SourceElement copyWith({
|
||||
SourceElementType? type,
|
||||
String? name,
|
||||
int? startPos,
|
||||
File? file,
|
||||
String? className,
|
||||
List<SourceLine>? comment,
|
||||
int? startLine,
|
||||
List<CodeSample>? samples,
|
||||
bool? override,
|
||||
}) {
|
||||
return SourceElement(
|
||||
type ?? this.type,
|
||||
name ?? this.name,
|
||||
startPos ?? this.startPos,
|
||||
file: file ?? this.file,
|
||||
className: className ?? this.className,
|
||||
comment: comment ?? this.comment,
|
||||
startLine: startLine ?? this.startLine,
|
||||
samples: samples ?? this.samples,
|
||||
override: override ?? this.override,
|
||||
);
|
||||
}
|
||||
}
|
426
dev/snippets/lib/src/import_sorter.dart
Normal file
426
dev/snippets/lib/src/import_sorter.dart
Normal file
@ -0,0 +1,426 @@
|
||||
// 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:math';
|
||||
|
||||
import 'package:analyzer/dart/analysis/features.dart';
|
||||
import 'package:analyzer/dart/analysis/results.dart';
|
||||
import 'package:analyzer/dart/analysis/utilities.dart';
|
||||
import 'package:analyzer/dart/ast/ast.dart';
|
||||
import 'package:analyzer/dart/ast/token.dart';
|
||||
import 'package:analyzer/error/error.dart';
|
||||
import 'package:analyzer/source/line_info.dart';
|
||||
import 'package:meta/meta.dart';
|
||||
|
||||
import 'util.dart';
|
||||
|
||||
/// Read the given source code, and return the new contents after sorting the
|
||||
/// imports.
|
||||
String sortImports(String contents) {
|
||||
final ParseStringResult parseResult = parseString(
|
||||
content: contents,
|
||||
featureSet: FeatureSet.fromEnableFlags2(
|
||||
sdkLanguageVersion: FlutterInformation.instance.getDartSdkVersion(),
|
||||
flags: <String>[],
|
||||
),
|
||||
);
|
||||
final List<AnalysisError> errors = <AnalysisError>[];
|
||||
final _ImportOrganizer organizer =
|
||||
_ImportOrganizer(contents, parseResult.unit, errors);
|
||||
final List<_SourceEdit> edits = organizer.organize();
|
||||
// Sort edits in reverse order
|
||||
edits.sort((_SourceEdit a, _SourceEdit b) {
|
||||
return b.offset.compareTo(a.offset);
|
||||
});
|
||||
// Apply edits
|
||||
for (final _SourceEdit edit in edits) {
|
||||
contents = contents.replaceRange(edit.offset, edit.end, edit.replacement);
|
||||
}
|
||||
return contents;
|
||||
}
|
||||
|
||||
/// Organizer of imports (and other directives) in the [unit].
|
||||
// Adapted from the analysis_server package.
|
||||
// This code is largely copied from:
|
||||
// https://github.com/dart-lang/sdk/blob/c7405b9d86b4b47cf7610667491f1db72723b0dd/pkg/analysis_server/lib/src/services/correction/organize_imports.dart#L15
|
||||
// TODO(gspencergoog): If ImportOrganizer ever becomes part of the public API,
|
||||
// this class should probably be replaced.
|
||||
// https://github.com/flutter/flutter/issues/86197
|
||||
class _ImportOrganizer {
|
||||
_ImportOrganizer(this.initialCode, this.unit, this.errors)
|
||||
: code = initialCode {
|
||||
endOfLine = getEOL(code);
|
||||
hasUnresolvedIdentifierError = errors.any((AnalysisError error) {
|
||||
return error.errorCode.isUnresolvedIdentifier;
|
||||
});
|
||||
}
|
||||
|
||||
final String initialCode;
|
||||
|
||||
final CompilationUnit unit;
|
||||
|
||||
final List<AnalysisError> errors;
|
||||
|
||||
String code;
|
||||
|
||||
String endOfLine = '\n';
|
||||
|
||||
bool hasUnresolvedIdentifierError = false;
|
||||
|
||||
/// Returns the number of characters common to the end of [a] and [b].
|
||||
int findCommonSuffix(String a, String b) {
|
||||
final int aLength = a.length;
|
||||
final int bLength = b.length;
|
||||
final int n = min(aLength, bLength);
|
||||
for (int i = 1; i <= n; i++) {
|
||||
if (a.codeUnitAt(aLength - i) != b.codeUnitAt(bLength - i)) {
|
||||
return i - 1;
|
||||
}
|
||||
}
|
||||
return n;
|
||||
}
|
||||
|
||||
/// Return the [_SourceEdit]s that organize imports in the [unit].
|
||||
List<_SourceEdit> organize() {
|
||||
_organizeDirectives();
|
||||
// prepare edits
|
||||
final List<_SourceEdit> edits = <_SourceEdit>[];
|
||||
if (code != initialCode) {
|
||||
final int suffixLength = findCommonSuffix(initialCode, code);
|
||||
final _SourceEdit edit = _SourceEdit(0, initialCode.length - suffixLength,
|
||||
code.substring(0, code.length - suffixLength));
|
||||
edits.add(edit);
|
||||
}
|
||||
return edits;
|
||||
}
|
||||
|
||||
/// Organize all [Directive]s.
|
||||
void _organizeDirectives() {
|
||||
final LineInfo lineInfo = unit.lineInfo;
|
||||
bool hasLibraryDirective = false;
|
||||
final List<_DirectiveInfo> directives = <_DirectiveInfo>[];
|
||||
for (final Directive directive in unit.directives) {
|
||||
if (directive is LibraryDirective) {
|
||||
hasLibraryDirective = true;
|
||||
}
|
||||
if (directive is UriBasedDirective) {
|
||||
final _DirectivePriority? priority = getDirectivePriority(directive);
|
||||
if (priority != null) {
|
||||
int offset = directive.offset;
|
||||
int end = directive.end;
|
||||
|
||||
final Token? leadingComment =
|
||||
getLeadingComment(unit, directive, lineInfo);
|
||||
final Token? trailingComment =
|
||||
getTrailingComment(unit, directive, lineInfo, end);
|
||||
|
||||
String? leadingCommentText;
|
||||
if (leadingComment != null) {
|
||||
leadingCommentText =
|
||||
code.substring(leadingComment.offset, directive.offset);
|
||||
offset = leadingComment.offset;
|
||||
}
|
||||
String? trailingCommentText;
|
||||
if (trailingComment != null) {
|
||||
trailingCommentText =
|
||||
code.substring(directive.end, trailingComment.end);
|
||||
end = trailingComment.end;
|
||||
}
|
||||
String? documentationText;
|
||||
final Comment? documentationComment = directive.documentationComment;
|
||||
if (documentationComment != null) {
|
||||
documentationText = code.substring(
|
||||
documentationComment.offset, documentationComment.end);
|
||||
}
|
||||
String? annotationText;
|
||||
final Token? beginToken = directive.metadata.beginToken;
|
||||
final Token? endToken = directive.metadata.endToken;
|
||||
if (beginToken != null && endToken != null) {
|
||||
annotationText = code.substring(beginToken.offset, endToken.end);
|
||||
}
|
||||
final String text = code.substring(
|
||||
directive.firstTokenAfterCommentAndMetadata.offset,
|
||||
directive.end);
|
||||
final String uriContent = directive.uri.stringValue ?? '';
|
||||
directives.add(
|
||||
_DirectiveInfo(
|
||||
directive,
|
||||
priority,
|
||||
leadingCommentText,
|
||||
documentationText,
|
||||
annotationText,
|
||||
uriContent,
|
||||
trailingCommentText,
|
||||
offset,
|
||||
end,
|
||||
text,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
// nothing to do
|
||||
if (directives.isEmpty) {
|
||||
return;
|
||||
}
|
||||
final int firstDirectiveOffset = directives.first.offset;
|
||||
final int lastDirectiveEnd = directives.last.end;
|
||||
|
||||
// Without a library directive, the library comment is the comment of the
|
||||
// first directive.
|
||||
_DirectiveInfo? libraryDocumentationDirective;
|
||||
if (!hasLibraryDirective && directives.isNotEmpty) {
|
||||
libraryDocumentationDirective = directives.first;
|
||||
}
|
||||
|
||||
// sort
|
||||
directives.sort();
|
||||
// append directives with grouping
|
||||
String directivesCode;
|
||||
{
|
||||
final StringBuffer sb = StringBuffer();
|
||||
if (libraryDocumentationDirective != null &&
|
||||
libraryDocumentationDirective.documentationText != null) {
|
||||
sb.write(libraryDocumentationDirective.documentationText);
|
||||
sb.write(endOfLine);
|
||||
}
|
||||
_DirectivePriority currentPriority = directives.first.priority;
|
||||
for (final _DirectiveInfo directiveInfo in directives) {
|
||||
if (currentPriority != directiveInfo.priority) {
|
||||
sb.write(endOfLine);
|
||||
currentPriority = directiveInfo.priority;
|
||||
}
|
||||
if (directiveInfo.leadingCommentText != null) {
|
||||
sb.write(directiveInfo.leadingCommentText);
|
||||
}
|
||||
if (directiveInfo != libraryDocumentationDirective &&
|
||||
directiveInfo.documentationText != null) {
|
||||
sb.write(directiveInfo.documentationText);
|
||||
sb.write(endOfLine);
|
||||
}
|
||||
if (directiveInfo.annotationText != null) {
|
||||
sb.write(directiveInfo.annotationText);
|
||||
sb.write(endOfLine);
|
||||
}
|
||||
sb.write(directiveInfo.text);
|
||||
if (directiveInfo.trailingCommentText != null) {
|
||||
sb.write(directiveInfo.trailingCommentText);
|
||||
}
|
||||
sb.write(endOfLine);
|
||||
}
|
||||
directivesCode = sb.toString();
|
||||
directivesCode = directivesCode.trimRight();
|
||||
}
|
||||
// prepare code
|
||||
final String beforeDirectives = code.substring(0, firstDirectiveOffset);
|
||||
final String afterDirectives = code.substring(lastDirectiveEnd);
|
||||
code = beforeDirectives + directivesCode + afterDirectives;
|
||||
}
|
||||
|
||||
static _DirectivePriority? getDirectivePriority(UriBasedDirective directive) {
|
||||
final String uriContent = directive.uri.stringValue ?? '';
|
||||
if (directive is ImportDirective) {
|
||||
if (uriContent.startsWith('dart:')) {
|
||||
return _DirectivePriority.IMPORT_SDK;
|
||||
} else if (uriContent.startsWith('package:')) {
|
||||
return _DirectivePriority.IMPORT_PKG;
|
||||
} else if (uriContent.contains('://')) {
|
||||
return _DirectivePriority.IMPORT_OTHER;
|
||||
} else {
|
||||
return _DirectivePriority.IMPORT_REL;
|
||||
}
|
||||
}
|
||||
if (directive is ExportDirective) {
|
||||
if (uriContent.startsWith('dart:')) {
|
||||
return _DirectivePriority.EXPORT_SDK;
|
||||
} else if (uriContent.startsWith('package:')) {
|
||||
return _DirectivePriority.EXPORT_PKG;
|
||||
} else if (uriContent.contains('://')) {
|
||||
return _DirectivePriority.EXPORT_OTHER;
|
||||
} else {
|
||||
return _DirectivePriority.EXPORT_REL;
|
||||
}
|
||||
}
|
||||
if (directive is PartDirective) {
|
||||
return _DirectivePriority.PART;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/// Return the EOL to use for [code].
|
||||
static String getEOL(String code) {
|
||||
if (code.contains('\r\n')) {
|
||||
return '\r\n';
|
||||
} else {
|
||||
return '\n';
|
||||
}
|
||||
}
|
||||
|
||||
/// Gets the first comment token considered to be the leading comment for this
|
||||
/// directive.
|
||||
///
|
||||
/// Leading comments for the first directive in a file are considered library
|
||||
/// comments and not returned unless they contain blank lines, in which case
|
||||
/// only the last part of the comment will be returned.
|
||||
static Token? getLeadingComment(
|
||||
CompilationUnit unit, UriBasedDirective directive, LineInfo lineInfo) {
|
||||
if (directive.beginToken.precedingComments == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
Token? firstComment = directive.beginToken.precedingComments;
|
||||
Token? comment = firstComment;
|
||||
Token? nextComment = comment?.next;
|
||||
// Don't connect comments that have a blank line between them
|
||||
while (comment != null && nextComment != null) {
|
||||
final int currentLine = lineInfo.getLocation(comment.offset).lineNumber;
|
||||
final int nextLine = lineInfo.getLocation(nextComment.offset).lineNumber;
|
||||
if (nextLine - currentLine > 1) {
|
||||
firstComment = nextComment;
|
||||
}
|
||||
comment = nextComment;
|
||||
nextComment = comment.next;
|
||||
}
|
||||
|
||||
// Check if the comment is the first comment in the document
|
||||
if (firstComment != unit.beginToken.precedingComments) {
|
||||
final int previousDirectiveLine =
|
||||
lineInfo.getLocation(directive.beginToken.previous!.end).lineNumber;
|
||||
|
||||
// Skip over any comments on the same line as the previous directive
|
||||
// as they will be attached to the end of it.
|
||||
Token? comment = firstComment;
|
||||
while (comment != null &&
|
||||
previousDirectiveLine ==
|
||||
lineInfo.getLocation(comment.offset).lineNumber) {
|
||||
comment = comment.next;
|
||||
}
|
||||
return comment;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/// Gets the last comment token considered to be the trailing comment for this
|
||||
/// directive.
|
||||
///
|
||||
/// To be considered a trailing comment, the comment must be on the same line
|
||||
/// as the directive.
|
||||
static Token? getTrailingComment(CompilationUnit unit,
|
||||
UriBasedDirective directive, LineInfo lineInfo, int end) {
|
||||
final int line = lineInfo.getLocation(end).lineNumber;
|
||||
Token? comment = directive.endToken.next!.precedingComments;
|
||||
while (comment != null) {
|
||||
if (lineInfo.getLocation(comment.offset).lineNumber == line) {
|
||||
return comment;
|
||||
}
|
||||
comment = comment.next;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
class _DirectiveInfo implements Comparable<_DirectiveInfo> {
|
||||
_DirectiveInfo(
|
||||
this.directive,
|
||||
this.priority,
|
||||
this.leadingCommentText,
|
||||
this.documentationText,
|
||||
this.annotationText,
|
||||
this.uri,
|
||||
this.trailingCommentText,
|
||||
this.offset,
|
||||
this.end,
|
||||
this.text,
|
||||
);
|
||||
|
||||
final UriBasedDirective directive;
|
||||
final _DirectivePriority priority;
|
||||
final String? leadingCommentText;
|
||||
final String? documentationText;
|
||||
final String? annotationText;
|
||||
final String uri;
|
||||
final String? trailingCommentText;
|
||||
|
||||
/// The offset of the first token, usually the keyword but may include leading comments.
|
||||
final int offset;
|
||||
|
||||
/// The offset after the last token, including the end-of-line comment.
|
||||
final int end;
|
||||
|
||||
/// The text excluding comments, documentation and annotations.
|
||||
final String text;
|
||||
|
||||
@override
|
||||
int compareTo(_DirectiveInfo other) {
|
||||
if (priority == other.priority) {
|
||||
return _compareUri(uri, other.uri);
|
||||
}
|
||||
return priority.index - other.priority.index;
|
||||
}
|
||||
|
||||
@override
|
||||
String toString() => '(priority=$priority; text=$text)';
|
||||
|
||||
static int _compareUri(String a, String b) {
|
||||
final List<String> aList = _splitUri(a);
|
||||
final List<String> bList = _splitUri(b);
|
||||
int result;
|
||||
if ((result = aList[0].compareTo(bList[0])) != 0) {
|
||||
return result;
|
||||
}
|
||||
if ((result = aList[1].compareTo(bList[1])) != 0) {
|
||||
return result;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
/// Split the given [uri] like `package:some.name/and/path.dart` into a list
|
||||
/// like `[package:some.name, and/path.dart]`.
|
||||
static List<String> _splitUri(String uri) {
|
||||
final int index = uri.indexOf('/');
|
||||
if (index == -1) {
|
||||
return <String>[uri, ''];
|
||||
}
|
||||
return <String>[uri.substring(0, index), uri.substring(index + 1)];
|
||||
}
|
||||
}
|
||||
|
||||
enum _DirectivePriority {
|
||||
IMPORT_SDK,
|
||||
IMPORT_PKG,
|
||||
IMPORT_OTHER,
|
||||
IMPORT_REL,
|
||||
EXPORT_SDK,
|
||||
EXPORT_PKG,
|
||||
EXPORT_OTHER,
|
||||
EXPORT_REL,
|
||||
PART
|
||||
}
|
||||
|
||||
/// SourceEdit
|
||||
///
|
||||
/// {
|
||||
/// "offset": int
|
||||
/// "length": int
|
||||
/// "replacement": String
|
||||
/// "id": optional String
|
||||
/// }
|
||||
///
|
||||
/// Clients may not extend, implement or mix-in this class.
|
||||
@immutable
|
||||
class _SourceEdit {
|
||||
const _SourceEdit(this.offset, this.length, this.replacement);
|
||||
|
||||
/// The offset of the region to be modified.
|
||||
final int offset;
|
||||
|
||||
/// The length of the region to be modified.
|
||||
final int length;
|
||||
|
||||
/// The end of the region to be modified.
|
||||
int get end => offset + length;
|
||||
|
||||
/// The code that is to replace the specified region in the original code.
|
||||
final String replacement;
|
||||
}
|
429
dev/snippets/lib/src/snippet_generator.dart
Normal file
429
dev/snippets/lib/src/snippet_generator.dart
Normal file
@ -0,0 +1,429 @@
|
||||
// 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:convert';
|
||||
import 'dart:io' as io;
|
||||
|
||||
import 'package:dart_style/dart_style.dart';
|
||||
import 'package:file/file.dart';
|
||||
import 'package:file/local.dart';
|
||||
import 'package:path/path.dart' as path;
|
||||
|
||||
import 'configuration.dart';
|
||||
import 'data_types.dart';
|
||||
import 'import_sorter.dart';
|
||||
import 'util.dart';
|
||||
|
||||
/// Generates the snippet HTML, as well as saving the output snippet main to
|
||||
/// the output directory.
|
||||
class SnippetGenerator {
|
||||
SnippetGenerator(
|
||||
{SnippetConfiguration? configuration,
|
||||
FileSystem filesystem = const LocalFileSystem(),
|
||||
Directory? flutterRoot})
|
||||
: flutterRoot =
|
||||
flutterRoot ?? FlutterInformation.instance.getFlutterRoot(),
|
||||
configuration = configuration ??
|
||||
FlutterRepoSnippetConfiguration(
|
||||
filesystem: filesystem,
|
||||
flutterRoot: flutterRoot ??
|
||||
FlutterInformation.instance.getFlutterRoot());
|
||||
|
||||
final Directory flutterRoot;
|
||||
|
||||
/// The configuration used to determine where to get/save data for the
|
||||
/// snippet.
|
||||
final SnippetConfiguration configuration;
|
||||
|
||||
static const JsonEncoder jsonEncoder = JsonEncoder.withIndent(' ');
|
||||
|
||||
/// A Dart formatted used to format the snippet code and finished application
|
||||
/// code.
|
||||
static DartFormatter formatter =
|
||||
DartFormatter(pageWidth: 80, fixes: StyleFix.all);
|
||||
|
||||
/// Interpolates the [injections] into an HTML skeleton file.
|
||||
///
|
||||
/// 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.
|
||||
String interpolateSkeleton(
|
||||
CodeSample sample,
|
||||
String skeleton,
|
||||
) {
|
||||
final List<String> codeParts = <String>[];
|
||||
const HtmlEscape htmlEscape = HtmlEscape();
|
||||
String? language;
|
||||
for (final SkeletonInjection injection in sample.parts) {
|
||||
if (!injection.name.startsWith('code')) {
|
||||
continue;
|
||||
}
|
||||
codeParts.addAll(injection.stringContents);
|
||||
if (injection.language.isNotEmpty) {
|
||||
language = injection.language;
|
||||
}
|
||||
codeParts.addAll(<String>['', '// ...', '']);
|
||||
}
|
||||
if (codeParts.length > 3) {
|
||||
codeParts.removeRange(codeParts.length - 3, codeParts.length);
|
||||
}
|
||||
// Only insert a div for the description if there actually is some text there.
|
||||
// This means that the {{description}} marker in the skeleton needs to
|
||||
// be inside of an {@inject-html} block.
|
||||
final String description = sample.description.trim().isNotEmpty
|
||||
? '<div class="snippet-description">{@end-inject-html}${sample.description.trim()}{@inject-html}</div>'
|
||||
: '';
|
||||
|
||||
// DartPad only supports stable or main as valid channels. Use main
|
||||
// if not on stable so that local runs will work (although they will
|
||||
// still take their sample code from the master docs server).
|
||||
final String channel =
|
||||
sample.metadata['channel'] == 'stable' ? 'stable' : 'main';
|
||||
|
||||
final Map<String, String> substitutions = <String, String>{
|
||||
'description': description,
|
||||
'code': htmlEscape.convert(codeParts.join('\n')),
|
||||
'language': language ?? 'dart',
|
||||
'serial': '',
|
||||
'id': sample.metadata['id']! as String,
|
||||
'channel': channel,
|
||||
'element': sample.metadata['element'] as String? ?? sample.element,
|
||||
'app': '',
|
||||
};
|
||||
if (sample is ApplicationSample) {
|
||||
substitutions
|
||||
..['serial'] = sample.metadata['serial']?.toString() ?? '0'
|
||||
..['app'] = htmlEscape.convert(sample.output);
|
||||
}
|
||||
return skeleton.replaceAllMapped(
|
||||
RegExp('{{(${substitutions.keys.join('|')})}}'), (Match match) {
|
||||
return substitutions[match[1]]!;
|
||||
});
|
||||
}
|
||||
|
||||
/// Consolidates all of the snippets and the assumptions into one snippet, in
|
||||
/// order to create a compilable result.
|
||||
Iterable<SourceLine> consolidateSnippets(List<CodeSample> samples,
|
||||
{bool addMarkers = false}) {
|
||||
if (samples.isEmpty) {
|
||||
return <SourceLine>[];
|
||||
}
|
||||
final Iterable<SnippetSample> snippets = samples.whereType<SnippetSample>();
|
||||
final List<SourceLine> snippetLines = <SourceLine>[
|
||||
...snippets.first.assumptions,
|
||||
];
|
||||
for (final SnippetSample sample in snippets) {
|
||||
parseInput(sample);
|
||||
snippetLines.addAll(_processBlocks(sample));
|
||||
}
|
||||
return snippetLines;
|
||||
}
|
||||
|
||||
/// A RegExp that matches a Dart constructor.
|
||||
static final RegExp _constructorRegExp =
|
||||
RegExp(r'(const\s+)?_*[A-Z][a-zA-Z0-9<>._]*\(');
|
||||
|
||||
/// A serial number so that we can create unique expression names when we
|
||||
/// generate them.
|
||||
int _expressionId = 0;
|
||||
|
||||
List<SourceLine> _surround(
|
||||
String prefix, Iterable<SourceLine> body, String suffix) {
|
||||
return <SourceLine>[
|
||||
if (prefix.isNotEmpty) SourceLine(prefix),
|
||||
...body,
|
||||
if (suffix.isNotEmpty) SourceLine(suffix),
|
||||
];
|
||||
}
|
||||
|
||||
/// Process one block of sample code (the part inside of "```" markers).
|
||||
/// Splits any sections denoted by "// ..." into separate blocks to be
|
||||
/// processed separately. Uses a primitive heuristic to make sample blocks
|
||||
/// into valid Dart code.
|
||||
List<SourceLine> _processBlocks(CodeSample sample) {
|
||||
final List<SourceLine> block = sample.parts
|
||||
.expand<SourceLine>((SkeletonInjection injection) => injection.contents)
|
||||
.toList();
|
||||
if (block.isEmpty) {
|
||||
return <SourceLine>[];
|
||||
}
|
||||
return _processBlock(block);
|
||||
}
|
||||
|
||||
List<SourceLine> _processBlock(List<SourceLine> block) {
|
||||
final String firstLine = block.first.text;
|
||||
if (firstLine.startsWith('new ') ||
|
||||
firstLine.startsWith(_constructorRegExp)) {
|
||||
_expressionId += 1;
|
||||
return _surround('dynamic expression$_expressionId = ', block, ';');
|
||||
} else if (firstLine.startsWith('await ')) {
|
||||
_expressionId += 1;
|
||||
return _surround(
|
||||
'Future<void> expression$_expressionId() async { ', block, ' }');
|
||||
} else if (block.first.text.startsWith('class ') ||
|
||||
block.first.text.startsWith('enum ')) {
|
||||
return block;
|
||||
} else if ((block.first.text.startsWith('_') ||
|
||||
block.first.text.startsWith('final ')) &&
|
||||
block.first.text.contains(' = ')) {
|
||||
_expressionId += 1;
|
||||
return _surround(
|
||||
'void expression$_expressionId() { ', block.toList(), ' }');
|
||||
} else {
|
||||
final List<SourceLine> buffer = <SourceLine>[];
|
||||
int blocks = 0;
|
||||
SourceLine? subLine;
|
||||
final List<SourceLine> subsections = <SourceLine>[];
|
||||
for (int index = 0; index < block.length; index += 1) {
|
||||
// Each section of the dart code that is either split by a blank line, or with
|
||||
// '// ...' is treated as a separate code block.
|
||||
if (block[index].text.trim().isEmpty || block[index].text == '// ...') {
|
||||
if (subLine == null) {
|
||||
continue;
|
||||
}
|
||||
blocks += 1;
|
||||
subsections.addAll(_processBlock(buffer));
|
||||
buffer.clear();
|
||||
assert(buffer.isEmpty);
|
||||
subLine = null;
|
||||
} else if (block[index].text.startsWith('// ')) {
|
||||
if (buffer.length > 1) {
|
||||
// don't include leading comments
|
||||
// so that it doesn't start with "// " and get caught in this again
|
||||
buffer.add(SourceLine('/${block[index].text}'));
|
||||
}
|
||||
} else {
|
||||
subLine ??= block[index];
|
||||
buffer.add(block[index]);
|
||||
}
|
||||
}
|
||||
if (blocks > 0) {
|
||||
if (subLine != null) {
|
||||
subsections.addAll(_processBlock(buffer));
|
||||
}
|
||||
// Combine all of the subsections into one section, now that they've been processed.
|
||||
return subsections;
|
||||
} else {
|
||||
return block;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Parses the input for the various code and description segments, and
|
||||
/// 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<SkeletonInjection> components = <SkeletonInjection>[];
|
||||
String? language;
|
||||
final RegExp codeStartEnd =
|
||||
RegExp(r'^\s*```(?<language>[-\w]+|[-\w]+ (?<section>[-\w]+))?\s*$');
|
||||
for (final SourceLine line in sample.input) {
|
||||
final RegExpMatch? match = codeStartEnd.firstMatch(line.text);
|
||||
if (match != null) {
|
||||
// If we saw the start or end of a code block
|
||||
inCodeBlock = !inCodeBlock;
|
||||
if (match.namedGroup('language') != null) {
|
||||
language = match[1];
|
||||
if (match.namedGroup('section') != null) {
|
||||
components.add(SkeletonInjection(
|
||||
'code-${match.namedGroup('section')}', <SourceLine>[],
|
||||
language: language!));
|
||||
} else {
|
||||
components.add(
|
||||
SkeletonInjection('code', <SourceLine>[], language: language!));
|
||||
}
|
||||
} else {
|
||||
language = null;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
if (!inCodeBlock) {
|
||||
description.add(line);
|
||||
} else {
|
||||
assert(language != null);
|
||||
components.last.contents.add(line);
|
||||
}
|
||||
}
|
||||
final List<String> descriptionLines = <String>[];
|
||||
bool lastWasWhitespace = false;
|
||||
for (final String line in description
|
||||
.map<String>((SourceLine line) => line.text.trimRight())) {
|
||||
final bool onlyWhitespace = line.trim().isEmpty;
|
||||
if (onlyWhitespace && descriptionLines.isEmpty) {
|
||||
// Don't add whitespace lines until we see something without whitespace.
|
||||
lastWasWhitespace = onlyWhitespace;
|
||||
continue;
|
||||
}
|
||||
if (onlyWhitespace && lastWasWhitespace) {
|
||||
// Don't add more than one whitespace line in a row.
|
||||
continue;
|
||||
}
|
||||
descriptionLines.add(line);
|
||||
lastWasWhitespace = onlyWhitespace;
|
||||
}
|
||||
sample.description = descriptionLines.join('\n').trimRight();
|
||||
sample.parts = <SkeletonInjection>[
|
||||
if (sample is SnippetSample)
|
||||
SkeletonInjection('#assumptions', sample.assumptions),
|
||||
...components,
|
||||
];
|
||||
return sample.parts;
|
||||
}
|
||||
|
||||
String _loadFileAsUtf8(File file) {
|
||||
return file.readAsStringSync();
|
||||
}
|
||||
|
||||
/// Generate the HTML using the skeleton file for the type of the given sample.
|
||||
///
|
||||
/// Returns a string with the HTML needed to embed in a web page for showing a
|
||||
/// sample on the web page.
|
||||
String generateHtml(CodeSample sample) {
|
||||
final String skeleton =
|
||||
_loadFileAsUtf8(configuration.getHtmlSkeletonFile(sample.type));
|
||||
return interpolateSkeleton(sample, skeleton);
|
||||
}
|
||||
|
||||
// Sets the description string on the sample and in the sample metadata to a
|
||||
// comment version of the description.
|
||||
// Trims lines of extra whitespace, and strips leading and trailing blank
|
||||
// lines.
|
||||
String _getDescription(CodeSample sample) {
|
||||
return sample.description.splitMapJoin(
|
||||
'\n',
|
||||
onMatch: (Match match) => match.group(0)!,
|
||||
onNonMatch: (String nonmatch) =>
|
||||
nonmatch.trimRight().isEmpty ? '//' : '// ${nonmatch.trimRight()}',
|
||||
);
|
||||
}
|
||||
|
||||
/// The main routine for generating code samples from the source code doc comments.
|
||||
///
|
||||
/// The `sample` is the block of sample code from a dartdoc comment.
|
||||
///
|
||||
/// The optional `output` is the file to write the generated sample code to.
|
||||
///
|
||||
/// If `includeAssumptions` is true, then the block in the "Examples can
|
||||
/// assume:" block will also be included in the output.
|
||||
///
|
||||
/// Returns a string containing the resulting code sample.
|
||||
String generateCode(
|
||||
CodeSample sample, {
|
||||
File? output,
|
||||
String? copyright,
|
||||
String? description,
|
||||
bool formatOutput = true,
|
||||
bool includeAssumptions = false,
|
||||
}) {
|
||||
sample.metadata['copyright'] ??= copyright;
|
||||
final List<SkeletonInjection> snippetData = parseInput(sample);
|
||||
sample.description = description ?? sample.description;
|
||||
sample.metadata['description'] = _getDescription(sample);
|
||||
switch (sample) {
|
||||
case DartpadSample _:
|
||||
case ApplicationSample _:
|
||||
final String app = sample.sourceFileContents;
|
||||
sample.output = app;
|
||||
if (formatOutput) {
|
||||
final DartFormatter formatter =
|
||||
DartFormatter(pageWidth: 80, fixes: StyleFix.all);
|
||||
try {
|
||||
sample.output = formatter.format(sample.output);
|
||||
} on FormatterException catch (exception) {
|
||||
io.stderr
|
||||
.write('Code to format:\n${_addLineNumbers(sample.output)}\n');
|
||||
errorExit('Unable to format sample code: $exception');
|
||||
}
|
||||
sample.output = sortImports(sample.output);
|
||||
}
|
||||
if (output != null) {
|
||||
output.writeAsStringSync(sample.output);
|
||||
|
||||
final File metadataFile = configuration.filesystem.file(path.join(
|
||||
path.dirname(output.path),
|
||||
'${path.basenameWithoutExtension(output.path)}.json'));
|
||||
sample.metadata['file'] = path.basename(output.path);
|
||||
final Map<String, Object?> metadata = sample.metadata;
|
||||
if (metadata.containsKey('description')) {
|
||||
metadata['description'] = (metadata['description']! as String)
|
||||
.replaceAll(RegExp(r'^// ?', multiLine: true), '');
|
||||
}
|
||||
metadataFile.writeAsStringSync(jsonEncoder.convert(metadata));
|
||||
}
|
||||
case SnippetSample _:
|
||||
String app;
|
||||
if (sample.sourceFile == null) {
|
||||
String templateContents;
|
||||
if (includeAssumptions) {
|
||||
templateContents =
|
||||
'${headers.map<String>((SourceLine line) {
|
||||
return line.text;
|
||||
}).join('\n')}\n{{#assumptions}}\n{{description}}\n{{code}}';
|
||||
} else {
|
||||
templateContents = '{{description}}\n{{code}}';
|
||||
}
|
||||
app = interpolateTemplate(
|
||||
snippetData,
|
||||
templateContents,
|
||||
sample.metadata,
|
||||
addCopyright: copyright != null,
|
||||
);
|
||||
} else {
|
||||
app = sample.inputAsString;
|
||||
}
|
||||
sample.output = app;
|
||||
}
|
||||
return sample.output;
|
||||
}
|
||||
|
||||
String _addLineNumbers(String code) {
|
||||
final StringBuffer buffer = StringBuffer();
|
||||
int count = 0;
|
||||
for (final String line in code.split('\n')) {
|
||||
count++;
|
||||
buffer.writeln('${count.toString().padLeft(5)}: $line');
|
||||
}
|
||||
return buffer.toString();
|
||||
}
|
||||
|
||||
/// Computes the headers needed for each snippet file.
|
||||
///
|
||||
/// Not used for "sample" and "dartpad" samples, which use their own template.
|
||||
List<SourceLine> get headers {
|
||||
return _headers ??= <String>[
|
||||
'// generated code',
|
||||
'// ignore_for_file: unused_import',
|
||||
'// ignore_for_file: unused_element',
|
||||
'// ignore_for_file: unused_local_variable',
|
||||
"import 'dart:async';",
|
||||
"import 'dart:convert';",
|
||||
"import 'dart:math' as math;",
|
||||
"import 'dart:typed_data';",
|
||||
"import 'dart:ui' as ui;",
|
||||
"import 'package:flutter_test/flutter_test.dart';",
|
||||
for (final File file in _listDartFiles(FlutterInformation.instance
|
||||
.getFlutterRoot()
|
||||
.childDirectory('packages')
|
||||
.childDirectory('flutter')
|
||||
.childDirectory('lib'))) ...<String>[
|
||||
'',
|
||||
'// ${file.path}',
|
||||
"import 'package:flutter/${path.basename(file.path)}';",
|
||||
],
|
||||
].map<SourceLine>((String code) => SourceLine(code)).toList();
|
||||
}
|
||||
|
||||
List<SourceLine>? _headers;
|
||||
|
||||
static List<File> _listDartFiles(Directory directory,
|
||||
{bool recursive = false}) {
|
||||
return directory
|
||||
.listSync(recursive: recursive, followLinks: false)
|
||||
.whereType<File>()
|
||||
.where((File file) => path.extension(file.path) == '.dart')
|
||||
.toList();
|
||||
}
|
||||
}
|
426
dev/snippets/lib/src/snippet_parser.dart
Normal file
426
dev/snippets/lib/src/snippet_parser.dart
Normal file
@ -0,0 +1,426 @@
|
||||
// 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 'package:file/file.dart';
|
||||
import 'package:path/path.dart' as path;
|
||||
|
||||
import 'data_types.dart';
|
||||
import 'util.dart';
|
||||
|
||||
/// Parses [CodeSample]s from the source file given to one of the parsing routines.
|
||||
///
|
||||
/// - [parseFromDartdocToolFile] parses the output of the dartdoc `@tool`
|
||||
/// directive, which contains the dartdoc comment lines (with comment markers
|
||||
/// stripped) contained between the tool markers.
|
||||
///
|
||||
/// - [parseAndAddAssumptions] parses the assumptions in the "Examples can
|
||||
/// assume:" block at the top of the file and adds them to the code samples
|
||||
/// contained in the given [SourceElement] iterable.
|
||||
class SnippetDartdocParser {
|
||||
SnippetDartdocParser(this.filesystem);
|
||||
|
||||
final FileSystem filesystem;
|
||||
|
||||
/// The prefix of each comment line
|
||||
static const String _dartDocPrefix = '///';
|
||||
|
||||
/// The prefix of each comment line with a space appended.
|
||||
static const String _dartDocPrefixWithSpace = '$_dartDocPrefix ';
|
||||
|
||||
/// A RegExp that matches the beginning of a dartdoc snippet or sample.
|
||||
static final RegExp _dartDocSampleBeginRegex =
|
||||
RegExp(r'\{@tool (?<type>sample|snippet|dartpad)(?:| (?<args>[^}]*))\}');
|
||||
|
||||
/// A RegExp that matches the end of a dartdoc snippet or sample.
|
||||
static final RegExp _dartDocSampleEndRegex = RegExp(r'\{@end-tool\}');
|
||||
|
||||
/// A RegExp that matches the start of a code block within dartdoc.
|
||||
static final RegExp _codeBlockStartRegex = RegExp(r'///\s+```dart.*$');
|
||||
|
||||
/// A RegExp that matches the end of a code block within dartdoc.
|
||||
static final RegExp _codeBlockEndRegex = RegExp(r'///\s+```\s*$');
|
||||
|
||||
/// A RegExp that matches a linked sample pointer.
|
||||
static final RegExp _filePointerRegex =
|
||||
RegExp(r'\*\* See code in (?<file>[^\]]+) \*\*');
|
||||
|
||||
/// Parses the assumptions in the "Examples can assume:" block at the top of
|
||||
/// the `assumptionsFile` and adds them to the code samples contained in the
|
||||
/// given `elements` iterable.
|
||||
void parseAndAddAssumptions(
|
||||
Iterable<SourceElement> elements,
|
||||
File assumptionsFile, {
|
||||
bool silent = true,
|
||||
}) {
|
||||
final List<SourceLine> assumptions = parseAssumptions(assumptionsFile);
|
||||
for (final CodeSample sample in elements
|
||||
.expand<CodeSample>((SourceElement element) => element.samples)) {
|
||||
if (sample is SnippetSample) {
|
||||
sample.assumptions = assumptions;
|
||||
}
|
||||
sample.metadata.addAll(<String, Object?>{
|
||||
'id': '${sample.element}.${sample.index}',
|
||||
'element': sample.element,
|
||||
'sourcePath': assumptionsFile.path,
|
||||
'sourceLine': sample.start.line,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/// Parses a file containing the output of the dartdoc `@tool` directive,
|
||||
/// which contains the dartdoc comment lines (with comment markers stripped)
|
||||
/// between the tool markers.
|
||||
///
|
||||
/// This is meant to be run as part of a dartdoc tool that handles snippets.
|
||||
SourceElement parseFromDartdocToolFile(
|
||||
File input, {
|
||||
int? startLine,
|
||||
String? element,
|
||||
required File sourceFile,
|
||||
String type = '',
|
||||
bool silent = true,
|
||||
}) {
|
||||
final List<SourceLine> lines = <SourceLine>[];
|
||||
int lineNumber = startLine ?? 0;
|
||||
final List<String> inputStrings = <String>[
|
||||
// The parser wants to read the arguments from the input, so we create a new
|
||||
// tool line to match the given arguments, so that we can use the same parser for
|
||||
// editing and docs generation.
|
||||
'/// {@tool $type}',
|
||||
// Snippet input comes in with the comment markers stripped, so we add them
|
||||
// back to make it conform to the source format, so we can use the same
|
||||
// parser for editing samples as we do for processing docs.
|
||||
...input
|
||||
.readAsLinesSync()
|
||||
.map<String>((String line) => '/// $line'.trimRight()),
|
||||
'/// {@end-tool}',
|
||||
];
|
||||
for (final String line in inputStrings) {
|
||||
lines.add(
|
||||
SourceLine(line,
|
||||
element: element ?? '', line: lineNumber, file: sourceFile),
|
||||
);
|
||||
lineNumber++;
|
||||
}
|
||||
// No need to get assumptions: dartdoc won't give that to us.
|
||||
final SourceElement newElement = SourceElement(
|
||||
SourceElementType.unknownType, element!, -1,
|
||||
file: input, comment: lines);
|
||||
parseFromComments(<SourceElement>[newElement], silent: silent);
|
||||
for (final CodeSample sample in newElement.samples) {
|
||||
sample.metadata.addAll(<String, Object?>{
|
||||
'id': '${sample.element}.${sample.index}',
|
||||
'element': sample.element,
|
||||
'sourcePath': sourceFile.path,
|
||||
'sourceLine': sample.start.line,
|
||||
});
|
||||
}
|
||||
return newElement;
|
||||
}
|
||||
|
||||
/// This parses the assumptions in the "Examples can assume:" block from the
|
||||
/// given `file`.
|
||||
List<SourceLine> parseAssumptions(File file) {
|
||||
// Whether or not we're in the file-wide preamble section ("Examples can assume").
|
||||
bool inPreamble = false;
|
||||
final List<SourceLine> preamble = <SourceLine>[];
|
||||
int lineNumber = 0;
|
||||
int charPosition = 0;
|
||||
for (final String line in file.readAsLinesSync()) {
|
||||
if (inPreamble && line.trim().isEmpty) {
|
||||
// Reached the end of the preamble.
|
||||
break;
|
||||
}
|
||||
if (!line.startsWith('// ')) {
|
||||
lineNumber++;
|
||||
charPosition += line.length + 1;
|
||||
continue;
|
||||
}
|
||||
if (line == '// Examples can assume:') {
|
||||
inPreamble = true;
|
||||
lineNumber++;
|
||||
charPosition += line.length + 1;
|
||||
continue;
|
||||
}
|
||||
if (inPreamble) {
|
||||
preamble.add(SourceLine(
|
||||
line.substring(3),
|
||||
startChar: charPosition,
|
||||
endChar: charPosition + line.length + 1,
|
||||
element: '#assumptions',
|
||||
file: file,
|
||||
line: lineNumber,
|
||||
));
|
||||
}
|
||||
lineNumber++;
|
||||
charPosition += line.length + 1;
|
||||
}
|
||||
return preamble;
|
||||
}
|
||||
|
||||
/// This parses the code snippets from the documentation comments in the given
|
||||
/// `elements`, and sets the resulting samples as the `samples` member of
|
||||
/// each element in the supplied iterable.
|
||||
void parseFromComments(
|
||||
Iterable<SourceElement> elements, {
|
||||
bool silent = true,
|
||||
}) {
|
||||
int dartpadCount = 0;
|
||||
int sampleCount = 0;
|
||||
int snippetCount = 0;
|
||||
|
||||
for (final SourceElement element in elements) {
|
||||
if (element.comment.isEmpty) {
|
||||
continue;
|
||||
}
|
||||
parseComment(element);
|
||||
for (final CodeSample sample in element.samples) {
|
||||
switch (sample) {
|
||||
case DartpadSample _:
|
||||
dartpadCount++;
|
||||
case ApplicationSample _:
|
||||
sampleCount++;
|
||||
case SnippetSample _:
|
||||
snippetCount++;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!silent) {
|
||||
print('Found:\n'
|
||||
' $snippetCount snippet code blocks,\n'
|
||||
' $sampleCount non-dartpad sample code sections, and\n'
|
||||
' $dartpadCount dartpad sections.\n');
|
||||
}
|
||||
}
|
||||
|
||||
/// This parses the documentation comment on a single [SourceElement] and
|
||||
/// assigns the resulting samples to the `samples` member of the given
|
||||
/// `element`.
|
||||
void parseComment(SourceElement element) {
|
||||
// Whether or not we're in a snippet code sample.
|
||||
bool inSnippet = false;
|
||||
// Whether or not we're in a '```dart' segment.
|
||||
bool inDart = false;
|
||||
bool foundSourceLink = false;
|
||||
bool foundDartSection = false;
|
||||
File? linkedFile;
|
||||
List<SourceLine> block = <SourceLine>[];
|
||||
List<String> snippetArgs = <String>[];
|
||||
final List<CodeSample> samples = <CodeSample>[];
|
||||
final Directory flutterRoot = FlutterInformation.instance.getFlutterRoot();
|
||||
|
||||
int index = 0;
|
||||
for (final SourceLine line in element.comment) {
|
||||
final String trimmedLine = line.text.trim();
|
||||
if (inSnippet) {
|
||||
if (!trimmedLine.startsWith(_dartDocPrefix)) {
|
||||
throw SnippetException('Snippet section unterminated.',
|
||||
file: line.file?.path, line: line.line);
|
||||
}
|
||||
if (_dartDocSampleEndRegex.hasMatch(trimmedLine)) {
|
||||
switch (snippetArgs.first) {
|
||||
case 'snippet':
|
||||
samples.add(
|
||||
SnippetSample(
|
||||
block,
|
||||
index: index++,
|
||||
lineProto: line,
|
||||
),
|
||||
);
|
||||
case 'sample':
|
||||
if (linkedFile != null) {
|
||||
samples.add(
|
||||
ApplicationSample.fromFile(
|
||||
input: block,
|
||||
args: snippetArgs,
|
||||
sourceFile: linkedFile,
|
||||
index: index++,
|
||||
lineProto: line,
|
||||
),
|
||||
);
|
||||
break;
|
||||
}
|
||||
samples.add(
|
||||
ApplicationSample(
|
||||
input: block,
|
||||
args: snippetArgs,
|
||||
index: index++,
|
||||
lineProto: line,
|
||||
),
|
||||
);
|
||||
case 'dartpad':
|
||||
if (linkedFile != null) {
|
||||
samples.add(
|
||||
DartpadSample.fromFile(
|
||||
input: block,
|
||||
args: snippetArgs,
|
||||
sourceFile: linkedFile,
|
||||
index: index++,
|
||||
lineProto: line,
|
||||
),
|
||||
);
|
||||
break;
|
||||
}
|
||||
samples.add(
|
||||
DartpadSample(
|
||||
input: block,
|
||||
args: snippetArgs,
|
||||
index: index++,
|
||||
lineProto: line,
|
||||
),
|
||||
);
|
||||
default:
|
||||
throw SnippetException(
|
||||
'Unknown snippet type ${snippetArgs.first}');
|
||||
}
|
||||
snippetArgs = <String>[];
|
||||
block = <SourceLine>[];
|
||||
inSnippet = false;
|
||||
foundSourceLink = false;
|
||||
foundDartSection = false;
|
||||
linkedFile = null;
|
||||
} else if (_filePointerRegex.hasMatch(trimmedLine)) {
|
||||
foundSourceLink = true;
|
||||
if (foundDartSection) {
|
||||
throw SnippetException(
|
||||
'Snippet contains a source link and a dart section. Cannot contain both.',
|
||||
file: line.file?.path,
|
||||
line: line.line,
|
||||
);
|
||||
}
|
||||
if (linkedFile != null) {
|
||||
throw SnippetException(
|
||||
'Found more than one linked sample. Only one linked file per sample is allowed.',
|
||||
file: line.file?.path,
|
||||
line: line.line,
|
||||
);
|
||||
}
|
||||
final RegExpMatch match = _filePointerRegex.firstMatch(trimmedLine)!;
|
||||
linkedFile = filesystem.file(
|
||||
path.join(flutterRoot.absolute.path, match.namedGroup('file')));
|
||||
} else {
|
||||
block.add(line.copyWith(
|
||||
text: line.text.replaceFirst(RegExp(r'\s*/// ?'), '')));
|
||||
}
|
||||
} else {
|
||||
if (_dartDocSampleEndRegex.hasMatch(trimmedLine)) {
|
||||
if (inDart) {
|
||||
throw SnippetException(
|
||||
"Dart section didn't terminate before end of sample",
|
||||
file: line.file?.path,
|
||||
line: line.line);
|
||||
}
|
||||
}
|
||||
if (inDart) {
|
||||
if (_codeBlockEndRegex.hasMatch(trimmedLine)) {
|
||||
inDart = false;
|
||||
block = <SourceLine>[];
|
||||
} else if (trimmedLine == _dartDocPrefix) {
|
||||
block.add(line.copyWith(text: ''));
|
||||
} else {
|
||||
final int index = line.text.indexOf(_dartDocPrefixWithSpace);
|
||||
if (index < 0) {
|
||||
throw SnippetException(
|
||||
'Dart section inexplicably did not contain "$_dartDocPrefixWithSpace" prefix.',
|
||||
file: line.file?.path,
|
||||
line: line.line,
|
||||
);
|
||||
}
|
||||
block.add(line.copyWith(text: line.text.substring(index + 4)));
|
||||
}
|
||||
} else if (_codeBlockStartRegex.hasMatch(trimmedLine)) {
|
||||
if (foundSourceLink) {
|
||||
throw SnippetException(
|
||||
'Snippet contains a source link and a dart section. Cannot contain both.',
|
||||
file: line.file?.path,
|
||||
line: line.line,
|
||||
);
|
||||
}
|
||||
assert(block.isEmpty);
|
||||
inDart = true;
|
||||
foundDartSection = true;
|
||||
}
|
||||
}
|
||||
if (!inSnippet && !inDart) {
|
||||
final RegExpMatch? sampleMatch =
|
||||
_dartDocSampleBeginRegex.firstMatch(trimmedLine);
|
||||
if (sampleMatch != null) {
|
||||
inSnippet = sampleMatch.namedGroup('type') == 'snippet' ||
|
||||
sampleMatch.namedGroup('type') == 'sample' ||
|
||||
sampleMatch.namedGroup('type') == 'dartpad';
|
||||
if (inSnippet) {
|
||||
if (sampleMatch.namedGroup('args') != null) {
|
||||
// There are arguments to the snippet tool to keep track of.
|
||||
snippetArgs = <String>[
|
||||
sampleMatch.namedGroup('type')!,
|
||||
..._splitUpQuotedArgs(sampleMatch.namedGroup('args')!)
|
||||
];
|
||||
} else {
|
||||
snippetArgs = <String>[
|
||||
sampleMatch.namedGroup('type')!,
|
||||
];
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
for (final CodeSample sample in samples) {
|
||||
sample.metadata.addAll(<String, Object?>{
|
||||
'id': '${sample.element}.${sample.index}',
|
||||
'element': sample.element,
|
||||
'sourcePath': sample.start.file?.path ?? '',
|
||||
'sourceLine': sample.start.line,
|
||||
});
|
||||
}
|
||||
element.replaceSamples(samples);
|
||||
}
|
||||
|
||||
// Helper to process arguments given as a (possibly quoted) string.
|
||||
//
|
||||
// First, this will split the given [argsAsString] into separate arguments,
|
||||
// taking any quoting (either ' or " are accepted) into account, including
|
||||
// handling backslash-escaped quotes.
|
||||
//
|
||||
// Then, it will prepend "--" to any args that start with an identifier
|
||||
// followed by an equals sign, allowing the argument parser to treat any
|
||||
// "foo=bar" argument as "--foo=bar" (which is a dartdoc-ism).
|
||||
Iterable<String> _splitUpQuotedArgs(String argsAsString) {
|
||||
// This function is used because the arg parser package doesn't handle
|
||||
// quoted args.
|
||||
|
||||
// Regexp to take care of splitting arguments, and handling the quotes
|
||||
// around arguments, if any.
|
||||
//
|
||||
// Match group 1 (option) is the "foo=" (or "--foo=") part of the option, if any.
|
||||
// Match group 2 (quote) contains the quote character used (which is discarded).
|
||||
// Match group 3 (value) is a quoted arg, if any, without the quotes.
|
||||
// Match group 4 (unquoted) is the unquoted arg, if any.
|
||||
final RegExp argMatcher = RegExp(
|
||||
r'(?<option>[-_a-zA-Z0-9]+=)?' // option name
|
||||
r'(?:' // Start a new non-capture group for the two possibilities.
|
||||
r'''(?<quote>["'])(?<value>(?:\\{2})*|(?:.*?[^\\](?:\\{2})*))\2|''' // value with quotes.
|
||||
r'(?<unquoted>[^ ]+))'); // without quotes.
|
||||
final Iterable<RegExpMatch> matches = argMatcher.allMatches(argsAsString);
|
||||
|
||||
// Remove quotes around args, then for any args that look like assignments
|
||||
// (start with valid option names followed by an equals sign), add a "--" in
|
||||
// front so that they parse as options to support legacy dartdoc
|
||||
// functionality of "option=value".
|
||||
return matches.map<String>((RegExpMatch match) {
|
||||
String option = '';
|
||||
if (match.namedGroup('option') != null &&
|
||||
!match.namedGroup('option')!.startsWith('-')) {
|
||||
option = '--';
|
||||
}
|
||||
if (match.namedGroup('quote') != null) {
|
||||
// This arg has quotes, so strip them.
|
||||
return '$option'
|
||||
'${match.namedGroup('value') ?? ''}'
|
||||
'${match.namedGroup('unquoted') ?? ''}';
|
||||
}
|
||||
return '$option${match[0]}';
|
||||
});
|
||||
}
|
||||
}
|
271
dev/snippets/lib/src/util.dart
Normal file
271
dev/snippets/lib/src/util.dart
Normal file
@ -0,0 +1,271 @@
|
||||
// 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:convert';
|
||||
import 'dart:io' as io;
|
||||
|
||||
import 'package:file/file.dart';
|
||||
import 'package:file/local.dart';
|
||||
import 'package:meta/meta.dart';
|
||||
import 'package:platform/platform.dart' show LocalPlatform, Platform;
|
||||
import 'package:process/process.dart' show LocalProcessManager, ProcessManager;
|
||||
import 'package:pub_semver/pub_semver.dart';
|
||||
|
||||
import 'data_types.dart';
|
||||
|
||||
/// An exception class to allow capture of exceptions generated by the Snippets
|
||||
/// package.
|
||||
class SnippetException implements Exception {
|
||||
SnippetException(this.message, {this.file, this.line});
|
||||
final String message;
|
||||
final String? file;
|
||||
final int? line;
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
if (file != null || line != null) {
|
||||
final String fileStr = file == null ? '' : '$file:';
|
||||
final String lineStr = line == null ? '' : '$line:';
|
||||
return '$runtimeType: $fileStr$lineStr: $message';
|
||||
} else {
|
||||
return '$runtimeType: $message';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Gets the number of whitespace characters at the beginning of a line.
|
||||
int getIndent(String line) => line.length - line.trimLeft().length;
|
||||
|
||||
/// Contains information about the installed Flutter repo.
|
||||
class FlutterInformation {
|
||||
FlutterInformation({
|
||||
this.platform = const LocalPlatform(),
|
||||
this.processManager = const LocalProcessManager(),
|
||||
this.filesystem = const LocalFileSystem(),
|
||||
});
|
||||
|
||||
final Platform platform;
|
||||
final ProcessManager processManager;
|
||||
final FileSystem filesystem;
|
||||
|
||||
static FlutterInformation? _instance;
|
||||
|
||||
static FlutterInformation get instance => _instance ??= FlutterInformation();
|
||||
|
||||
@visibleForTesting
|
||||
static set instance(FlutterInformation? value) => _instance = value;
|
||||
|
||||
Directory getFlutterRoot() {
|
||||
if (platform.environment['FLUTTER_ROOT'] != null) {
|
||||
return filesystem.directory(platform.environment['FLUTTER_ROOT']);
|
||||
}
|
||||
return getFlutterInformation()['flutterRoot'] as Directory;
|
||||
}
|
||||
|
||||
Version getFlutterVersion() =>
|
||||
getFlutterInformation()['frameworkVersion'] as Version;
|
||||
|
||||
Version getDartSdkVersion() =>
|
||||
getFlutterInformation()['dartSdkVersion'] as Version;
|
||||
|
||||
Map<String, dynamic>? _cachedFlutterInformation;
|
||||
|
||||
Map<String, dynamic> getFlutterInformation() {
|
||||
if (_cachedFlutterInformation != null) {
|
||||
return _cachedFlutterInformation!;
|
||||
}
|
||||
|
||||
String flutterVersionJson;
|
||||
if (platform.environment['FLUTTER_VERSION'] != null) {
|
||||
flutterVersionJson = platform.environment['FLUTTER_VERSION']!;
|
||||
} else {
|
||||
String flutterCommand;
|
||||
if (platform.environment['FLUTTER_ROOT'] != null) {
|
||||
flutterCommand = filesystem
|
||||
.directory(platform.environment['FLUTTER_ROOT'])
|
||||
.childDirectory('bin')
|
||||
.childFile('flutter')
|
||||
.absolute
|
||||
.path;
|
||||
} else {
|
||||
flutterCommand = 'flutter';
|
||||
}
|
||||
io.ProcessResult result;
|
||||
try {
|
||||
result = processManager.runSync(
|
||||
<String>[flutterCommand, '--version', '--machine'],
|
||||
stdoutEncoding: utf8);
|
||||
} on io.ProcessException catch (e) {
|
||||
throw SnippetException(
|
||||
'Unable to determine Flutter information. Either set FLUTTER_ROOT, or place flutter command in your path.\n$e');
|
||||
}
|
||||
if (result.exitCode != 0) {
|
||||
throw SnippetException(
|
||||
'Unable to determine Flutter information, because of abnormal exit to flutter command.');
|
||||
}
|
||||
flutterVersionJson = (result.stdout as String).replaceAll(
|
||||
'Waiting for another flutter command to release the startup lock...',
|
||||
'');
|
||||
}
|
||||
|
||||
final Map<String, dynamic> flutterVersion =
|
||||
json.decode(flutterVersionJson) as Map<String, dynamic>;
|
||||
if (flutterVersion['flutterRoot'] == null ||
|
||||
flutterVersion['frameworkVersion'] == null ||
|
||||
flutterVersion['dartSdkVersion'] == null) {
|
||||
throw SnippetException(
|
||||
'Flutter command output has unexpected format, unable to determine flutter root location.');
|
||||
}
|
||||
|
||||
final Map<String, dynamic> info = <String, dynamic>{};
|
||||
info['flutterRoot'] =
|
||||
filesystem.directory(flutterVersion['flutterRoot']! as String);
|
||||
info['frameworkVersion'] =
|
||||
Version.parse(flutterVersion['frameworkVersion'] as String);
|
||||
|
||||
final RegExpMatch? dartVersionRegex =
|
||||
RegExp(r'(?<base>[\d.]+)(?:\s+\(build (?<detail>[-.\w]+)\))?')
|
||||
.firstMatch(flutterVersion['dartSdkVersion'] as String);
|
||||
if (dartVersionRegex == null) {
|
||||
throw SnippetException(
|
||||
'Flutter command output has unexpected format, unable to parse dart SDK version ${flutterVersion['dartSdkVersion']}.');
|
||||
}
|
||||
info['dartSdkVersion'] = Version.parse(
|
||||
dartVersionRegex.namedGroup('detail') ??
|
||||
dartVersionRegex.namedGroup('base')!);
|
||||
_cachedFlutterInformation = info;
|
||||
|
||||
return info;
|
||||
}
|
||||
}
|
||||
|
||||
/// Injects the [injections] into the [template], while turning the
|
||||
/// "description" injection into a comment.
|
||||
String interpolateTemplate(
|
||||
List<SkeletonInjection> injections,
|
||||
String template,
|
||||
Map<String, Object?> metadata, {
|
||||
bool addCopyright = false,
|
||||
}) {
|
||||
String wrapSectionMarker(Iterable<String> contents, {required String name}) {
|
||||
if (contents.join().trim().isEmpty) {
|
||||
// Skip empty sections.
|
||||
return '';
|
||||
}
|
||||
// We don't wrap some sections, because otherwise they generate invalid files.
|
||||
final String result = <String>[
|
||||
...contents,
|
||||
].join('\n');
|
||||
final RegExp wrappingNewlines = RegExp(r'^\n*(.*)\n*$', dotAll: true);
|
||||
return result.replaceAllMapped(
|
||||
wrappingNewlines, (Match match) => match.group(1)!);
|
||||
}
|
||||
|
||||
return '${addCopyright ? '{{copyright}}\n\n' : ''}$template'
|
||||
.replaceAllMapped(RegExp(r'{{([^}]+)}}'), (Match match) {
|
||||
final String name = match[1]!;
|
||||
final int componentIndex = injections
|
||||
.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.
|
||||
return wrapSectionMarker((metadata[name]! as String).split('\n'),
|
||||
name: name);
|
||||
}
|
||||
return wrapSectionMarker(
|
||||
componentIndex >= 0
|
||||
? injections[componentIndex].stringContents
|
||||
: <String>[],
|
||||
name: name);
|
||||
}).replaceAll(RegExp(r'\n\n+'), '\n\n');
|
||||
}
|
||||
|
||||
class SampleStats {
|
||||
const SampleStats({
|
||||
this.totalSamples = 0,
|
||||
this.dartpadSamples = 0,
|
||||
this.snippetSamples = 0,
|
||||
this.applicationSamples = 0,
|
||||
this.wordCount = 0,
|
||||
this.lineCount = 0,
|
||||
this.linkCount = 0,
|
||||
this.description = '',
|
||||
});
|
||||
|
||||
final int totalSamples;
|
||||
final int dartpadSamples;
|
||||
final int snippetSamples;
|
||||
final int applicationSamples;
|
||||
final int wordCount;
|
||||
final int lineCount;
|
||||
final int linkCount;
|
||||
final String description;
|
||||
bool get allOneKind =>
|
||||
totalSamples == snippetSamples ||
|
||||
totalSamples == applicationSamples ||
|
||||
totalSamples == dartpadSamples;
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return description;
|
||||
}
|
||||
}
|
||||
|
||||
Iterable<CodeSample> getSamplesInElements(Iterable<SourceElement>? elements) {
|
||||
return elements
|
||||
?.expand<CodeSample>((SourceElement element) => element.samples) ??
|
||||
const <CodeSample>[];
|
||||
}
|
||||
|
||||
SampleStats getSampleStats(SourceElement element) {
|
||||
if (element.comment.isEmpty) {
|
||||
return const SampleStats();
|
||||
}
|
||||
final int total = element.sampleCount;
|
||||
if (total == 0) {
|
||||
return const SampleStats();
|
||||
}
|
||||
final int dartpads = element.dartpadSampleCount;
|
||||
final int snippets = element.snippetCount;
|
||||
final int applications = element.applicationSampleCount;
|
||||
final String sampleCount = <String>[
|
||||
if (snippets > 0) '$snippets snippet${snippets != 1 ? 's' : ''}',
|
||||
if (applications > 0)
|
||||
'$applications application sample${applications != 1 ? 's' : ''}',
|
||||
if (dartpads > 0) '$dartpads dartpad sample${dartpads != 1 ? 's' : ''}'
|
||||
].join(', ');
|
||||
final int wordCount = element.wordCount;
|
||||
final int lineCount = element.lineCount;
|
||||
final int linkCount = element.referenceCount;
|
||||
final String description = <String>[
|
||||
'Documentation has $wordCount ${wordCount == 1 ? 'word' : 'words'} on ',
|
||||
'$lineCount ${lineCount == 1 ? 'line' : 'lines'}',
|
||||
if (linkCount > 0 && element.hasSeeAlso) ', ',
|
||||
if (linkCount > 0 && !element.hasSeeAlso) ' and ',
|
||||
if (linkCount > 0)
|
||||
'refers to $linkCount other ${linkCount == 1 ? 'symbol' : 'symbols'}',
|
||||
if (linkCount > 0 && element.hasSeeAlso) ', and ',
|
||||
if (linkCount == 0 && element.hasSeeAlso) 'and ',
|
||||
if (element.hasSeeAlso) 'has a "See also:" section',
|
||||
'.',
|
||||
].join();
|
||||
return SampleStats(
|
||||
totalSamples: total,
|
||||
dartpadSamples: dartpads,
|
||||
snippetSamples: snippets,
|
||||
applicationSamples: applications,
|
||||
wordCount: wordCount,
|
||||
lineCount: lineCount,
|
||||
linkCount: linkCount,
|
||||
description: 'Has $sampleCount. $description',
|
||||
);
|
||||
}
|
||||
|
||||
/// Exit the app with a message to stderr.
|
||||
/// Can be overridden by tests to avoid exits.
|
||||
// ignore: prefer_function_declarations_over_variables
|
||||
void Function(String message) errorExit = (String message) {
|
||||
io.stderr.writeln(message);
|
||||
io.exit(1);
|
||||
};
|
64
dev/snippets/pubspec.yaml
Normal file
64
dev/snippets/pubspec.yaml
Normal file
@ -0,0 +1,64 @@
|
||||
name: snippets
|
||||
description: A package for parsing and manipulating code samples in Flutter repo dartdoc comments.
|
||||
|
||||
environment:
|
||||
sdk: '>=3.2.0-0 <4.0.0'
|
||||
|
||||
dependencies:
|
||||
analyzer: 6.4.1
|
||||
args: 2.4.2
|
||||
dart_style: 2.3.6
|
||||
file: 7.0.0
|
||||
meta: 1.12.0
|
||||
path: 1.9.0
|
||||
platform: 3.1.4
|
||||
process: 5.0.2
|
||||
|
||||
_fe_analyzer_shared: 67.0.0 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade"
|
||||
async: 2.11.0 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade"
|
||||
boolean_selector: 2.1.1 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade"
|
||||
collection: 1.18.0 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade"
|
||||
convert: 3.1.1 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade"
|
||||
coverage: 1.7.2 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade"
|
||||
crypto: 3.0.3 # 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"
|
||||
glob: 2.1.2 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade"
|
||||
http_multi_server: 3.2.1 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade"
|
||||
http_parser: 4.0.2 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade"
|
||||
io: 1.0.4 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade"
|
||||
js: 0.7.1 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade"
|
||||
logging: 1.2.0 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade"
|
||||
matcher: 0.12.16+1 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade"
|
||||
mime: 1.0.5 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade"
|
||||
node_preamble: 2.0.2 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade"
|
||||
package_config: 2.1.0 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade"
|
||||
pool: 1.5.1 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade"
|
||||
pub_semver: 2.1.4 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade"
|
||||
shelf: 1.4.1 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade"
|
||||
shelf_packages_handler: 3.0.2 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade"
|
||||
shelf_static: 1.1.2 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade"
|
||||
shelf_web_socket: 1.0.4 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade"
|
||||
source_map_stack_trace: 2.1.1 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade"
|
||||
source_maps: 0.10.12 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade"
|
||||
source_span: 1.10.0 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade"
|
||||
stack_trace: 1.11.1 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade"
|
||||
stream_channel: 2.1.2 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade"
|
||||
string_scanner: 1.2.0 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade"
|
||||
term_glyph: 1.2.1 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade"
|
||||
test_api: 0.7.0 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade"
|
||||
test_core: 0.6.0 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade"
|
||||
typed_data: 1.3.2 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade"
|
||||
vm_service: 14.2.1 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade"
|
||||
watcher: 1.1.0 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade"
|
||||
web: 0.5.1 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade"
|
||||
web_socket_channel: 2.4.4 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade"
|
||||
webkit_inspection_protocol: 1.2.1 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade"
|
||||
yaml: 3.1.2 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade"
|
||||
|
||||
dev_dependencies:
|
||||
test: 1.25.2
|
||||
|
||||
executables:
|
||||
snippets:
|
||||
|
||||
# PUBSPEC CHECKSUM: 94f5
|
49
dev/snippets/test/configuration_test.dart
Normal file
49
dev/snippets/test/configuration_test.dart
Normal file
@ -0,0 +1,49 @@
|
||||
// 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 'package:file/memory.dart';
|
||||
import 'package:snippets/snippets.dart';
|
||||
import 'package:test/test.dart' hide TypeMatcher, isInstanceOf;
|
||||
|
||||
void main() {
|
||||
group('Configuration', () {
|
||||
final MemoryFileSystem memoryFileSystem = MemoryFileSystem();
|
||||
late SnippetConfiguration config;
|
||||
|
||||
setUp(() {
|
||||
config = FlutterRepoSnippetConfiguration(
|
||||
flutterRoot: memoryFileSystem.directory('/flutter sdk'),
|
||||
filesystem: memoryFileSystem,
|
||||
);
|
||||
});
|
||||
test('config directory is correct', () async {
|
||||
expect(config.configDirectory.path,
|
||||
matches(RegExp(r'[/\\]flutter sdk[/\\]dev[/\\]snippets[/\\]config')));
|
||||
});
|
||||
test('skeleton directory is correct', () async {
|
||||
expect(
|
||||
config.skeletonsDirectory.path,
|
||||
matches(RegExp(
|
||||
r'[/\\]flutter sdk[/\\]dev[/\\]snippets[/\\]config[/\\]skeletons')));
|
||||
});
|
||||
test('html skeleton file for sample is correct', () async {
|
||||
expect(
|
||||
config.getHtmlSkeletonFile('snippet').path,
|
||||
matches(RegExp(
|
||||
r'[/\\]flutter sdk[/\\]dev[/\\]snippets[/\\]config[/\\]skeletons[/\\]snippet.html')));
|
||||
});
|
||||
test('html skeleton file for app with no dartpad is correct', () async {
|
||||
expect(
|
||||
config.getHtmlSkeletonFile('sample').path,
|
||||
matches(RegExp(
|
||||
r'[/\\]flutter sdk[/\\]dev[/\\]snippets[/\\]config[/\\]skeletons[/\\]sample.html')));
|
||||
});
|
||||
test('html skeleton file for app with dartpad is correct', () async {
|
||||
expect(
|
||||
config.getHtmlSkeletonFile('dartpad').path,
|
||||
matches(RegExp(
|
||||
r'[/\\]flutter sdk[/\\]dev[/\\]snippets[/\\]config[/\\]skeletons[/\\]dartpad-sample.html')));
|
||||
});
|
||||
});
|
||||
}
|
33
dev/snippets/test/fake_process_manager.dart
Normal file
33
dev/snippets/test/fake_process_manager.dart
Normal file
@ -0,0 +1,33 @@
|
||||
// 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:convert';
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:process/process.dart';
|
||||
|
||||
class FakeProcessManager extends LocalProcessManager {
|
||||
FakeProcessManager(
|
||||
{this.stdout = '', this.stderr = '', this.exitCode = 0, this.pid = 1});
|
||||
|
||||
int runs = 0;
|
||||
String stdout;
|
||||
String stderr;
|
||||
int exitCode;
|
||||
int pid;
|
||||
|
||||
@override
|
||||
ProcessResult runSync(
|
||||
List<Object> command, {
|
||||
String? workingDirectory,
|
||||
Map<String, String>? environment,
|
||||
bool includeParentEnvironment = true,
|
||||
bool runInShell = false,
|
||||
Encoding? stdoutEncoding = systemEncoding,
|
||||
Encoding? stderrEncoding = systemEncoding,
|
||||
}) {
|
||||
runs++;
|
||||
return ProcessResult(pid, exitCode, stdout, stderr);
|
||||
}
|
||||
}
|
423
dev/snippets/test/filesystem_resource_provider.dart
Normal file
423
dev/snippets/test/filesystem_resource_provider.dart
Normal file
@ -0,0 +1,423 @@
|
||||
// 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' as io;
|
||||
import 'dart:typed_data';
|
||||
|
||||
import 'package:analyzer/file_system/file_system.dart';
|
||||
import 'package:analyzer/src/generated/source.dart';
|
||||
import 'package:analyzer/src/source/source_resource.dart';
|
||||
import 'package:file/file.dart' as file;
|
||||
import 'package:file/local.dart' as file;
|
||||
import 'package:meta/meta.dart';
|
||||
import 'package:path/path.dart';
|
||||
import 'package:watcher/watcher.dart';
|
||||
|
||||
/// The name of the directory containing plugin specific subfolders used to
|
||||
/// store data across sessions.
|
||||
const String _SERVER_DIR = '.dartServer';
|
||||
|
||||
/// Returns the path to default state location.
|
||||
///
|
||||
/// Generally this is ~/.dartServer. It can be overridden via the
|
||||
/// ANALYZER_STATE_LOCATION_OVERRIDE environment variable, in which case this
|
||||
/// method will return the contents of that environment variable.
|
||||
String? _getStandardStateLocation() {
|
||||
final Map<String, String> env = io.Platform.environment;
|
||||
if (env.containsKey('ANALYZER_STATE_LOCATION_OVERRIDE')) {
|
||||
return env['ANALYZER_STATE_LOCATION_OVERRIDE'];
|
||||
}
|
||||
|
||||
final String? home =
|
||||
io.Platform.isWindows ? env['LOCALAPPDATA'] : env['HOME'];
|
||||
return home != null && io.FileSystemEntity.isDirectorySync(home)
|
||||
? join(home, _SERVER_DIR)
|
||||
: null;
|
||||
}
|
||||
|
||||
/// A `dart:io` based implementation of [ResourceProvider].
|
||||
class FileSystemResourceProvider implements ResourceProvider {
|
||||
FileSystemResourceProvider(this.filesystem, {String? stateLocation})
|
||||
: _stateLocation = stateLocation ?? _getStandardStateLocation();
|
||||
|
||||
static final FileSystemResourceProvider instance =
|
||||
FileSystemResourceProvider(const file.LocalFileSystem());
|
||||
|
||||
/// The path to the base folder where state is stored.
|
||||
final String? _stateLocation;
|
||||
|
||||
final file.FileSystem filesystem;
|
||||
|
||||
@override
|
||||
Context get pathContext => context;
|
||||
|
||||
@override
|
||||
File getFile(String path) {
|
||||
_ensureAbsoluteAndNormalized(path);
|
||||
return _PhysicalFile(filesystem.file(path));
|
||||
}
|
||||
|
||||
@override
|
||||
Folder getFolder(String path) {
|
||||
_ensureAbsoluteAndNormalized(path);
|
||||
return _PhysicalFolder(filesystem.directory(path));
|
||||
}
|
||||
|
||||
@override
|
||||
Resource getResource(String path) {
|
||||
_ensureAbsoluteAndNormalized(path);
|
||||
if (filesystem.isDirectorySync(path)) {
|
||||
return getFolder(path);
|
||||
} else {
|
||||
return getFile(path);
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Folder? getStateLocation(String pluginId) {
|
||||
if (_stateLocation != null) {
|
||||
final file.Directory directory =
|
||||
filesystem.directory(join(_stateLocation, pluginId));
|
||||
directory.createSync(recursive: true);
|
||||
return _PhysicalFolder(directory);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/// The file system abstraction supports only absolute and normalized paths.
|
||||
/// This method is used to validate any input paths to prevent errors later.
|
||||
void _ensureAbsoluteAndNormalized(String path) {
|
||||
assert(() {
|
||||
if (!pathContext.isAbsolute(path)) {
|
||||
throw ArgumentError('Path must be absolute : $path');
|
||||
}
|
||||
if (pathContext.normalize(path) != path) {
|
||||
throw ArgumentError('Path must be normalized : $path');
|
||||
}
|
||||
return true;
|
||||
}());
|
||||
}
|
||||
}
|
||||
|
||||
/// A `dart:io` based implementation of [File].
|
||||
class _PhysicalFile extends _PhysicalResource implements File {
|
||||
const _PhysicalFile(io.File super.file);
|
||||
|
||||
@override
|
||||
Stream<WatchEvent> get changes => FileWatcher(_entry.path).events;
|
||||
|
||||
@override
|
||||
int get lengthSync {
|
||||
try {
|
||||
return _file.lengthSync();
|
||||
} on io.FileSystemException catch (exception) {
|
||||
throw _wrapException(exception);
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
int get modificationStamp {
|
||||
try {
|
||||
return _file.lastModifiedSync().millisecondsSinceEpoch;
|
||||
} on io.FileSystemException catch (exception) {
|
||||
throw _wrapException(exception);
|
||||
}
|
||||
}
|
||||
|
||||
/// Return the underlying file being represented by this wrapper.
|
||||
io.File get _file => _entry as io.File;
|
||||
|
||||
@override
|
||||
File copyTo(Folder parentFolder) {
|
||||
parentFolder.create();
|
||||
final File destination = parentFolder.getChildAssumingFile(shortName);
|
||||
destination.writeAsBytesSync(readAsBytesSync());
|
||||
return destination;
|
||||
}
|
||||
|
||||
@override
|
||||
Source createSource([Uri? uri]) {
|
||||
return FileSource(this, uri ?? pathContext.toUri(path));
|
||||
}
|
||||
|
||||
@override
|
||||
bool isOrContains(String path) {
|
||||
return path == this.path;
|
||||
}
|
||||
|
||||
@override
|
||||
Uint8List readAsBytesSync() {
|
||||
_throwIfWindowsDeviceDriver();
|
||||
try {
|
||||
return _file.readAsBytesSync();
|
||||
} on io.FileSystemException catch (exception) {
|
||||
throw _wrapException(exception);
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
String readAsStringSync() {
|
||||
_throwIfWindowsDeviceDriver();
|
||||
try {
|
||||
return _file.readAsStringSync();
|
||||
} on io.FileSystemException catch (exception) {
|
||||
throw _wrapException(exception);
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
File renameSync(String newPath) {
|
||||
try {
|
||||
return _PhysicalFile(_file.renameSync(newPath));
|
||||
} on io.FileSystemException catch (exception) {
|
||||
throw _wrapException(exception);
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
File resolveSymbolicLinksSync() {
|
||||
try {
|
||||
return _PhysicalFile(io.File(_file.resolveSymbolicLinksSync()));
|
||||
} on io.FileSystemException catch (exception) {
|
||||
throw _wrapException(exception);
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Uri toUri() => Uri.file(path);
|
||||
|
||||
@override
|
||||
void writeAsBytesSync(List<int> bytes) {
|
||||
try {
|
||||
_file.writeAsBytesSync(bytes);
|
||||
} on io.FileSystemException catch (exception) {
|
||||
throw _wrapException(exception);
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
void writeAsStringSync(String content) {
|
||||
try {
|
||||
_file.writeAsStringSync(content);
|
||||
} on io.FileSystemException catch (exception) {
|
||||
throw _wrapException(exception);
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
ResourceWatcher watch() {
|
||||
throw UnimplementedError();
|
||||
}
|
||||
}
|
||||
|
||||
/// A `dart:io` based implementation of [Folder].
|
||||
class _PhysicalFolder extends _PhysicalResource implements Folder {
|
||||
const _PhysicalFolder(io.Directory super.directory);
|
||||
|
||||
@override
|
||||
Stream<WatchEvent> get changes =>
|
||||
DirectoryWatcher(_entry.path).events.handleError((Object error) {},
|
||||
test: (dynamic error) =>
|
||||
error is io.FileSystemException &&
|
||||
// Don't suppress "Directory watcher closed," so the outer
|
||||
// listener can see the interruption & act on it.
|
||||
!error.message
|
||||
.startsWith('Directory watcher closed unexpectedly'));
|
||||
|
||||
@override
|
||||
bool get isRoot {
|
||||
final String parentPath = provider.pathContext.dirname(path);
|
||||
return parentPath == path;
|
||||
}
|
||||
|
||||
/// Return the underlying file being represented by this wrapper.
|
||||
io.Directory get _directory => _entry as io.Directory;
|
||||
|
||||
@override
|
||||
String canonicalizePath(String relPath) {
|
||||
return normalize(join(path, relPath));
|
||||
}
|
||||
|
||||
@override
|
||||
bool contains(String path) {
|
||||
FileSystemResourceProvider.instance._ensureAbsoluteAndNormalized(path);
|
||||
return pathContext.isWithin(this.path, path);
|
||||
}
|
||||
|
||||
@override
|
||||
Folder copyTo(Folder parentFolder) {
|
||||
final Folder destination = parentFolder.getChildAssumingFolder(shortName);
|
||||
destination.create();
|
||||
for (final Resource child in getChildren()) {
|
||||
child.copyTo(destination);
|
||||
}
|
||||
return destination;
|
||||
}
|
||||
|
||||
@override
|
||||
void create() {
|
||||
_directory.createSync(recursive: true);
|
||||
}
|
||||
|
||||
@override
|
||||
Resource getChild(String relPath) {
|
||||
final String canonicalPath = canonicalizePath(relPath);
|
||||
return FileSystemResourceProvider.instance.getResource(canonicalPath);
|
||||
}
|
||||
|
||||
@override
|
||||
_PhysicalFile getChildAssumingFile(String relPath) {
|
||||
final String canonicalPath = canonicalizePath(relPath);
|
||||
final io.File file = io.File(canonicalPath);
|
||||
return _PhysicalFile(file);
|
||||
}
|
||||
|
||||
@override
|
||||
_PhysicalFolder getChildAssumingFolder(String relPath) {
|
||||
final String canonicalPath = canonicalizePath(relPath);
|
||||
final io.Directory directory = io.Directory(canonicalPath);
|
||||
return _PhysicalFolder(directory);
|
||||
}
|
||||
|
||||
@override
|
||||
List<Resource> getChildren() {
|
||||
try {
|
||||
final List<Resource> children = <Resource>[];
|
||||
final io.Directory directory = _entry as io.Directory;
|
||||
final List<io.FileSystemEntity> entries = directory.listSync();
|
||||
final int numEntries = entries.length;
|
||||
for (int i = 0; i < numEntries; i++) {
|
||||
final io.FileSystemEntity entity = entries[i];
|
||||
if (entity is io.Directory) {
|
||||
children.add(_PhysicalFolder(entity));
|
||||
} else if (entity is io.File) {
|
||||
children.add(_PhysicalFile(entity));
|
||||
}
|
||||
}
|
||||
return children;
|
||||
} on io.FileSystemException catch (exception) {
|
||||
throw _wrapException(exception);
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
bool isOrContains(String path) {
|
||||
if (path == this.path) {
|
||||
return true;
|
||||
}
|
||||
return contains(path);
|
||||
}
|
||||
|
||||
@override
|
||||
Folder resolveSymbolicLinksSync() {
|
||||
try {
|
||||
return _PhysicalFolder(
|
||||
io.Directory(_directory.resolveSymbolicLinksSync()));
|
||||
} on io.FileSystemException catch (exception) {
|
||||
throw _wrapException(exception);
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Uri toUri() => Uri.directory(path);
|
||||
|
||||
@override
|
||||
ResourceWatcher watch() {
|
||||
throw UnimplementedError();
|
||||
}
|
||||
}
|
||||
|
||||
/// A `dart:io` based implementation of [Resource].
|
||||
@immutable
|
||||
abstract class _PhysicalResource implements Resource {
|
||||
const _PhysicalResource(this._entry);
|
||||
|
||||
final io.FileSystemEntity _entry;
|
||||
|
||||
@override
|
||||
bool get exists {
|
||||
try {
|
||||
return _entry.existsSync();
|
||||
} on FileSystemException {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
int get hashCode => path.hashCode;
|
||||
|
||||
@override
|
||||
Folder get parent {
|
||||
final String parentPath = pathContext.dirname(path);
|
||||
return _PhysicalFolder(io.Directory(parentPath));
|
||||
}
|
||||
|
||||
@override
|
||||
Folder get parent2 {
|
||||
final String parentPath = pathContext.dirname(path);
|
||||
return _PhysicalFolder(io.Directory(parentPath));
|
||||
}
|
||||
|
||||
@override
|
||||
String get path => _entry.path;
|
||||
|
||||
/// Return the path context used by this resource provider.
|
||||
Context get pathContext => io.Platform.isWindows ? windows : posix;
|
||||
|
||||
@override
|
||||
ResourceProvider get provider => FileSystemResourceProvider.instance;
|
||||
|
||||
@override
|
||||
String get shortName => pathContext.basename(path);
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) {
|
||||
if (runtimeType != other.runtimeType) {
|
||||
return false;
|
||||
}
|
||||
// ignore: test_types_in_equals
|
||||
return path == (other as _PhysicalResource).path;
|
||||
}
|
||||
|
||||
@override
|
||||
void delete() {
|
||||
try {
|
||||
_entry.deleteSync(recursive: true);
|
||||
} on io.FileSystemException catch (exception) {
|
||||
throw _wrapException(exception);
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
String toString() => path;
|
||||
|
||||
/// If the operating system is Windows and the resource references one of the
|
||||
/// device drivers, throw a [FileSystemException].
|
||||
///
|
||||
/// https://support.microsoft.com/en-us/kb/74496
|
||||
void _throwIfWindowsDeviceDriver() {
|
||||
if (io.Platform.isWindows) {
|
||||
final String shortName = this.shortName.toUpperCase();
|
||||
if (shortName == r'CON' ||
|
||||
shortName == r'PRN' ||
|
||||
shortName == r'AUX' ||
|
||||
shortName == r'CLOCK$' ||
|
||||
shortName == r'NUL' ||
|
||||
shortName == r'COM1' ||
|
||||
shortName == r'LPT1' ||
|
||||
shortName == r'LPT2' ||
|
||||
shortName == r'LPT3' ||
|
||||
shortName == r'COM2' ||
|
||||
shortName == r'COM3' ||
|
||||
shortName == r'COM4') {
|
||||
throw FileSystemException(
|
||||
path, 'Windows device drivers cannot be read.');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
FileSystemException _wrapException(io.FileSystemException e) {
|
||||
return FileSystemException(e.path ?? path, e.message);
|
||||
}
|
||||
}
|
103
dev/snippets/test/import_sorter_test.dart
Normal file
103
dev/snippets/test/import_sorter_test.dart
Normal file
@ -0,0 +1,103 @@
|
||||
// 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 'package:file/file.dart';
|
||||
import 'package:file/memory.dart';
|
||||
import 'package:path/path.dart' as path;
|
||||
import 'package:pub_semver/pub_semver.dart';
|
||||
import 'package:snippets/snippets.dart';
|
||||
import 'package:test/test.dart' hide TypeMatcher, isInstanceOf;
|
||||
|
||||
class FakeFlutterInformation extends FlutterInformation {
|
||||
FakeFlutterInformation(this.flutterRoot);
|
||||
|
||||
final Directory flutterRoot;
|
||||
|
||||
@override
|
||||
Map<String, dynamic> getFlutterInformation() {
|
||||
return <String, dynamic>{
|
||||
'flutterRoot': flutterRoot,
|
||||
'frameworkVersion': Version(2, 10, 0),
|
||||
'dartSdkVersion': Version(2, 12, 1),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
void main() {
|
||||
late MemoryFileSystem memoryFileSystem = MemoryFileSystem();
|
||||
late Directory tmpDir;
|
||||
|
||||
setUp(() {
|
||||
// Create a new filesystem.
|
||||
memoryFileSystem = MemoryFileSystem();
|
||||
tmpDir = memoryFileSystem.systemTempDirectory
|
||||
.createTempSync('flutter_snippets_test.');
|
||||
final Directory flutterRoot =
|
||||
memoryFileSystem.directory(path.join(tmpDir.absolute.path, 'flutter'));
|
||||
FlutterInformation.instance = FakeFlutterInformation(flutterRoot);
|
||||
});
|
||||
|
||||
test('Sorting packages works', () async {
|
||||
final String result = sortImports('''
|
||||
// Unit comment
|
||||
|
||||
// third import
|
||||
import 'packages:gamma/gamma.dart'; // third
|
||||
|
||||
// second import
|
||||
import 'packages:beta/beta.dart'; // second
|
||||
|
||||
// first import
|
||||
import 'packages:alpha/alpha.dart'; // first
|
||||
|
||||
void main() {}
|
||||
''');
|
||||
expect(result, equals('''
|
||||
// Unit comment
|
||||
|
||||
// first import
|
||||
import 'packages:alpha/alpha.dart'; // first
|
||||
// second import
|
||||
import 'packages:beta/beta.dart'; // second
|
||||
// third import
|
||||
import 'packages:gamma/gamma.dart'; // third
|
||||
|
||||
void main() {}
|
||||
'''));
|
||||
});
|
||||
test('Sorting dart and packages works', () async {
|
||||
final String result = sortImports('''
|
||||
// Unit comment
|
||||
|
||||
// third import
|
||||
import 'packages:gamma/gamma.dart'; // third
|
||||
|
||||
// second import
|
||||
import 'packages:beta/beta.dart'; // second
|
||||
|
||||
// first import
|
||||
import 'packages:alpha/alpha.dart'; // first
|
||||
|
||||
// first dart
|
||||
import 'dart:async';
|
||||
|
||||
void main() {}
|
||||
''');
|
||||
expect(result, equals('''
|
||||
// Unit comment
|
||||
|
||||
// first dart
|
||||
import 'dart:async';
|
||||
|
||||
// first import
|
||||
import 'packages:alpha/alpha.dart'; // first
|
||||
// second import
|
||||
import 'packages:beta/beta.dart'; // second
|
||||
// third import
|
||||
import 'packages:gamma/gamma.dart'; // third
|
||||
|
||||
void main() {}
|
||||
'''));
|
||||
});
|
||||
}
|
292
dev/snippets/test/snippet_parser_test.dart
Normal file
292
dev/snippets/test/snippet_parser_test.dart
Normal file
@ -0,0 +1,292 @@
|
||||
// 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 'package:file/file.dart';
|
||||
import 'package:file/memory.dart';
|
||||
import 'package:path/path.dart' as path;
|
||||
import 'package:pub_semver/pub_semver.dart';
|
||||
import 'package:snippets/snippets.dart';
|
||||
import 'package:test/test.dart' hide TypeMatcher, isInstanceOf;
|
||||
|
||||
import 'filesystem_resource_provider.dart';
|
||||
|
||||
class FakeFlutterInformation extends FlutterInformation {
|
||||
FakeFlutterInformation(this.flutterRoot);
|
||||
|
||||
final Directory flutterRoot;
|
||||
|
||||
@override
|
||||
Directory getFlutterRoot() {
|
||||
return flutterRoot;
|
||||
}
|
||||
|
||||
@override
|
||||
Map<String, dynamic> getFlutterInformation() {
|
||||
return <String, dynamic>{
|
||||
'flutterRoot': flutterRoot,
|
||||
'frameworkVersion': Version(2, 10, 0),
|
||||
'dartSdkVersion': Version(2, 12, 1),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
void main() {
|
||||
group('Parser', () {
|
||||
late MemoryFileSystem memoryFileSystem = MemoryFileSystem();
|
||||
late FlutterRepoSnippetConfiguration configuration;
|
||||
late SnippetGenerator generator;
|
||||
late Directory tmpDir;
|
||||
late Directory flutterRoot;
|
||||
|
||||
void writeSkeleton(String type) {
|
||||
switch (type) {
|
||||
case 'dartpad':
|
||||
configuration.getHtmlSkeletonFile('dartpad').writeAsStringSync('''
|
||||
<div>HTML Bits (DartPad-style)</div>
|
||||
<iframe class="snippet-dartpad" src="https://dartpad.dev/embed-flutter.html?split=60&run=true&sample_id={{id}}&sample_channel={{channel}}"></iframe>
|
||||
<div>More HTML Bits</div>
|
||||
''');
|
||||
case 'sample':
|
||||
case 'snippet':
|
||||
configuration.getHtmlSkeletonFile(type).writeAsStringSync('''
|
||||
<div>HTML Bits</div>
|
||||
{{description}}
|
||||
<pre>{{code}}</pre>
|
||||
<pre>{{app}}</pre>
|
||||
<div>More HTML Bits</div>
|
||||
''');
|
||||
}
|
||||
}
|
||||
|
||||
setUp(() {
|
||||
// Create a new filesystem.
|
||||
memoryFileSystem = MemoryFileSystem();
|
||||
tmpDir = memoryFileSystem.systemTempDirectory
|
||||
.createTempSync('flutter_snippets_test.');
|
||||
flutterRoot = memoryFileSystem
|
||||
.directory(path.join(tmpDir.absolute.path, 'flutter'));
|
||||
configuration = FlutterRepoSnippetConfiguration(
|
||||
flutterRoot: flutterRoot, filesystem: memoryFileSystem);
|
||||
configuration.skeletonsDirectory.createSync(recursive: true);
|
||||
<String>['dartpad', 'sample', 'snippet'].forEach(writeSkeleton);
|
||||
FlutterInformation.instance = FakeFlutterInformation(flutterRoot);
|
||||
generator = SnippetGenerator(
|
||||
configuration: configuration,
|
||||
filesystem: memoryFileSystem,
|
||||
flutterRoot: configuration.skeletonsDirectory.parent);
|
||||
});
|
||||
|
||||
test('parses from comments', () async {
|
||||
final File inputFile = _createSnippetSourceFile(tmpDir, memoryFileSystem);
|
||||
final Iterable<SourceElement> elements = getFileElements(inputFile,
|
||||
resourceProvider: FileSystemResourceProvider(memoryFileSystem));
|
||||
expect(elements, isNotEmpty);
|
||||
final SnippetDartdocParser sampleParser =
|
||||
SnippetDartdocParser(memoryFileSystem);
|
||||
sampleParser.parseFromComments(elements);
|
||||
sampleParser.parseAndAddAssumptions(elements, inputFile);
|
||||
expect(elements.length, equals(7));
|
||||
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(
|
||||
'''^String elementName = '${element.elementName}';\$''',
|
||||
multiLine: true)));
|
||||
final String html = generator.generateHtml(element.samples.first);
|
||||
expect(
|
||||
html,
|
||||
contains(RegExp(
|
||||
'''^<pre>String elementName = '${element.elementName}';.*\$''',
|
||||
multiLine: true)));
|
||||
expect(
|
||||
html,
|
||||
contains(
|
||||
'<div class="snippet-description">{@end-inject-html}Description{@inject-html}</div>\n'));
|
||||
}
|
||||
expect(sampleCount, equals(8));
|
||||
});
|
||||
test('parses dartpad samples from linked file', () async {
|
||||
final File inputFile = _createDartpadSourceFile(
|
||||
tmpDir, memoryFileSystem, flutterRoot,
|
||||
linked: true);
|
||||
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, formatOutput: false);
|
||||
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 assumptions', () async {
|
||||
final File inputFile = _createSnippetSourceFile(tmpDir, memoryFileSystem);
|
||||
final SnippetDartdocParser sampleParser =
|
||||
SnippetDartdocParser(memoryFileSystem);
|
||||
final List<SourceLine> assumptions =
|
||||
sampleParser.parseAssumptions(inputFile);
|
||||
expect(assumptions.length, equals(1));
|
||||
expect(assumptions.first.text, equals('int integer = 3;'));
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
File _createSnippetSourceFile(Directory tmpDir, FileSystem filesystem) {
|
||||
return filesystem.file(path.join(tmpDir.absolute.path, 'snippet_in.dart'))
|
||||
..createSync(recursive: true)
|
||||
..writeAsStringSync(r'''
|
||||
// Copyright
|
||||
|
||||
// @dart = 2.12
|
||||
|
||||
import 'foo.dart';
|
||||
|
||||
// Examples can assume:
|
||||
// int integer = 3;
|
||||
|
||||
/// Top level variable comment
|
||||
///
|
||||
/// {@tool snippet}
|
||||
/// Description
|
||||
/// ```dart
|
||||
/// String elementName = 'topLevelVariable';
|
||||
/// ```
|
||||
/// {@end-tool}
|
||||
int topLevelVariable = 4;
|
||||
|
||||
/// Top level function comment
|
||||
///
|
||||
/// {@tool snippet}
|
||||
/// Description
|
||||
/// ```dart
|
||||
/// String elementName = 'topLevelFunction';
|
||||
/// ```
|
||||
/// {@end-tool}
|
||||
int topLevelFunction() {
|
||||
return integer;
|
||||
}
|
||||
|
||||
/// Class comment
|
||||
///
|
||||
/// {@tool snippet}
|
||||
/// Description
|
||||
/// ```dart
|
||||
/// String elementName = 'DocumentedClass';
|
||||
/// ```
|
||||
/// {@end-tool}
|
||||
///
|
||||
/// {@tool snippet}
|
||||
/// Description2
|
||||
/// ```dart
|
||||
/// String elementName = 'DocumentedClass';
|
||||
/// ```
|
||||
/// {@end-tool}
|
||||
class DocumentedClass {
|
||||
/// Constructor comment
|
||||
/// {@tool snippet}
|
||||
/// Description
|
||||
/// ```dart
|
||||
/// String elementName = 'DocumentedClass';
|
||||
/// ```
|
||||
/// {@end-tool}
|
||||
const DocumentedClass();
|
||||
|
||||
/// Named constructor comment
|
||||
/// {@tool snippet}
|
||||
/// Description
|
||||
/// ```dart
|
||||
/// String elementName = 'DocumentedClass.name';
|
||||
/// ```
|
||||
/// {@end-tool}
|
||||
const DocumentedClass.name();
|
||||
|
||||
/// Member variable comment
|
||||
/// {@tool snippet}
|
||||
/// Description
|
||||
/// ```dart
|
||||
/// String elementName = 'DocumentedClass.intMember';
|
||||
/// ```
|
||||
/// {@end-tool}
|
||||
int intMember;
|
||||
|
||||
/// Member comment
|
||||
/// {@tool snippet}
|
||||
/// Description
|
||||
/// ```dart
|
||||
/// String elementName = 'DocumentedClass.member';
|
||||
/// ```
|
||||
/// {@end-tool}
|
||||
void member() {}
|
||||
}
|
||||
''');
|
||||
}
|
||||
|
||||
File _createDartpadSourceFile(
|
||||
Directory tmpDir, FileSystem filesystem, Directory flutterRoot,
|
||||
{bool linked = false}) {
|
||||
final File linkedFile =
|
||||
filesystem.file(path.join(flutterRoot.absolute.path, 'linked_file.dart'))
|
||||
..createSync(recursive: true)
|
||||
..writeAsStringSync('''
|
||||
// Copyright
|
||||
|
||||
import 'foo.dart';
|
||||
|
||||
// Description
|
||||
|
||||
void DocumentedClassSample() {
|
||||
String elementName = 'DocumentedClass';
|
||||
}
|
||||
''');
|
||||
|
||||
final String source = linked
|
||||
? '''
|
||||
/// ** See code in ${path.relative(linkedFile.path, from: flutterRoot.absolute.path)} **'''
|
||||
: '''
|
||||
/// ```dart
|
||||
/// void DocumentedClassSample() {
|
||||
/// String elementName = 'DocumentedClass';
|
||||
/// }
|
||||
/// ```''';
|
||||
|
||||
return filesystem.file(path.join(tmpDir.absolute.path, 'snippet_in.dart'))
|
||||
..createSync(recursive: true)
|
||||
..writeAsStringSync('''
|
||||
// Copyright
|
||||
|
||||
// @dart = 2.12
|
||||
|
||||
import 'foo.dart';
|
||||
|
||||
/// Class comment
|
||||
///
|
||||
/// {@tool dartpad --template=template}
|
||||
/// Description
|
||||
$source
|
||||
/// {@end-tool}
|
||||
class DocumentedClass {}
|
||||
''');
|
||||
}
|
433
dev/snippets/test/snippets_test.dart
Normal file
433
dev/snippets/test/snippets_test.dart
Normal file
@ -0,0 +1,433 @@
|
||||
// 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:convert';
|
||||
|
||||
import 'package:file/file.dart';
|
||||
import 'package:file/memory.dart';
|
||||
import 'package:path/path.dart' as path;
|
||||
import 'package:platform/platform.dart';
|
||||
import 'package:pub_semver/pub_semver.dart';
|
||||
import 'package:snippets/snippets.dart';
|
||||
import 'package:test/test.dart' hide TypeMatcher, isInstanceOf;
|
||||
|
||||
import '../bin/snippets.dart' as snippets_main;
|
||||
import 'fake_process_manager.dart';
|
||||
|
||||
class FakeFlutterInformation extends FlutterInformation {
|
||||
FakeFlutterInformation(this.flutterRoot);
|
||||
|
||||
final Directory flutterRoot;
|
||||
|
||||
@override
|
||||
Directory getFlutterRoot() {
|
||||
return flutterRoot;
|
||||
}
|
||||
|
||||
@override
|
||||
Map<String, dynamic> getFlutterInformation() {
|
||||
return <String, dynamic>{
|
||||
'flutterRoot': flutterRoot,
|
||||
'frameworkVersion': Version(2, 10, 0),
|
||||
'dartSdkVersion': Version(2, 12, 1),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
void main() {
|
||||
group('Generator', () {
|
||||
late MemoryFileSystem memoryFileSystem = MemoryFileSystem();
|
||||
late FlutterRepoSnippetConfiguration configuration;
|
||||
late SnippetGenerator generator;
|
||||
late Directory tmpDir;
|
||||
|
||||
void writeSkeleton(String type) {
|
||||
switch (type) {
|
||||
case 'dartpad':
|
||||
configuration.getHtmlSkeletonFile('dartpad').writeAsStringSync('''
|
||||
<div>HTML Bits (DartPad-style)</div>
|
||||
<iframe class="snippet-dartpad" src="https://dartpad.dev/embed-flutter.html?split=60&run=true&sample_id={{id}}&sample_channel={{channel}}"></iframe>
|
||||
<div>More HTML Bits</div>
|
||||
''');
|
||||
case 'sample':
|
||||
case 'snippet':
|
||||
configuration.getHtmlSkeletonFile(type).writeAsStringSync('''
|
||||
<div>HTML Bits</div>
|
||||
{{description}}
|
||||
<pre>{{code}}</pre>
|
||||
<pre>{{app}}</pre>
|
||||
<div>More HTML Bits</div>
|
||||
''');
|
||||
}
|
||||
}
|
||||
|
||||
setUp(() {
|
||||
// Create a new filesystem.
|
||||
memoryFileSystem = MemoryFileSystem();
|
||||
tmpDir = memoryFileSystem.systemTempDirectory
|
||||
.createTempSync('flutter_snippets_test.');
|
||||
configuration = FlutterRepoSnippetConfiguration(
|
||||
flutterRoot: memoryFileSystem
|
||||
.directory(path.join(tmpDir.absolute.path, 'flutter')),
|
||||
filesystem: memoryFileSystem);
|
||||
configuration.skeletonsDirectory.createSync(recursive: true);
|
||||
<String>['dartpad', 'sample', 'snippet'].forEach(writeSkeleton);
|
||||
FlutterInformation.instance =
|
||||
FakeFlutterInformation(configuration.flutterRoot);
|
||||
generator = SnippetGenerator(
|
||||
configuration: configuration,
|
||||
filesystem: memoryFileSystem,
|
||||
flutterRoot: configuration.skeletonsDirectory.parent);
|
||||
});
|
||||
|
||||
test('generates samples', () async {
|
||||
final File inputFile = memoryFileSystem
|
||||
.file(path.join(tmpDir.absolute.path, 'snippet_in.txt'))
|
||||
..createSync(recursive: true)
|
||||
..writeAsStringSync(r'''
|
||||
A description of the sample.
|
||||
|
||||
On several lines.
|
||||
|
||||
** 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 =
|
||||
SnippetDartdocParser(memoryFileSystem);
|
||||
const String sourcePath = 'packages/flutter/lib/src/widgets/foo.dart';
|
||||
const int sourceLine = 222;
|
||||
final SourceElement element = sampleParser.parseFromDartdocToolFile(
|
||||
inputFile,
|
||||
element: 'MyElement',
|
||||
startLine: sourceLine,
|
||||
sourceFile: memoryFileSystem.file(sourcePath),
|
||||
type: 'sample',
|
||||
);
|
||||
|
||||
expect(element.samples, isNotEmpty);
|
||||
element.samples.first.metadata.addAll(<String, Object?>{
|
||||
'channel': 'stable',
|
||||
});
|
||||
final String code = generator.generateCode(
|
||||
element.samples.first,
|
||||
output: outputFile,
|
||||
);
|
||||
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'''runApp(MaterialApp(title: 'foo'));'''));
|
||||
expect(html, isNot(contains('sample_channel=stable')));
|
||||
expect(
|
||||
html,
|
||||
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('void main() {'));
|
||||
});
|
||||
|
||||
test('generates snippets', () async {
|
||||
final File inputFile = memoryFileSystem
|
||||
.file(path.join(tmpDir.absolute.path, 'snippet_in.txt'))
|
||||
..createSync(recursive: true)
|
||||
..writeAsStringSync(r'''
|
||||
A description of the snippet.
|
||||
|
||||
On several lines.
|
||||
|
||||
```code
|
||||
void main() {
|
||||
print('The actual $name.');
|
||||
}
|
||||
```
|
||||
''');
|
||||
|
||||
final SnippetDartdocParser sampleParser =
|
||||
SnippetDartdocParser(memoryFileSystem);
|
||||
const String sourcePath = 'packages/flutter/lib/src/widgets/foo.dart';
|
||||
const int sourceLine = 222;
|
||||
final SourceElement element = sampleParser.parseFromDartdocToolFile(
|
||||
inputFile,
|
||||
element: 'MyElement',
|
||||
startLine: sourceLine,
|
||||
sourceFile: memoryFileSystem.file(sourcePath),
|
||||
type: 'snippet',
|
||||
);
|
||||
expect(element.samples, isNotEmpty);
|
||||
element.samples.first.metadata.addAll(<String, Object>{
|
||||
'channel': 'stable',
|
||||
});
|
||||
final String code = generator.generateCode(element.samples.first);
|
||||
expect(code, contains('// A description of the snippet.'));
|
||||
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('The actual $name.');'));
|
||||
expect(
|
||||
html,
|
||||
contains(
|
||||
'<div class="snippet-description">{@end-inject-html}A description of the snippet.\n\n'
|
||||
'On several lines.{@inject-html}</div>\n'));
|
||||
expect(html, contains('main() {'));
|
||||
});
|
||||
|
||||
test('generates dartpad samples', () async {
|
||||
final File inputFile = memoryFileSystem
|
||||
.file(path.join(tmpDir.absolute.path, 'snippet_in.txt'))
|
||||
..createSync(recursive: true)
|
||||
..writeAsStringSync(r'''
|
||||
A description of the snippet.
|
||||
|
||||
On several lines.
|
||||
|
||||
** 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);
|
||||
const String sourcePath = 'packages/flutter/lib/src/widgets/foo.dart';
|
||||
const int sourceLine = 222;
|
||||
final SourceElement element = sampleParser.parseFromDartdocToolFile(
|
||||
inputFile,
|
||||
element: 'MyElement',
|
||||
startLine: sourceLine,
|
||||
sourceFile: memoryFileSystem.file(sourcePath),
|
||||
type: 'dartpad',
|
||||
);
|
||||
expect(element.samples, isNotEmpty);
|
||||
element.samples.first.metadata.addAll(<String, Object>{
|
||||
'channel': 'stable',
|
||||
});
|
||||
final String code = generator.generateCode(element.samples.first);
|
||||
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>'));
|
||||
expect(
|
||||
html,
|
||||
contains(
|
||||
'<iframe class="snippet-dartpad" src="https://dartpad.dev/embed-flutter.html?split=60&run=true&sample_id=MyElement.0&sample_channel=stable"></iframe>\n'));
|
||||
});
|
||||
|
||||
test('generates sample metadata', () async {
|
||||
final File inputFile = memoryFileSystem
|
||||
.file(path.join(tmpDir.absolute.path, 'snippet_in.txt'))
|
||||
..createSync(recursive: true)
|
||||
..writeAsStringSync(r'''
|
||||
A description of the snippet.
|
||||
|
||||
On several lines.
|
||||
|
||||
```dart
|
||||
void main() {
|
||||
print('The actual $name.');
|
||||
}
|
||||
```
|
||||
''');
|
||||
|
||||
final File outputFile = memoryFileSystem
|
||||
.file(path.join(tmpDir.absolute.path, 'snippet_out.dart'));
|
||||
final File expectedMetadataFile = memoryFileSystem
|
||||
.file(path.join(tmpDir.absolute.path, 'snippet_out.json'));
|
||||
|
||||
final SnippetDartdocParser sampleParser =
|
||||
SnippetDartdocParser(memoryFileSystem);
|
||||
const String sourcePath = 'packages/flutter/lib/src/widgets/foo.dart';
|
||||
const int sourceLine = 222;
|
||||
final SourceElement element = sampleParser.parseFromDartdocToolFile(
|
||||
inputFile,
|
||||
element: 'MyElement',
|
||||
startLine: sourceLine,
|
||||
sourceFile: memoryFileSystem.file(sourcePath),
|
||||
type: 'sample',
|
||||
);
|
||||
expect(element.samples, isNotEmpty);
|
||||
element.samples.first.metadata
|
||||
.addAll(<String, Object>{'channel': 'stable'});
|
||||
generator.generateCode(element.samples.first, output: outputFile);
|
||||
expect(expectedMetadataFile.existsSync(), isTrue);
|
||||
final Map<String, dynamic> json =
|
||||
jsonDecode(expectedMetadataFile.readAsStringSync())
|
||||
as Map<String, dynamic>;
|
||||
expect(json['id'], equals('MyElement.0'));
|
||||
expect(json['channel'], equals('stable'));
|
||||
expect(json['file'], equals('snippet_out.dart'));
|
||||
expect(json['description'],
|
||||
equals('A description of the snippet.\n\nOn several lines.'));
|
||||
expect(json['sourcePath'],
|
||||
equals('packages/flutter/lib/src/widgets/foo.dart'));
|
||||
});
|
||||
});
|
||||
|
||||
group('snippets command line argument test', () {
|
||||
late MemoryFileSystem memoryFileSystem = MemoryFileSystem();
|
||||
late Directory tmpDir;
|
||||
late Directory flutterRoot;
|
||||
late FakeProcessManager fakeProcessManager;
|
||||
|
||||
setUp(() {
|
||||
fakeProcessManager = FakeProcessManager();
|
||||
memoryFileSystem = MemoryFileSystem();
|
||||
tmpDir = memoryFileSystem.systemTempDirectory
|
||||
.createTempSync('flutter_snippets_test.');
|
||||
flutterRoot = memoryFileSystem
|
||||
.directory(path.join(tmpDir.absolute.path, 'flutter'))
|
||||
..createSync(recursive: true);
|
||||
});
|
||||
|
||||
test('command line arguments are parsed and passed to generator', () {
|
||||
final FakePlatform platform = FakePlatform(environment: <String, String>{
|
||||
'PACKAGE_NAME': 'dart:ui',
|
||||
'LIBRARY_NAME': 'library',
|
||||
'ELEMENT_NAME': 'element',
|
||||
'FLUTTER_ROOT': flutterRoot.absolute.path,
|
||||
// The details here don't really matter other than the flutter root.
|
||||
'FLUTTER_VERSION': '''
|
||||
{
|
||||
"frameworkVersion": "2.5.0-6.0.pre.55",
|
||||
"channel": "use_snippets_pkg",
|
||||
"repositoryUrl": "git@github.com:flutter/flutter.git",
|
||||
"frameworkRevision": "fec4641e1c88923ecd6c969e2ff8a0dd12dc0875",
|
||||
"frameworkCommitDate": "2021-08-11 15:19:48 -0700",
|
||||
"engineRevision": "d8bbebed60a77b3d4fe9c840dc94dfbce159d951",
|
||||
"dartSdkVersion": "2.14.0 (build 2.14.0-393.0.dev)",
|
||||
"flutterRoot": "${flutterRoot.absolute.path}"
|
||||
}''',
|
||||
});
|
||||
final FlutterInformation flutterInformation = FlutterInformation(
|
||||
filesystem: memoryFileSystem,
|
||||
processManager: fakeProcessManager,
|
||||
platform: platform,
|
||||
);
|
||||
FlutterInformation.instance = flutterInformation;
|
||||
MockSnippetGenerator mockSnippetGenerator = MockSnippetGenerator();
|
||||
snippets_main.snippetGenerator = mockSnippetGenerator;
|
||||
String errorMessage = '';
|
||||
errorExit = (String message) {
|
||||
errorMessage = message;
|
||||
};
|
||||
|
||||
snippets_main.platform = platform;
|
||||
snippets_main.filesystem = memoryFileSystem;
|
||||
snippets_main.processManager = fakeProcessManager;
|
||||
final File input = memoryFileSystem
|
||||
.file(tmpDir.childFile('input.snippet'))
|
||||
..writeAsString('/// Test file');
|
||||
snippets_main.main(
|
||||
<String>['--input=${input.absolute.path}']);
|
||||
|
||||
final Map<String, dynamic> metadata =
|
||||
mockSnippetGenerator.sample.metadata;
|
||||
// Ignore the channel, because channel is really just the branch, and will be
|
||||
// different on development workstations.
|
||||
metadata.remove('channel');
|
||||
expect(
|
||||
metadata,
|
||||
equals(<String, dynamic>{
|
||||
'id': 'dart_ui.library.element',
|
||||
'element': 'element',
|
||||
'sourcePath': 'unknown.dart',
|
||||
'sourceLine': 1,
|
||||
'serial': '',
|
||||
'package': 'dart:ui',
|
||||
'library': 'library',
|
||||
}));
|
||||
|
||||
snippets_main.main(<String>[]);
|
||||
expect(
|
||||
errorMessage,
|
||||
equals(
|
||||
'The --input option must be specified, either on the command line, or in the INPUT environment variable.'));
|
||||
errorMessage = '';
|
||||
|
||||
snippets_main
|
||||
.main(<String>['--input=${input.absolute.path}', '--type=snippet']);
|
||||
expect(errorMessage, equals(''));
|
||||
errorMessage = '';
|
||||
|
||||
mockSnippetGenerator = MockSnippetGenerator();
|
||||
snippets_main.snippetGenerator = mockSnippetGenerator;
|
||||
snippets_main.main(<String>[
|
||||
'--input=${input.absolute.path}',
|
||||
'--type=snippet',
|
||||
'--no-format-output'
|
||||
]);
|
||||
expect(mockSnippetGenerator.formatOutput, equals(false));
|
||||
errorMessage = '';
|
||||
|
||||
input.deleteSync();
|
||||
snippets_main.main(
|
||||
<String>['--input=${input.absolute.path}']);
|
||||
expect(errorMessage,
|
||||
equals('The input file ${input.absolute.path} does not exist.'));
|
||||
errorMessage = '';
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
class MockSnippetGenerator extends SnippetGenerator {
|
||||
late CodeSample sample;
|
||||
File? output;
|
||||
String? copyright;
|
||||
String? description;
|
||||
late bool formatOutput;
|
||||
late bool addSectionMarkers;
|
||||
late bool includeAssumptions;
|
||||
|
||||
@override
|
||||
String generateCode(
|
||||
CodeSample sample, {
|
||||
File? output,
|
||||
String? copyright,
|
||||
String? description,
|
||||
bool formatOutput = true,
|
||||
bool addSectionMarkers = false,
|
||||
bool includeAssumptions = false,
|
||||
}) {
|
||||
this.sample = sample;
|
||||
this.output = output;
|
||||
this.copyright = copyright;
|
||||
this.description = description;
|
||||
this.formatOutput = formatOutput;
|
||||
this.addSectionMarkers = addSectionMarkers;
|
||||
this.includeAssumptions = includeAssumptions;
|
||||
|
||||
return '';
|
||||
}
|
||||
|
||||
@override
|
||||
String generateHtml(CodeSample sample) {
|
||||
return '';
|
||||
}
|
||||
}
|
87
dev/snippets/test/util_test.dart
Normal file
87
dev/snippets/test/util_test.dart
Normal file
@ -0,0 +1,87 @@
|
||||
// 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/memory.dart';
|
||||
import 'package:platform/platform.dart';
|
||||
import 'package:pub_semver/pub_semver.dart';
|
||||
import 'package:snippets/snippets.dart';
|
||||
import 'package:test/test.dart' hide TypeMatcher, isInstanceOf;
|
||||
|
||||
import 'fake_process_manager.dart';
|
||||
|
||||
const String testVersionInfo = r'''
|
||||
{
|
||||
"frameworkVersion": "2.5.0-2.0.pre.63",
|
||||
"channel": "master",
|
||||
"repositoryUrl": "git@github.com:flutter/flutter.git",
|
||||
"frameworkRevision": "9b2f6f7f9ab96bb3302f81b814a094f33023e79a",
|
||||
"frameworkCommitDate": "2021-07-28 13:03:40 -0700",
|
||||
"engineRevision": "0ed62a16f36348e97b2baadd8ccfec3825f80c5d",
|
||||
"dartSdkVersion": "2.14.0 (build 2.14.0-360.0.dev)",
|
||||
"flutterRoot": "/home/user/flutter"
|
||||
}
|
||||
''';
|
||||
|
||||
void main() {
|
||||
group('FlutterInformation', () {
|
||||
late FakeProcessManager fakeProcessManager;
|
||||
late FakePlatform fakePlatform;
|
||||
late MemoryFileSystem memoryFileSystem;
|
||||
late FlutterInformation flutterInformation;
|
||||
|
||||
setUp(() {
|
||||
fakeProcessManager = FakeProcessManager();
|
||||
memoryFileSystem = MemoryFileSystem();
|
||||
fakePlatform = FakePlatform(environment: <String, String>{});
|
||||
flutterInformation = FlutterInformation(
|
||||
filesystem: memoryFileSystem,
|
||||
processManager: fakeProcessManager,
|
||||
platform: fakePlatform,
|
||||
);
|
||||
});
|
||||
|
||||
test('calls out to flutter if FLUTTER_VERSION is not set', () async {
|
||||
fakeProcessManager.stdout = testVersionInfo;
|
||||
final Map<String, dynamic> info =
|
||||
flutterInformation.getFlutterInformation();
|
||||
expect(fakeProcessManager.runs, equals(1));
|
||||
expect(
|
||||
info['frameworkVersion'], equals(Version.parse('2.5.0-2.0.pre.63')));
|
||||
});
|
||||
test("doesn't call out to flutter if FLUTTER_VERSION is set", () async {
|
||||
fakePlatform.environment['FLUTTER_VERSION'] = testVersionInfo;
|
||||
final Map<String, dynamic> info =
|
||||
flutterInformation.getFlutterInformation();
|
||||
expect(fakeProcessManager.runs, equals(0));
|
||||
expect(
|
||||
info['frameworkVersion'], equals(Version.parse('2.5.0-2.0.pre.63')));
|
||||
});
|
||||
test('getFlutterRoot calls out to flutter if FLUTTER_ROOT is not set',
|
||||
() async {
|
||||
fakeProcessManager.stdout = testVersionInfo;
|
||||
final Directory root = flutterInformation.getFlutterRoot();
|
||||
expect(fakeProcessManager.runs, equals(1));
|
||||
expect(root.path, equals('/home/user/flutter'));
|
||||
});
|
||||
test("getFlutterRoot doesn't call out to flutter if FLUTTER_ROOT is set",
|
||||
() async {
|
||||
fakePlatform.environment['FLUTTER_ROOT'] = '/home/user/flutter';
|
||||
final Directory root = flutterInformation.getFlutterRoot();
|
||||
expect(fakeProcessManager.runs, equals(0));
|
||||
expect(root.path, equals('/home/user/flutter'));
|
||||
});
|
||||
test('parses version properly', () async {
|
||||
fakePlatform.environment['FLUTTER_VERSION'] = testVersionInfo;
|
||||
final Map<String, dynamic> info =
|
||||
flutterInformation.getFlutterInformation();
|
||||
expect(info['frameworkVersion'], isNotNull);
|
||||
expect(
|
||||
info['frameworkVersion'], equals(Version.parse('2.5.0-2.0.pre.63')));
|
||||
expect(info['dartSdkVersion'], isNotNull);
|
||||
expect(info['dartSdkVersion'], equals(Version.parse('2.14.0-360.0.dev')));
|
||||
});
|
||||
});
|
||||
}
|
@ -521,8 +521,8 @@ class DartdocGenerator {
|
||||
|
||||
final Version version = FlutterInformation.instance.getFlutterVersion();
|
||||
|
||||
// Verify which version of snippets and dartdoc we're using.
|
||||
final ProcessResult snippetsResult = processManager.runSync(
|
||||
// Verify which version of the global activated packages we're using.
|
||||
final ProcessResult versionResults = processManager.runSync(
|
||||
<String>[
|
||||
FlutterInformation.instance.getFlutterBinaryPath().path,
|
||||
'pub',
|
||||
@ -535,8 +535,8 @@ class DartdocGenerator {
|
||||
);
|
||||
print('');
|
||||
final Iterable<RegExpMatch> versionMatches =
|
||||
RegExp(r'^(?<name>snippets|dartdoc) (?<version>[^\s]+)', multiLine: true)
|
||||
.allMatches(snippetsResult.stdout as String);
|
||||
RegExp(r'^(?<name>dartdoc) (?<version>[^\s]+)', multiLine: true)
|
||||
.allMatches(versionResults.stdout as String);
|
||||
for (final RegExpMatch match in versionMatches) {
|
||||
print('${match.namedGroup('name')} version: ${match.namedGroup('version')}');
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user