| // Copyright (c) 2014, the Dart project authors. Please see the AUTHORS file |
| // for details. 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:analysis_server/src/collections.dart'; |
| import 'package:analyzer/dart/analysis/results.dart'; |
| import 'package:analyzer/dart/ast/token.dart'; |
| import 'package:analyzer/dart/ast/visitor.dart'; |
| import 'package:analyzer/dart/element/element.dart' as engine; |
| import 'package:analyzer/src/dart/ast/ast.dart'; |
| import 'package:analyzer/src/utilities/extensions/flutter.dart'; |
| import 'package:analyzer_plugin/protocol/protocol_common.dart'; |
| |
| /// A computer for [CompilationUnit] outline. |
| class DartUnitOutlineComputer { |
| final ResolvedUnitResult resolvedUnit; |
| final bool withBasicFlutter; |
| |
| DartUnitOutlineComputer(this.resolvedUnit, {this.withBasicFlutter = false}); |
| |
| /// Returns the computed outline, not `null`. |
| Outline compute() { |
| var unit = resolvedUnit.unit; |
| var unitContents = <Outline>[]; |
| for (var unitMember in unit.declarations) { |
| if (unitMember is ClassDeclaration) { |
| unitContents.add( |
| _newClassOutline(unitMember, _outlinesForMembers(unitMember.members)), |
| ); |
| } else if (unitMember is MixinDeclaration) { |
| unitContents.add( |
| _newMixinOutline(unitMember, _outlinesForMembers(unitMember.members)), |
| ); |
| } else if (unitMember is EnumDeclaration) { |
| unitContents.add( |
| _newEnumOutline(unitMember, [ |
| for (var constant in unitMember.constants) |
| _newEnumConstant(constant), |
| ..._outlinesForMembers(unitMember.members), |
| ]), |
| ); |
| } else if (unitMember is ExtensionDeclaration) { |
| unitContents.add( |
| _newExtensionOutline( |
| unitMember, |
| _outlinesForMembers(unitMember.members), |
| ), |
| ); |
| } else if (unitMember is ExtensionTypeDeclaration) { |
| unitContents.add( |
| _newExtensionTypeOutline( |
| unitMember, |
| _outlinesForMembers(unitMember.members), |
| ), |
| ); |
| } else if (unitMember is TopLevelVariableDeclaration) { |
| var fieldDeclaration = unitMember; |
| var fields = fieldDeclaration.variables; |
| var fieldType = fields.type; |
| var fieldTypeName = _safeToSource(fieldType); |
| for (var field in fields.variables) { |
| unitContents.add( |
| _newVariableOutline( |
| fieldTypeName, |
| ElementKind.TOP_LEVEL_VARIABLE, |
| field, |
| false, |
| ), |
| ); |
| } |
| } else if (unitMember is FunctionDeclaration) { |
| var functionDeclaration = unitMember; |
| unitContents.add(_newFunctionOutline(functionDeclaration, true)); |
| } else if (unitMember is ClassTypeAlias) { |
| var alias = unitMember; |
| unitContents.add(_newClassTypeAlias(alias)); |
| } else if (unitMember is FunctionTypeAlias) { |
| var alias = unitMember; |
| unitContents.add(_newFunctionTypeAliasOutline(alias)); |
| } else if (unitMember is GenericTypeAlias) { |
| var alias = unitMember; |
| unitContents.add(_newGenericTypeAliasOutline(alias)); |
| } |
| } |
| var unitOutline = _newUnitOutline(unitContents); |
| return unitOutline; |
| } |
| |
| List<Outline> _addFunctionBodyOutlines(FunctionBody body) { |
| var contents = <Outline>[]; |
| body.accept(_FunctionBodyOutlinesVisitor(this, contents)); |
| return contents; |
| } |
| |
| Location _getLocationNode(AstNode node) { |
| var offset = node.offset; |
| var length = node.length; |
| return _getLocationOffsetLength(offset, length); |
| } |
| |
| Location _getLocationOffsetLength(int offset, int length) { |
| var path = resolvedUnit.path; |
| var startLocation = resolvedUnit.lineInfo.getLocation(offset); |
| var startLine = startLocation.lineNumber; |
| var startColumn = startLocation.columnNumber; |
| var endLocation = resolvedUnit.lineInfo.getLocation(offset + length); |
| var endLine = endLocation.lineNumber; |
| var endColumn = endLocation.columnNumber; |
| return Location( |
| path, |
| offset, |
| length, |
| startLine, |
| startColumn, |
| endLine: endLine, |
| endColumn: endColumn, |
| ); |
| } |
| |
| Location _getLocationToken(Token token) { |
| return _getLocationOffsetLength(token.offset, token.length); |
| } |
| |
| Outline _newClassOutline(ClassDeclaration node, List<Outline> classContents) { |
| var nameToken = node.name; |
| var name = nameToken.lexeme; |
| var element = Element( |
| ElementKind.CLASS, |
| name, |
| Element.makeFlags( |
| isPrivate: Identifier.isPrivateName(name), |
| isDeprecated: _hasDeprecated(node.metadata), |
| isAbstract: node.abstractKeyword != null, |
| ), |
| location: _getLocationToken(nameToken), |
| typeParameters: _getTypeParametersStr(node.typeParameters), |
| ); |
| return _nodeOutline(node, element, classContents); |
| } |
| |
| Outline _newClassTypeAlias(ClassTypeAlias node) { |
| var nameToken = node.name; |
| var name = nameToken.lexeme; |
| var element = Element( |
| ElementKind.CLASS_TYPE_ALIAS, |
| name, |
| Element.makeFlags( |
| isPrivate: Identifier.isPrivateName(name), |
| isDeprecated: _hasDeprecated(node.metadata), |
| isAbstract: node.abstractKeyword != null, |
| ), |
| location: _getLocationToken(nameToken), |
| typeParameters: _getTypeParametersStr(node.typeParameters), |
| ); |
| return _nodeOutline(node, element); |
| } |
| |
| Outline _newConstructorOutline(ConstructorDeclaration constructor) { |
| var returnType = constructor.returnType; |
| var name = returnType.name; |
| var offset = returnType.offset; |
| var length = returnType.length; |
| var constructorNameToken = constructor.name; |
| var isPrivate = false; |
| if (constructorNameToken != null) { |
| var constructorName = constructorNameToken.lexeme; |
| isPrivate = Identifier.isPrivateName(constructorName); |
| name += '.$constructorName'; |
| offset = constructorNameToken.offset; |
| length = constructorNameToken.length; |
| } |
| var parameters = constructor.parameters; |
| var parametersStr = _safeToSource(parameters); |
| var element = Element( |
| ElementKind.CONSTRUCTOR, |
| name, |
| Element.makeFlags( |
| isPrivate: isPrivate, |
| isDeprecated: _hasDeprecated(constructor.metadata), |
| ), |
| location: _getLocationOffsetLength(offset, length), |
| parameters: parametersStr, |
| ); |
| var contents = _addFunctionBodyOutlines(constructor.body); |
| return _nodeOutline(constructor, element, contents); |
| } |
| |
| Outline _newEnumConstant(EnumConstantDeclaration node) { |
| var nameToken = node.name; |
| var name = nameToken.lexeme; |
| var element = Element( |
| ElementKind.ENUM_CONSTANT, |
| name, |
| Element.makeFlags( |
| isPrivate: Identifier.isPrivateName(name), |
| isDeprecated: _hasDeprecated(node.metadata), |
| ), |
| location: _getLocationToken(nameToken), |
| ); |
| return _nodeOutline(node, element); |
| } |
| |
| Outline _newEnumOutline(EnumDeclaration node, List<Outline> children) { |
| var nameToken = node.name; |
| var name = nameToken.lexeme; |
| var element = Element( |
| ElementKind.ENUM, |
| name, |
| Element.makeFlags( |
| isPrivate: Identifier.isPrivateName(name), |
| isDeprecated: _hasDeprecated(node.metadata), |
| ), |
| location: _getLocationToken(nameToken), |
| ); |
| return _nodeOutline(node, element, children); |
| } |
| |
| Outline _newExtensionOutline( |
| ExtensionDeclaration node, |
| List<Outline> extensionContents, |
| ) { |
| var nameToken = node.name; |
| var name = nameToken?.lexeme ?? ''; |
| |
| Location? location; |
| if (nameToken != null) { |
| location = _getLocationToken(nameToken); |
| } else if (node.onClause case var onClause?) { |
| location = _getLocationNode(onClause.extendedType); |
| } |
| |
| var element = Element( |
| ElementKind.EXTENSION, |
| name, |
| Element.makeFlags( |
| isPrivate: Identifier.isPrivateName(name), |
| isDeprecated: _hasDeprecated(node.metadata), |
| ), |
| location: location, |
| typeParameters: _getTypeParametersStr(node.typeParameters), |
| ); |
| return _nodeOutline(node, element, extensionContents); |
| } |
| |
| Outline _newExtensionTypeOutline( |
| ExtensionTypeDeclaration node, |
| List<Outline> extensionContents, |
| ) { |
| var nameToken = node.name; |
| var name = nameToken.lexeme; |
| var element = Element( |
| ElementKind.EXTENSION_TYPE, |
| name, |
| Element.makeFlags( |
| isPrivate: Identifier.isPrivateName(name), |
| isDeprecated: _hasDeprecated(node.metadata), |
| ), |
| location: _getLocationToken(nameToken), |
| typeParameters: _getTypeParametersStr(node.typeParameters), |
| ); |
| return _nodeOutline(node, element, extensionContents); |
| } |
| |
| Outline _newFunctionOutline(FunctionDeclaration function, bool isStatic) { |
| var returnType = function.returnType; |
| var nameToken = function.name; |
| var name = nameToken.lexeme; |
| var functionExpression = function.functionExpression; |
| var parameters = functionExpression.parameters; |
| ElementKind kind; |
| if (function.isGetter) { |
| kind = ElementKind.GETTER; |
| } else if (function.isSetter) { |
| kind = ElementKind.SETTER; |
| } else { |
| kind = ElementKind.FUNCTION; |
| } |
| var parametersStr = _safeToSource(parameters); |
| var returnTypeStr = _safeToSource(returnType); |
| var element = Element( |
| kind, |
| name, |
| Element.makeFlags( |
| isPrivate: Identifier.isPrivateName(name), |
| isDeprecated: _hasDeprecated(function.metadata), |
| isStatic: isStatic, |
| ), |
| location: _getLocationToken(nameToken), |
| parameters: parametersStr, |
| returnType: returnTypeStr, |
| typeParameters: _getTypeParametersStr(functionExpression.typeParameters), |
| ); |
| var contents = _addFunctionBodyOutlines(functionExpression.body); |
| return _nodeOutline(function, element, contents); |
| } |
| |
| Outline _newFunctionTypeAliasOutline(FunctionTypeAlias node) { |
| var returnType = node.returnType; |
| var nameToken = node.name; |
| var name = nameToken.lexeme; |
| var parameters = node.parameters; |
| var parametersStr = _safeToSource(parameters); |
| var returnTypeStr = _safeToSource(returnType); |
| var element = Element( |
| ElementKind.FUNCTION_TYPE_ALIAS, |
| name, |
| Element.makeFlags( |
| isPrivate: Identifier.isPrivateName(name), |
| isDeprecated: _hasDeprecated(node.metadata), |
| ), |
| location: _getLocationToken(nameToken), |
| parameters: parametersStr, |
| returnType: returnTypeStr, |
| typeParameters: _getTypeParametersStr(node.typeParameters), |
| ); |
| return _nodeOutline(node, element); |
| } |
| |
| Outline _newGenericTypeAliasOutline(GenericTypeAlias node) { |
| var nameToken = node.name; |
| var name = nameToken.lexeme; |
| |
| var aliasedType = node.type; |
| var aliasedFunctionType = |
| aliasedType is GenericFunctionType ? aliasedType : null; |
| |
| var element = Element( |
| aliasedFunctionType != null |
| ? ElementKind.FUNCTION_TYPE_ALIAS |
| : ElementKind.TYPE_ALIAS, |
| name, |
| Element.makeFlags( |
| isPrivate: Identifier.isPrivateName(name), |
| isDeprecated: _hasDeprecated(node.metadata), |
| ), |
| aliasedType: _safeToSource(aliasedType), |
| location: _getLocationToken(nameToken), |
| parameters: |
| aliasedFunctionType != null |
| ? _safeToSource(aliasedFunctionType.parameters) |
| : null, |
| returnType: |
| aliasedFunctionType != null |
| ? _safeToSource(aliasedFunctionType.returnType) |
| : null, |
| typeParameters: _getTypeParametersStr(node.typeParameters), |
| ); |
| |
| return _nodeOutline(node, element); |
| } |
| |
| Outline _newMethodOutline(MethodDeclaration method) { |
| var returnType = method.returnType; |
| var nameToken = method.name; |
| var name = nameToken.lexeme; |
| var parameters = method.parameters; |
| ElementKind kind; |
| if (method.isGetter) { |
| kind = ElementKind.GETTER; |
| } else if (method.isSetter) { |
| kind = ElementKind.SETTER; |
| } else { |
| kind = ElementKind.METHOD; |
| } |
| var parametersStr = parameters?.toSource(); |
| var returnTypeStr = _safeToSource(returnType); |
| var element = Element( |
| kind, |
| name, |
| Element.makeFlags( |
| isPrivate: Identifier.isPrivateName(name), |
| isDeprecated: _hasDeprecated(method.metadata), |
| isAbstract: method.isAbstract, |
| isStatic: method.isStatic, |
| ), |
| location: _getLocationToken(nameToken), |
| parameters: parametersStr, |
| returnType: returnTypeStr, |
| typeParameters: _getTypeParametersStr(method.typeParameters), |
| ); |
| var contents = _addFunctionBodyOutlines(method.body); |
| return _nodeOutline(method, element, contents); |
| } |
| |
| Outline _newMixinOutline(MixinDeclaration node, List<Outline> mixinContents) { |
| node.firstTokenAfterCommentAndMetadata; |
| var nameToken = node.name; |
| var name = nameToken.lexeme; |
| var element = Element( |
| ElementKind.MIXIN, |
| name, |
| Element.makeFlags( |
| isPrivate: Identifier.isPrivateName(name), |
| isDeprecated: _hasDeprecated(node.metadata), |
| ), |
| location: _getLocationToken(nameToken), |
| typeParameters: _getTypeParametersStr(node.typeParameters), |
| ); |
| return _nodeOutline(node, element, mixinContents); |
| } |
| |
| Outline _newUnitOutline(List<Outline> unitContents) { |
| var unit = resolvedUnit.unit; |
| var element = Element( |
| ElementKind.COMPILATION_UNIT, |
| '<unit>', |
| Element.makeFlags(), |
| location: _getLocationNode(unit), |
| ); |
| return _nodeOutline(unit, element, unitContents); |
| } |
| |
| Outline _newVariableOutline( |
| String typeName, |
| ElementKind kind, |
| VariableDeclaration variable, |
| bool isStatic, |
| ) { |
| var nameToken = variable.name; |
| var name = nameToken.lexeme; |
| var element = Element( |
| kind, |
| name, |
| Element.makeFlags( |
| isPrivate: Identifier.isPrivateName(name), |
| isDeprecated: _hasDeprecated(variable.metadata), |
| isStatic: isStatic, |
| isConst: variable.isConst, |
| isFinal: variable.isFinal, |
| ), |
| location: _getLocationToken(nameToken), |
| returnType: typeName, |
| ); |
| return _nodeOutline(variable, element); |
| } |
| |
| Outline _nodeOutline( |
| AstNode node, |
| Element element, [ |
| List<Outline>? children, |
| ]) { |
| var offset = node.offset; |
| var end = node.end; |
| if (node is VariableDeclaration) { |
| var parent = node.parent; |
| var grandParent = parent?.parent; |
| if (grandParent != null && |
| parent is VariableDeclarationList && |
| parent.variables.isNotEmpty) { |
| if (parent.variables[0] == node) { |
| offset = grandParent.offset; |
| } |
| if (parent.variables.last == node) { |
| end = grandParent.end; |
| } |
| } |
| } |
| |
| var codeOffset = node.offset; |
| if (node is AnnotatedNode) { |
| codeOffset = node.firstTokenAfterCommentAndMetadata.offset; |
| } |
| |
| var length = end - offset; |
| var codeLength = node.end - codeOffset; |
| return Outline( |
| element, |
| offset, |
| length, |
| codeOffset, |
| codeLength, |
| children: nullIfEmpty(children), |
| ); |
| } |
| |
| List<Outline> _outlinesForMembers(List<ClassMember> members) { |
| var memberOutlines = <Outline>[]; |
| for (var classMember in members) { |
| if (classMember is ConstructorDeclaration) { |
| var constructorDeclaration = classMember; |
| memberOutlines.add(_newConstructorOutline(constructorDeclaration)); |
| } |
| if (classMember is FieldDeclaration) { |
| var fieldDeclaration = classMember; |
| var fields = fieldDeclaration.fields; |
| var fieldType = fields.type; |
| var fieldTypeName = _safeToSource(fieldType); |
| for (var field in fields.variables) { |
| memberOutlines.add( |
| _newVariableOutline( |
| fieldTypeName, |
| ElementKind.FIELD, |
| field, |
| fieldDeclaration.isStatic, |
| ), |
| ); |
| } |
| } |
| if (classMember is MethodDeclaration) { |
| var methodDeclaration = classMember; |
| memberOutlines.add(_newMethodOutline(methodDeclaration)); |
| } |
| } |
| return memberOutlines; |
| } |
| |
| static String? _getTypeParametersStr(TypeParameterList? parameters) { |
| if (parameters == null) { |
| return null; |
| } |
| return parameters.toSource(); |
| } |
| |
| /// Whether the list of [annotations] includes a `deprecated` annotation. |
| static bool _hasDeprecated(List<Annotation> annotations) { |
| for (var annotation in annotations) { |
| if (annotation.elementAnnotation?.isDeprecated ?? false) { |
| return true; |
| } |
| } |
| return false; |
| } |
| |
| static String _safeToSource(AstNode? node) => |
| node == null ? '' : node.toSource(); |
| } |
| |
| /// A visitor for building local function outlines. |
| class _FunctionBodyOutlinesVisitor extends RecursiveAstVisitor<void> { |
| final DartUnitOutlineComputer outlineComputer; |
| final List<Outline> contents; |
| |
| _FunctionBodyOutlinesVisitor(this.outlineComputer, this.contents); |
| |
| /// Return `true` if the given [element] is the method 'group' defined in the |
| /// test package. |
| bool isGroup(engine.ExecutableElement? element) { |
| if (element != null && element.metadata.hasIsTestGroup) { |
| return true; |
| } |
| return element is engine.TopLevelFunctionElement && |
| element.name == 'group' && |
| _isInsideTestPackage(element); |
| } |
| |
| /// Return `true` if the given [element] is the method 'test' defined in the |
| /// test package. |
| bool isTest(engine.ExecutableElement? element) { |
| if (element != null && element.metadata.hasIsTest) { |
| return true; |
| } |
| return element is engine.TopLevelFunctionElement && |
| element.name == 'test' && |
| _isInsideTestPackage(element); |
| } |
| |
| @override |
| void visitFunctionDeclaration(FunctionDeclaration node) { |
| contents.add(outlineComputer._newFunctionOutline(node, false)); |
| } |
| |
| @override |
| void visitInstanceCreationExpression(InstanceCreationExpression node) { |
| if (outlineComputer.withBasicFlutter && node.isWidgetCreation) { |
| var children = <Outline>[]; |
| node.argumentList.accept( |
| _FunctionBodyOutlinesVisitor(outlineComputer, children), |
| ); |
| |
| // The method `getWidgetPresentationText` should not return `null` when |
| // `isWidgetCreation` returns `true`. |
| var text = node.widgetPresentationText ?? '<unknown>'; |
| var element = Element( |
| ElementKind.CONSTRUCTOR_INVOCATION, |
| text, |
| 0, |
| location: outlineComputer._getLocationOffsetLength(node.offset, 0), |
| ); |
| |
| contents.add( |
| Outline( |
| element, |
| node.offset, |
| node.length, |
| node.offset, |
| node.length, |
| children: nullIfEmpty(children), |
| ), |
| ); |
| } else { |
| super.visitInstanceCreationExpression(node); |
| } |
| } |
| |
| @override |
| void visitMethodInvocation(MethodInvocation node) { |
| var nameNode = node.methodName; |
| |
| var nameElement = nameNode.element; |
| if (nameElement is! engine.ExecutableElement) { |
| return; |
| } |
| |
| String extractString(NodeList<Expression>? arguments) { |
| if (arguments != null && arguments.isNotEmpty) { |
| var argument = arguments[0]; |
| if (argument is StringLiteral) { |
| var value = argument.stringValue; |
| if (value != null) { |
| return value; |
| } |
| } |
| return argument.toSource(); |
| } |
| return 'unnamed'; |
| } |
| |
| void addOutlineNode(ElementKind kind, [List<Outline>? children]) { |
| var executableName = nameNode.name; |
| var description = extractString(node.argumentList.arguments); |
| var name = '$executableName("$description")'; |
| var element = Element( |
| kind, |
| name, |
| 0, |
| location: outlineComputer._getLocationNode(nameNode), |
| ); |
| contents.add( |
| Outline( |
| element, |
| node.offset, |
| node.length, |
| node.offset, |
| node.length, |
| children: nullIfEmpty(children), |
| ), |
| ); |
| } |
| |
| if (isGroup(nameElement)) { |
| var groupContents = <Outline>[]; |
| node.argumentList.accept( |
| _FunctionBodyOutlinesVisitor(outlineComputer, groupContents), |
| ); |
| addOutlineNode(ElementKind.UNIT_TEST_GROUP, groupContents); |
| } else if (isTest(nameElement)) { |
| addOutlineNode(ElementKind.UNIT_TEST_TEST); |
| } else { |
| super.visitMethodInvocation(node); |
| } |
| } |
| |
| /// Return `true` if the given [element] is a top-level member of the test |
| /// package. |
| bool _isInsideTestPackage(engine.TopLevelFunctionElement element) { |
| var parent = element.library; |
| return parent.firstFragment.source.fullName.endsWith('test.dart'); |
| } |
| } |