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