blob: 0917e032ea79ffbfe152e6cd1507d16be4c08563 [file] [log] [blame]
// Copyright (c) 2019, 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/protocol/protocol_generated.dart' as protocol;
import 'package:analysis_server/src/protocol_server.dart' as protocol;
import 'package:analysis_server/src/services/flutter/class_description.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/token.dart';
import 'package:analyzer/dart/element/element.dart';
import 'package:analyzer/source/source_range.dart';
import 'package:analyzer/src/dart/analysis/session_helper.dart';
import 'package:analyzer/src/util/comment.dart';
import 'package:analyzer_plugin/utilities/change_builder/change_builder_dart.dart';
import 'package:analyzer_plugin/utilities/range_factory.dart';
String getFieldDocumentation(FieldElement field) {
var rawComment = field.documentationComment;
return getDartDocPlainText(rawComment);
}
String getParameterDocumentation(ParameterElement parameter) {
if (parameter is FieldFormalParameterElement) {
var rawComment = parameter.field.documentationComment;
return getDartDocPlainText(rawComment);
}
return null;
}
class PropertyDescription {
static int _nextPropertyId = 0;
final PropertyDescription parent;
/// The resolved unit, where the property value is.
final ResolvedUnitResult resolvedUnit;
/// The instance of [Flutter] support for the [resolvedUnit].
final Flutter flutter;
/// If the object that has this property is not materialized yet, so the
/// [instanceCreation] is `null`, the description of the object to
/// materialize.
final ClassDescription classDescription;
/// The instance creation of the object that has this property. Or `null`
/// if the object is not materialized yet, in this case [classDescription]
/// is set.
final InstanceCreationExpression instanceCreation;
/// Information about the `Container` property, which is not based on an
/// actual [instanceCreation] of the `Container` widget, i.e. is not
/// materialized.
final VirtualContainerProperty virtualContainer;
/// If the property is set, the full argument expression, might be a
/// [NamedExpression].
final Expression argumentExpression;
/// If the property is set, the value part of the argument expression,
/// the same as [argumentExpression] if a positional argument, or the
/// expression part of the [NamedExpression].
final Expression valueExpression;
/// The parameter element in the object constructor that is actually
/// invoked by [instanceCreation], or will be invoked when
/// [classDescription] is materialized.
final ParameterElement parameterElement;
/// Optional nested properties.
final List<PropertyDescription> children = [];
final protocol.FlutterWidgetProperty protocolProperty;
/// If this is a `EdgeInsets` typed property, the instance of helper.
/// Otherwise `null`.
_EdgeInsetsProperty _edgeInsetsProperty;
PropertyDescription({
this.parent,
this.resolvedUnit,
this.flutter,
this.classDescription,
this.instanceCreation,
this.argumentExpression,
this.valueExpression,
this.parameterElement,
this.protocolProperty,
this.virtualContainer,
});
String get name => protocolProperty.name;
/// This property has type `EdgeInsets`, add its nested properties.
void addEdgeInsetsNestedProperties(ClassElement classEdgeInsets) {
_edgeInsetsProperty = _EdgeInsetsProperty(classEdgeInsets, this);
_edgeInsetsProperty.addNested();
}
Future<protocol.SourceChange> changeValue(
protocol.FlutterWidgetPropertyValue value) async {
if (parent?._edgeInsetsProperty != null) {
return parent._edgeInsetsProperty.changeValue(this, value);
}
var changeBuilder = DartChangeBuilder(resolvedUnit.session);
ClassElement enumClassElement;
var enumValue = value.enumValue;
if (enumValue != null) {
var helper = AnalysisSessionHelper(resolvedUnit.session);
enumClassElement = await helper.getClass(
enumValue.libraryUri,
enumValue.className,
);
}
await changeBuilder.addFileEdit(resolvedUnit.path, (builder) {
_changeCode(builder, (builder) {
if (value.expression != null) {
builder.write(value.expression);
} else if (enumClassElement != null) {
builder.writeReference(enumClassElement);
builder.write('.');
builder.write(enumValue.name);
} else {
var code = _toPrimitiveValueCode(value);
builder.write(code);
}
});
_formatEnclosingFunctionBody(builder);
});
return changeBuilder.sourceChange;
}
Future<protocol.SourceChange> removeValue() async {
var changeBuilder = DartChangeBuilder(resolvedUnit.session);
if (argumentExpression != null) {
int endOffset;
var argumentList = instanceCreation.argumentList;
var arguments = argumentList.arguments;
var argumentIndex = arguments.indexOf(argumentExpression);
if (argumentIndex < arguments.length - 1) {
endOffset = arguments[argumentIndex + 1].offset;
} else {
endOffset = argumentList.rightParenthesis.offset;
}
var beginOffset = argumentExpression.offset;
await changeBuilder.addFileEdit(resolvedUnit.path, (builder) {
builder.addDeletion(
SourceRange(beginOffset, endOffset - beginOffset),
);
});
}
return changeBuilder.sourceChange;
}
void replaceChild(String name, PropertyDescription newChild) {
assert(newChild.parent == this);
for (var i = 0; i < children.length; i++) {
if (children[i].name == name) {
children[i] = newChild;
break;
}
}
}
void _changeCode(
DartFileEditBuilder builder,
void buildCode(DartEditBuilder builder),
) {
if (valueExpression != null) {
builder.addReplacement(range.node(valueExpression), buildCode);
} else {
var parameterName = parameterElement.name;
if (instanceCreation != null) {
var argumentList = instanceCreation.argumentList;
var insertOffset = 0;
for (var argument in argumentList.arguments) {
if (argument is NamedExpression) {
var argumentName = argument.name.label.name;
if (argumentName.compareTo(parameterName) > 0 ||
_isChildArgument(argument) ||
_isChildrenArgument(argument)) {
insertOffset = argument.offset;
break;
}
}
}
var needsLeadingComma = false;
if (insertOffset == 0) {
var rightParenthesis = argumentList.rightParenthesis;
insertOffset = rightParenthesis.offset;
var previous = rightParenthesis.previous;
if (previous.type != TokenType.COMMA &&
previous != argumentList.leftParenthesis) {
needsLeadingComma = true;
}
}
builder.addInsertion(insertOffset, (builder) {
if (needsLeadingComma) {
builder.write(', ');
}
builder.write(parameterName);
builder.write(': ');
buildCode(builder);
builder.write(', ');
});
} else {
if (parent.virtualContainer != null) {
parent._changeCodeVirtualContainer(builder, parameterName, buildCode);
} else {
parent._changeCode(builder, (builder) {
builder.writeReference(classDescription.element);
// TODO(scheglov) constructor name
builder.write('(');
builder.write(parameterName);
builder.write(': ');
buildCode(builder);
builder.write(', ');
builder.write(')');
});
}
}
}
}
void _changeCodeVirtualContainer(
DartFileEditBuilder builder,
String parameterName,
void writeArgumentValue(DartEditBuilder builder),
) {
if (virtualContainer._parentCreation != null) {
// `new Padding(...)` -> `Container(...)`
builder.addReplacement(
range.startEnd(
virtualContainer._parentCreation,
virtualContainer._parentCreation.constructorName,
),
(builder) {
builder.writeReference(virtualContainer.containerElement);
},
);
var existingArgument = virtualContainer._parentArgumentToMove;
var existingName = existingArgument.name.label.name;
int parameterOffset;
var leadingComma = false;
var trailingComma = false;
if (existingName.compareTo(parameterName) > 0) {
// `Container(padding: ..., child: ...)`
// ->
// `Container(alignment: ..., padding: ..., child: ...)`
parameterOffset = existingArgument.offset;
trailingComma = true;
} else {
// `Container(alignment: ..., child: ...)`
// ->
// `Container(alignment: ..., padding: ..., child: ...)`
parameterOffset = existingArgument.end;
leadingComma = true;
}
builder.addInsertion(
parameterOffset,
(builder) {
if (leadingComma) {
builder.write(', ');
}
builder.write(parameterName);
builder.write(': ');
writeArgumentValue(builder);
if (trailingComma) {
builder.write(', ');
}
},
);
} else {
builder.addInsertion(
virtualContainer.widgetCreation.offset,
(builder) {
builder.writeReference(virtualContainer.containerElement);
builder.write('(');
builder.write(parameterName);
builder.write(': ');
writeArgumentValue(builder);
builder.write(', ');
builder.write('child: ');
},
);
builder.addSimpleInsertion(virtualContainer.widgetCreation.end, ',)');
}
}
FunctionBody _enclosingFunctionBody() {
if (parent != null) {
return parent._enclosingFunctionBody();
}
var anchorExpr = virtualContainer?.widgetCreation ?? instanceCreation;
return anchorExpr.thisOrAncestorOfType<FunctionBody>();
}
void _formatEnclosingFunctionBody(DartFileEditBuilder builder) {
var functionBody = _enclosingFunctionBody();
builder.format(range.node(functionBody));
}
/// TODO(scheglov) Generalize to identifying by type.
bool _isChildArgument(NamedExpression argument) {
var argumentName = argument.name.label.name;
return argumentName == 'child';
}
/// TODO(scheglov) Generalize to identifying by type.
bool _isChildrenArgument(NamedExpression argument) {
var argumentName = argument.name.label.name;
return argumentName == 'children';
}
String _toPrimitiveValueCode(protocol.FlutterWidgetPropertyValue value) {
if (value.boolValue != null) {
return '${value.boolValue}';
}
if (value.doubleValue != null) {
var code = value.doubleValue.toStringAsFixed(1);
if (code.endsWith('.0')) {
code = code.substring(0, code.length - 2);
}
return code;
}
if (value.intValue != null) {
return '${value.intValue}';
}
if (value.stringValue != null) {
var code = value.stringValue;
if (code.contains("'")) {
code = code.replaceAll("'", r"\'");
}
return "'$code'";
}
throw StateError('Not a primitive value: $value');
}
static int nextId() => _nextPropertyId++;
}
/// Every widget has the `Container` property, either based of an actual
/// `Container` widget instance creation, or virtual, materialized when a
/// nested property is set.
///
/// This class provides information necessary for such materialization.
class VirtualContainerProperty {
final ClassElement containerElement;
final InstanceCreationExpression widgetCreation;
/// The existing wrapper around the widget, with semantic that is a subset
/// of the `Container` semantic, such as `Padding`. Such wrapper should be
/// replaced with full `Container` when `Container` is materialized.
///
/// Might be `null`, if no existing replacable wrapped.
InstanceCreationExpression _parentCreation;
/// The argument from the [_parentCreation] that should be moved into
/// the new `Container` creation during its materialization.
NamedExpression _parentArgumentToMove;
VirtualContainerProperty(
this.containerElement,
this.widgetCreation,
);
void setParentCreation(
InstanceCreationExpression parentCreation,
NamedExpression parentArgumentToMove,
) {
_parentCreation = parentCreation;
_parentArgumentToMove = parentArgumentToMove;
}
}
/// Support for `EdgeInsets` typed properties.
///
/// We try to generate nice looking code for `EdgeInsets` instances.
class _EdgeInsetsProperty {
final ClassElement classEdgeInsets;
/// The property that has type `EdgeInsets`.
final PropertyDescription property;
/// The constructor `EdgeInsets.only`.
ConstructorElement onlyConstructor;
double leftValue;
double topValue;
double rightValue;
double bottomValue;
PropertyDescription leftProperty;
PropertyDescription topProperty;
PropertyDescription rightProperty;
PropertyDescription bottomProperty;
_EdgeInsetsProperty(this.classEdgeInsets, this.property);
Flutter get flutter => property.flutter;
void addNested() {
Expression leftExpression;
Expression topExpression;
Expression rightExpression;
Expression bottomExpression;
var propertyExpression = property.valueExpression;
if (propertyExpression is InstanceCreationExpression) {
var constructor = propertyExpression.staticElement;
if (constructor?.enclosingElement == classEdgeInsets) {
var arguments = propertyExpression.argumentList.arguments;
if (constructor.name == 'all') {
var expression = flutter.argumentByIndex(arguments, 0);
leftExpression = expression;
topExpression = expression;
rightExpression = expression;
bottomExpression = expression;
} else if (constructor.name == 'fromLTRB') {
leftExpression = flutter.argumentByIndex(arguments, 0);
topExpression = flutter.argumentByIndex(arguments, 1);
rightExpression = flutter.argumentByIndex(arguments, 2);
bottomExpression = flutter.argumentByIndex(arguments, 3);
} else if (constructor.name == 'only') {
var leftArgument = flutter.argumentByName(arguments, 'left');
var topArgument = flutter.argumentByName(arguments, 'top');
var rightArgument = flutter.argumentByName(arguments, 'right');
var bottomArgument = flutter.argumentByName(arguments, 'bottom');
leftExpression = leftArgument?.expression;
topExpression = topArgument?.expression;
rightExpression = rightArgument?.expression;
bottomExpression = bottomArgument?.expression;
} else if (constructor.name == 'symmetric') {
var hArgument = flutter.argumentByName(arguments, 'horizontal');
var vArgument = flutter.argumentByName(arguments, 'vertical');
leftExpression = hArgument?.expression;
topExpression = vArgument?.expression;
rightExpression = hArgument?.expression;
bottomExpression = vArgument?.expression;
}
leftValue = _valueDouble(leftExpression);
topValue = _valueDouble(topExpression);
rightValue = _valueDouble(rightExpression);
bottomValue = _valueDouble(bottomExpression);
}
}
onlyConstructor = classEdgeInsets.getNamedConstructor('only');
leftProperty = _addNestedProperty(
name: 'left',
expression: leftExpression,
value: leftValue,
);
topProperty = _addNestedProperty(
name: 'top',
expression: topExpression,
value: topValue,
);
rightProperty = _addNestedProperty(
name: 'right',
expression: rightExpression,
value: rightValue,
);
bottomProperty = _addNestedProperty(
name: 'bottom',
expression: bottomExpression,
value: bottomValue,
);
}
/// The value of the [nested] property is changed, make changes to the
/// value of the [property] is a whole, to generate nice code.
Future<protocol.SourceChange> changeValue(
PropertyDescription nested,
protocol.FlutterWidgetPropertyValue value,
) async {
var doubleValue = value.doubleValue;
if (doubleValue == null) return null;
if (nested == leftProperty) {
leftValue = doubleValue;
} else if (nested == topProperty) {
topValue = doubleValue;
} else if (nested == rightProperty) {
rightValue = doubleValue;
} else if (nested == bottomProperty) {
bottomValue = doubleValue;
}
var leftCode = _toDoubleCode(leftValue);
var topCode = _toDoubleCode(topValue);
var rightCode = _toDoubleCode(rightValue);
var bottomCode = _toDoubleCode(bottomValue);
if (leftCode == '0' &&
topCode == '0' &&
rightCode == '0' &&
bottomCode == '0') {
return property.removeValue();
}
var changeBuilder = DartChangeBuilder(property.resolvedUnit.session);
await changeBuilder.addFileEdit(property.resolvedUnit.path, (builder) {
property._changeCode(builder, (builder) {
if (leftCode == rightCode && topCode == bottomCode) {
builder.writeReference(classEdgeInsets);
if (leftCode == topCode) {
builder.write('.all(');
builder.write(leftCode);
builder.write(')');
} else {
var hasHorizontal = false;
builder.write('.symmetric(');
if (leftCode != '0') {
builder.write('horizontal: ');
builder.write(leftCode);
hasHorizontal = true;
}
if (topCode != '0') {
if (hasHorizontal) {
builder.write(', ');
}
builder.write('vertical: ');
builder.write(topCode);
}
builder.write(')');
}
} else {
builder.writeReference(classEdgeInsets);
builder.write('.only(');
var needsComma = false;
if (leftCode != '0') {
builder.write('left: ');
builder.write(leftCode);
needsComma = true;
}
if (topCode != '0') {
if (needsComma) {
builder.write(', ');
}
builder.write('top: ');
builder.write(topCode);
needsComma = true;
}
if (rightCode != '0') {
if (needsComma) {
builder.write(', ');
}
builder.write('right: ');
builder.write(rightCode);
needsComma = true;
}
if (bottomCode != '0') {
if (needsComma) {
builder.write(', ');
}
builder.write('bottom: ');
builder.write(bottomCode);
needsComma = true;
}
builder.write(')');
}
});
property._formatEnclosingFunctionBody(builder);
});
return changeBuilder.sourceChange;
}
PropertyDescription _addNestedProperty({
String name,
Expression expression,
double value,
}) {
var parameter = onlyConstructor.parameters.singleWhere(
(p) => p.name == name,
);
var parameterDocumentation = getParameterDocumentation(parameter);
var nested = PropertyDescription(
parent: property,
resolvedUnit: property.resolvedUnit,
valueExpression: expression,
parameterElement: parameter,
protocolProperty: protocol.FlutterWidgetProperty(
PropertyDescription.nextId(),
true,
true,
name,
documentation: parameterDocumentation,
expression: _expressionCode(expression),
value: _protocolValueDouble(value),
editor: protocol.FlutterWidgetPropertyEditor(
protocol.FlutterWidgetPropertyEditorKind.DOUBLE,
),
),
);
property.children.add(nested);
return nested;
}
String _expressionCode(Expression expression) {
if (expression != null) {
var content = property.resolvedUnit.content;
return content.substring(expression.offset, expression.end);
}
return null;
}
static protocol.FlutterWidgetPropertyValue _protocolValueDouble(
double value,
) {
if (value != null) {
return protocol.FlutterWidgetPropertyValue(
doubleValue: value,
);
}
return null;
}
static String _toDoubleCode(double value) {
if (value == null) {
return '0';
}
var code = value.toStringAsFixed(1);
if (code.endsWith('.0')) {
code = code.substring(0, code.length - 2);
}
return code;
}
static double _valueDouble(Expression expression) {
if (expression is DoubleLiteral) {
return expression.value;
}
if (expression is IntegerLiteral) {
return expression.value.toDouble();
}
return null;
}
}