| // Copyright (c) 2018, 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/computer/computer_outline.dart'; |
| import 'package:analysis_server/src/protocol_server.dart' as protocol; |
| import 'package:analysis_server/src/protocol_server.dart'; |
| import 'package:analysis_server/src/utilities/flutter.dart'; |
| import 'package:analyzer/dart/ast/ast.dart'; |
| import 'package:analyzer/dart/ast/visitor.dart'; |
| import 'package:analyzer/dart/element/element.dart'; |
| import 'package:analyzer/dart/element/type.dart'; |
| import 'package:analyzer/src/generated/resolver.dart'; |
| import 'package:analyzer/src/generated/source.dart'; |
| |
| /// Computer for Flutter specific outlines. |
| class FlutterOutlineComputer { |
| static const CONSTRUCTOR_NAME = 'forDesignTime'; |
| |
| /// Code to append to the instrumented library code. |
| static const RENDER_APPEND = r''' |
| |
| final flutterDesignerWidgets = <int, Widget>{}; |
| |
| T _registerWidgetInstance<T extends Widget>(int id, T widget) { |
| flutterDesignerWidgets[id] = widget; |
| return widget; |
| } |
| '''; |
| |
| final String file; |
| final String content; |
| final LineInfo lineInfo; |
| final CompilationUnit unit; |
| final TypeProvider typeProvider; |
| |
| final List<protocol.FlutterOutline> _depthFirstOrder = []; |
| |
| int nextWidgetId = 0; |
| |
| /// This map is filled with information about widget classes that can be |
| /// rendered. Its keys are class name offsets. |
| final Map<int, _WidgetClass> widgets = {}; |
| |
| final List<protocol.SourceEdit> instrumentationEdits = []; |
| String instrumentedCode; |
| |
| FlutterOutlineComputer(this.file, this.content, this.lineInfo, this.unit) |
| : typeProvider = unit.element.context.typeProvider; |
| |
| protocol.FlutterOutline compute() { |
| protocol.Outline dartOutline = new DartUnitOutlineComputer( |
| file, lineInfo, unit, |
| withBasicFlutter: false) |
| .compute(); |
| |
| // Find widget classes. |
| // IDEA plugin only supports rendering widgets in libraries. |
| if (unit.element.source == unit.element.librarySource) { |
| _findWidgets(); |
| } |
| |
| // Convert Dart outlines into Flutter outlines. |
| var flutterDartOutline = _convert(dartOutline); |
| |
| // Create outlines for widgets. |
| unit.accept(new _FlutterOutlineBuilder(this)); |
| |
| // Compute instrumented code. |
| if (widgets.isNotEmpty) { |
| instrumentationEdits.sort((a, b) => b.offset - a.offset); |
| instrumentedCode = |
| SourceEdit.applySequence(content, instrumentationEdits); |
| instrumentedCode += RENDER_APPEND; |
| } |
| |
| return flutterDartOutline; |
| } |
| |
| /// If the given [argument] for the [parameter] can be represented as a |
| /// Flutter attribute, add it to the [attributes]. |
| void _addAttribute(List<protocol.FlutterOutlineAttribute> attributes, |
| Expression argument, ParameterElement parameter) { |
| if (parameter == null) { |
| return; |
| } |
| if (argument is NamedExpression) { |
| argument = (argument as NamedExpression).expression; |
| } |
| |
| String name = parameter.displayName; |
| |
| String label = content.substring(argument.offset, argument.end); |
| if (label.contains('\n')) { |
| label = '…'; |
| } |
| |
| if (argument is BooleanLiteral) { |
| attributes.add(new protocol.FlutterOutlineAttribute(name, label, |
| literalValueBoolean: argument.value)); |
| } else if (argument is IntegerLiteral) { |
| attributes.add(new protocol.FlutterOutlineAttribute(name, label, |
| literalValueInteger: argument.value)); |
| } else if (argument is StringLiteral) { |
| attributes.add(new protocol.FlutterOutlineAttribute(name, label, |
| literalValueString: argument.stringValue)); |
| } else { |
| if (argument is FunctionExpression) { |
| bool hasParameters = argument.parameters != null && |
| argument.parameters.parameters.isNotEmpty; |
| if (argument.body is ExpressionFunctionBody) { |
| label = hasParameters ? '(…) => …' : '() => …'; |
| } else { |
| label = hasParameters ? '(…) { … }' : '() { … }'; |
| } |
| } else if (argument is ListLiteral) { |
| label = '[…]'; |
| } else if (argument is MapLiteral) { |
| label = '{…}'; |
| } |
| attributes.add(new protocol.FlutterOutlineAttribute(name, label)); |
| } |
| } |
| |
| int _addInstrumentationEdits(Expression expression) { |
| int id = nextWidgetId++; |
| instrumentationEdits.add(new protocol.SourceEdit( |
| expression.offset, 0, '_registerWidgetInstance($id, ')); |
| instrumentationEdits.add(new protocol.SourceEdit(expression.end, 0, ')')); |
| return id; |
| } |
| |
| protocol.FlutterOutline _convert(protocol.Outline dartOutline) { |
| protocol.FlutterOutline flutterOutline = new protocol.FlutterOutline( |
| protocol.FlutterOutlineKind.DART_ELEMENT, |
| dartOutline.offset, |
| dartOutline.length, |
| dartElement: dartOutline.element); |
| if (dartOutline.children != null) { |
| flutterOutline.children = dartOutline.children.map(_convert).toList(); |
| } |
| |
| // Fill rendering information for widget classes. |
| if (dartOutline.element.kind == protocol.ElementKind.CLASS) { |
| var widget = widgets[dartOutline.element.location.offset]; |
| if (widget != null) { |
| flutterOutline.renderConstructor = CONSTRUCTOR_NAME; |
| flutterOutline.stateOffset = widget.state?.offset; |
| flutterOutline.stateLength = widget.state?.length; |
| } |
| } |
| |
| _depthFirstOrder.add(flutterOutline); |
| return flutterOutline; |
| } |
| |
| /// If the [node] is a supported Flutter widget creation, create a new |
| /// outline item for it. If the node is not a widget creation, but its type |
| /// is a Flutter Widget class subtype, and [withGeneric] is `true`, return |
| /// a widget reference outline item. |
| protocol.FlutterOutline _createOutline(Expression node, bool withGeneric) { |
| DartType type = node.staticType; |
| if (!isWidgetType(type)) { |
| return null; |
| } |
| String className = type.element.displayName; |
| |
| if (node is InstanceCreationExpression) { |
| int id = _addInstrumentationEdits(node); |
| |
| var attributes = <protocol.FlutterOutlineAttribute>[]; |
| var children = <protocol.FlutterOutline>[]; |
| for (var argument in node.argumentList.arguments) { |
| bool isWidgetArgument = isWidgetType(argument.staticType); |
| bool isWidgetListArgument = isListOfWidgetsType(argument.staticType); |
| |
| String parentAssociationLabel; |
| Expression childrenExpression; |
| |
| if (argument is NamedExpression) { |
| parentAssociationLabel = argument.name.label.name; |
| childrenExpression = argument.expression; |
| } else { |
| childrenExpression = argument; |
| } |
| |
| if (isWidgetArgument) { |
| var child = _createOutline(childrenExpression, true); |
| if (child != null) { |
| child.parentAssociationLabel = parentAssociationLabel; |
| children.add(child); |
| } |
| } else if (isWidgetListArgument) { |
| if (childrenExpression is ListLiteral) { |
| for (var element in childrenExpression.elements) { |
| var child = _createOutline(element, true); |
| if (child != null) { |
| children.add(child); |
| } |
| } |
| } |
| } else { |
| ParameterElement parameter = argument.staticParameterElement; |
| _addAttribute(attributes, argument, parameter); |
| } |
| } |
| |
| return new protocol.FlutterOutline( |
| protocol.FlutterOutlineKind.NEW_INSTANCE, node.offset, node.length, |
| className: className, |
| attributes: attributes, |
| children: children, |
| id: id); |
| } |
| |
| // A generic Widget typed expression. |
| if (withGeneric) { |
| var kind = protocol.FlutterOutlineKind.GENERIC; |
| |
| String variableName; |
| if (node is SimpleIdentifier) { |
| kind = protocol.FlutterOutlineKind.VARIABLE; |
| variableName = node.name; |
| } |
| |
| String label; |
| if (kind == protocol.FlutterOutlineKind.GENERIC) { |
| label = _getShortLabel(node); |
| } |
| |
| int id = _addInstrumentationEdits(node); |
| return new protocol.FlutterOutline(kind, node.offset, node.length, |
| className: className, |
| variableName: variableName, |
| label: label, |
| id: id); |
| } |
| |
| return null; |
| } |
| |
| /// Return the `State` declaration for the given `StatefulWidget` declaration. |
| /// Return `null` if cannot be found. |
| ClassDeclaration _findState(ClassDeclaration widget) { |
| MethodDeclaration createStateMethod = widget.members.firstWhere( |
| (method) => |
| method is MethodDeclaration && |
| method.name.name == 'createState' && |
| method.body != null, |
| orElse: () => null); |
| if (createStateMethod == null) { |
| return null; |
| } |
| |
| DartType stateType; |
| { |
| FunctionBody buildBody = createStateMethod.body; |
| if (buildBody is ExpressionFunctionBody) { |
| stateType = buildBody.expression.staticType; |
| } else if (buildBody is BlockFunctionBody) { |
| List<Statement> statements = buildBody.block.statements; |
| if (statements.isNotEmpty) { |
| Statement lastStatement = statements.last; |
| if (lastStatement is ReturnStatement) { |
| stateType = lastStatement.expression?.staticType; |
| } |
| } |
| } |
| } |
| if (stateType == null) { |
| return null; |
| } |
| |
| ClassElement stateElement; |
| if (stateType is InterfaceType && isState(stateType.element)) { |
| stateElement = stateType.element; |
| } else { |
| return null; |
| } |
| |
| for (var stateNode in unit.declarations) { |
| if (stateNode is ClassDeclaration && stateNode.element == stateElement) { |
| return stateNode; |
| } |
| } |
| |
| return null; |
| } |
| |
| /// Fill [widgets] with information about classes that can be rendered. |
| void _findWidgets() { |
| for (var widget in unit.declarations) { |
| if (widget is ClassDeclaration) { |
| int nameOffset = widget.name.offset; |
| |
| var designTimeConstructor = widget.getConstructor(CONSTRUCTOR_NAME); |
| if (designTimeConstructor == null) { |
| continue; |
| } |
| |
| InterfaceType superType = widget.element.supertype; |
| if (isExactlyStatelessWidgetType(superType)) { |
| widgets[nameOffset] = new _WidgetClass(nameOffset); |
| } else if (isExactlyStatefulWidgetType(superType)) { |
| ClassDeclaration state = _findState(widget); |
| if (state != null) { |
| widgets[nameOffset] = new _WidgetClass(nameOffset, state); |
| } |
| } |
| } |
| } |
| } |
| |
| String _getShortLabel(AstNode node) { |
| if (node is MethodInvocation) { |
| var buffer = new StringBuffer(); |
| |
| if (node.target != null) { |
| buffer.write(_getShortLabel(node.target)); |
| buffer.write('.'); |
| } |
| |
| buffer.write(node.methodName.name); |
| |
| if (node.argumentList == null || node.argumentList.arguments.isEmpty) { |
| buffer.write('()'); |
| } else { |
| buffer.write('(…)'); |
| } |
| |
| return buffer.toString(); |
| } |
| return node.toString(); |
| } |
| } |
| |
| class _FlutterOutlineBuilder extends GeneralizingAstVisitor<void> { |
| final FlutterOutlineComputer computer; |
| |
| _FlutterOutlineBuilder(this.computer); |
| |
| @override |
| void visitExpression(Expression node) { |
| var outline = computer._createOutline(node, false); |
| if (outline != null) { |
| for (var parent in computer._depthFirstOrder) { |
| if (parent.offset < outline.offset && |
| outline.offset + outline.length < parent.offset + parent.length) { |
| parent.children ??= <protocol.FlutterOutline>[]; |
| parent.children.add(outline); |
| return; |
| } |
| } |
| } else { |
| super.visitExpression(node); |
| } |
| } |
| } |
| |
| /// Information about a Widget class that can be rendered. |
| class _WidgetClass { |
| final int nameOffset; |
| |
| /// If a `StatefulWidget` with the `State` in the same file. |
| final ClassDeclaration state; |
| |
| _WidgetClass(this.nameOffset, [this.state]); |
| } |