// 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> 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> getDocumentationComments( Iterable elements) { return elements .where((SourceElement element) => element.comment.isNotEmpty) .map>((SourceElement element) => element.comment); } /// Gets an iterable over the comment [SourceElement]s in a file. Iterable getFileCommentElements(File file) { return getCommentElements(getFileElements(file)); } /// Filters the source `elements` to only return the comment elements. Iterable getCommentElements(Iterable 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 getElementsFromString(String content, File file) { final ParseStringResult parseResult = parseString( featureSet: FeatureSet.fromEnableFlags2( sdkLanguageVersion: FlutterInformation.instance.getDartSdkVersion(), flags: [], ), content: content); final _SourceVisitor visitor = _SourceVisitor(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 getFileElements(File file, {afs.ResourceProvider? resourceProvider}) { resourceProvider ??= afs.PhysicalResourceProvider.INSTANCE; final ParseStringResult parseResult = parseFile( featureSet: FeatureSet.fromEnableFlags2( sdkLanguageVersion: FlutterInformation.instance.getDartSdkVersion(), flags: [], ), path: file.absolute.path, resourceProvider: resourceProvider); final _SourceVisitor visitor = _SourceVisitor(file); visitor.visitCompilationUnit(parseResult.unit); visitor.assignLineNumbers(); return visitor.elements; } class _SourceVisitor extends RecursiveAstVisitor { _SourceVisitor(this.file) : elements = {}; final Set elements; String enclosingClass = ''; File file; void assignLineNumbers() { final String contents = file.readAsStringSync(); final LineInfo lineInfo = LineInfo.fromContent(contents); final Set removedElements = {}; final Set replacedElements = {}; for (final SourceElement element in elements) { final List newLines = []; 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 _processComment(String element, Comment comment) { final List result = []; 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 comment = []; 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 comment = []; 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 comment = []; 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 comment = []; 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 comment = []; // 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 comment = []; 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 comment = []; 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 comment = []; 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; } }