blob: 5e6682657054c39f352dc78f8327cf88979553ad [file] [log] [blame]
// 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]);
}