// 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. // @dart = 2.9 import 'package:analysis_server/src/collections.dart'; import 'package:analysis_server/src/utilities/flutter.dart'; import 'package:analyzer/dart/analysis/results.dart'; import 'package:analyzer/dart/ast/ast.dart'; import 'package:analyzer/dart/ast/visitor.dart'; import 'package:analyzer/dart/element/element.dart' as engine; 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 unitContents = []; for (var unitMember in resolvedUnit.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) { var enumDeclaration = unitMember; var constantOutlines = []; for (var constant in enumDeclaration.constants) { constantOutlines.add(_newEnumConstant(constant)); } unitContents.add(_newEnumOutline(enumDeclaration, constantOutlines)); } else if (unitMember is ExtensionDeclaration) { unitContents.add(_newExtensionOutline( unitMember, _outlinesForMembers(unitMember.members))); } else if (unitMember is TopLevelVariableDeclaration) { var fieldDeclaration = unitMember; var fields = fieldDeclaration.variables; if (fields != null) { 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 _addFunctionBodyOutlines(FunctionBody body) { var contents = []; 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 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(resolvedUnit.path, offset, length, startLine, startColumn, endLine, endColumn); } Outline _newClassOutline(ClassDeclaration node, List classContents) { var nameNode = node.name; var name = nameNode.name; var element = Element( ElementKind.CLASS, name, Element.makeFlags( isPrivate: Identifier.isPrivateName(name), isDeprecated: _isDeprecated(node), isAbstract: node.isAbstract), location: _getLocationNode(nameNode), typeParameters: _getTypeParametersStr(node.typeParameters)); return _nodeOutline(node, element, classContents); } Outline _newClassTypeAlias(ClassTypeAlias node) { var nameNode = node.name; var name = nameNode.name; var element = Element( ElementKind.CLASS_TYPE_ALIAS, name, Element.makeFlags( isPrivate: Identifier.isPrivateName(name), isDeprecated: _isDeprecated(node), isAbstract: node.isAbstract), location: _getLocationNode(nameNode), 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 constructorNameNode = constructor.name; var isPrivate = false; if (constructorNameNode != null) { var constructorName = constructorNameNode.name; isPrivate = Identifier.isPrivateName(constructorName); name += '.$constructorName'; offset = constructorNameNode.offset; length = constructorNameNode.length; } var parameters = constructor.parameters; var parametersStr = _safeToSource(parameters); var element = Element( ElementKind.CONSTRUCTOR, name, Element.makeFlags( isPrivate: isPrivate, isDeprecated: _isDeprecated(constructor)), location: _getLocationOffsetLength(offset, length), parameters: parametersStr); var contents = _addFunctionBodyOutlines(constructor.body); return _nodeOutline(constructor, element, contents); } Outline _newEnumConstant(EnumConstantDeclaration node) { var nameNode = node.name; var name = nameNode.name; var element = Element( ElementKind.ENUM_CONSTANT, name, Element.makeFlags( isPrivate: Identifier.isPrivateName(name), isDeprecated: _isDeprecated(node)), location: _getLocationNode(nameNode)); return _nodeOutline(node, element); } Outline _newEnumOutline(EnumDeclaration node, List children) { var nameNode = node.name; var name = nameNode.name; var element = Element( ElementKind.ENUM, name, Element.makeFlags( isPrivate: Identifier.isPrivateName(name), isDeprecated: _isDeprecated(node)), location: _getLocationNode(nameNode)); return _nodeOutline(node, element, children); } Outline _newExtensionOutline( ExtensionDeclaration node, List extensionContents) { var nameNode = node.name; var name = nameNode?.name ?? ''; var element = Element( ElementKind.EXTENSION, name, Element.makeFlags( isPrivate: Identifier.isPrivateName(name), isDeprecated: _isDeprecated(node)), location: _getLocationNode(nameNode ?? node.extendedType), typeParameters: _getTypeParametersStr(node.typeParameters)); return _nodeOutline(node, element, extensionContents); } Outline _newFunctionOutline(FunctionDeclaration function, bool isStatic) { var returnType = function.returnType; var nameNode = function.name; var name = nameNode.name; 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: _isDeprecated(function), isStatic: isStatic), location: _getLocationNode(nameNode), 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 nameNode = node.name; var name = nameNode.name; 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: _isDeprecated(node)), location: _getLocationNode(nameNode), parameters: parametersStr, returnType: returnTypeStr, typeParameters: _getTypeParametersStr(node.typeParameters)); return _nodeOutline(node, element); } Outline _newGenericTypeAliasOutline(GenericTypeAlias node) { var nameNode = node.name; var name = nameNode.name; 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: _isDeprecated(node), ), aliasedType: _safeToSource(aliasedType), location: _getLocationNode(nameNode), 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 nameNode = method.name; var name = nameNode.name; 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: _isDeprecated(method), isAbstract: method.isAbstract, isStatic: method.isStatic), location: _getLocationNode(nameNode), parameters: parametersStr, returnType: returnTypeStr, typeParameters: _getTypeParametersStr(method.typeParameters)); var contents = _addFunctionBodyOutlines(method.body); return _nodeOutline(method, element, contents); } Outline _newMixinOutline(MixinDeclaration node, List mixinContents) { node.firstTokenAfterCommentAndMetadata; var nameNode = node.name; var name = nameNode.name; var element = Element( ElementKind.MIXIN, name, Element.makeFlags( isPrivate: Identifier.isPrivateName(name), isDeprecated: _isDeprecated(node)), location: _getLocationNode(nameNode), typeParameters: _getTypeParametersStr(node.typeParameters)); return _nodeOutline(node, element, mixinContents); } Outline _newUnitOutline(List unitContents) { var element = Element( ElementKind.COMPILATION_UNIT, '', Element.makeFlags(), location: _getLocationNode(resolvedUnit.unit)); return _nodeOutline(resolvedUnit.unit, element, unitContents); } Outline _newVariableOutline(String typeName, ElementKind kind, VariableDeclaration variable, bool isStatic) { var nameNode = variable.name; var name = nameNode.name; var element = Element( kind, name, Element.makeFlags( isPrivate: Identifier.isPrivateName(name), isDeprecated: _isDeprecated(variable), isStatic: isStatic, isConst: variable.isConst, isFinal: variable.isFinal), location: _getLocationNode(nameNode), returnType: typeName); return _nodeOutline(variable, element); } Outline _nodeOutline(AstNode node, Element element, [List children]) { var offset = node.offset; var end = node.end; if (node is VariableDeclaration) { var parent = node.parent; if (parent is VariableDeclarationList && parent.variables.isNotEmpty) { if (parent.variables[0] == node) { offset = parent.parent.offset; } if (parent.variables.last == node) { end = parent.parent.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 _outlinesForMembers(List members) { var memberOutlines = []; 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; if (fields != null) { 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(); } /// Returns `true` if the given [element] is not `null` and deprecated. static bool _isDeprecated(Declaration declaration) { var element = declaration.declaredElement; return element != null && element.hasDeprecated; } static String _safeToSource(AstNode node) => node == null ? '' : node.toSource(); } /// A visitor for building local function outlines. class _FunctionBodyOutlinesVisitor extends RecursiveAstVisitor { final DartUnitOutlineComputer outlineComputer; final List contents; _FunctionBodyOutlinesVisitor(this.outlineComputer, this.contents); Flutter get _flutter => Flutter.instance; /// Return `true` if the given [element] is the method 'group' defined in the /// test package. bool isGroup(engine.ExecutableElement element) { if (element != null && element.hasIsTestGroup) { return true; } return element is engine.FunctionElement && 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.hasIsTest) { return true; } return element is engine.FunctionElement && element.name == 'test' && _isInsideTestPackage(element); } @override void visitFunctionDeclaration(FunctionDeclaration node) { contents.add(outlineComputer._newFunctionOutline(node, false)); } @override void visitInstanceCreationExpression(InstanceCreationExpression node) { if (outlineComputer.withBasicFlutter && _flutter.isWidgetCreation(node)) { var children = []; node.argumentList .accept(_FunctionBodyOutlinesVisitor(outlineComputer, children)); var text = _flutter.getWidgetPresentationText(node); 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.staticElement; if (nameElement is! engine.ExecutableElement) { return; } engine.ExecutableElement executableElement = nameElement; String extractString(NodeList 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 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(executableElement)) { var groupContents = []; node.argumentList .accept(_FunctionBodyOutlinesVisitor(outlineComputer, groupContents)); addOutlineNode(ElementKind.UNIT_TEST_GROUP, groupContents); } else if (isTest(executableElement)) { 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.FunctionElement element) { var parent = element.enclosingElement; return parent is engine.CompilationUnitElement && parent.source.fullName.endsWith('test.dart'); } }