blob: 73a871ae427840c40e4b5dec024fed6bab5d7c3c [file] [log] [blame]
// 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;
}
}