flutter/dev/snippets/lib/src/analysis.dart
Greg Spencer 183bc15816
Move snippets package back into flutter repo (#147690)
## Description

This moves the snippets package back into the Flutter repo so that API documentation generation can happen without the use of `dart pub global run` because `pub run` doesn't handle concurrency well.

The change modifies the dartdoc building process to include building an executable from the snippets tool and installing that in the cache directory for use during docs generation.

The snippets tool will reside in dev/snippets, where it originally resided before being moved to https://github.com/flutter/assets-for-api-docs.

The snippets code itself is unchanged from the code that is in https://github.com/flutter/assets-for-api-docs/packages/snippets.

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

## Tests
 - Added snippets tests to the overall testing build.
2024-05-03 06:09:03 +00:00

362 lines
12 KiB
Dart

// 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;
}
}