blob: 2a6586d0ebdd347b938400a38fd0c423545de8ab [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/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_core.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,
required this.resolvedUnit,
this.flutter,
this.classDescription,
this.instanceCreation,
this.argumentExpression,
this.valueExpression,
this.parameterElement,
required 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)
..addNested();
}
Future<protocol.SourceChange?> changeValue(
protocol.FlutterWidgetPropertyValue value) async {
var edgeInsetsProperty = parent?._edgeInsetsProperty;
if (edgeInsetsProperty != null) {
return edgeInsetsProperty.changeValue(this, value);
}
var builder = ChangeBuilder(session: 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 builder.addDartFileEdit(resolvedUnit.path, (builder) {
_changeCode(builder, (builder) {
var expression = value.expression;
if (expression != null) {
builder.write(expression);
} else if (enumClassElement != null && enumValue != null) {
builder.writeReference(enumClassElement);
builder.write('.');
builder.write(enumValue.name);
} else {
var code = _toPrimitiveValueCode(value);
builder.write(code);
}
});
_formatEnclosingFunctionBody(builder);
});
return builder.sourceChange;
}
Future<protocol.SourceChange> removeValue() async {
var builder = ChangeBuilder(session: resolvedUnit.session);
final argumentExpression = this.argumentExpression;
final instanceCreation = this.instanceCreation;
if (argumentExpression != null && instanceCreation != 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 builder.addDartFileEdit(resolvedUnit.path, (builder) {
builder.addDeletion(
SourceRange(beginOffset, endOffset - beginOffset),
);
});
}
return builder.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 Function(DartEditBuilder builder) buildCode,
) {
final valueExpression = this.valueExpression;
if (valueExpression != null) {
builder.addReplacement(range.node(valueExpression), buildCode);
} else {
final parameterElement = this.parameterElement;
if (parameterElement == null) {
return;
}
var parameterName = parameterElement.name;
final instanceCreation = this.instanceCreation;
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 {
final parent = this.parent;
if (parent == null) {
return;
}
if (parent.virtualContainer != null) {
parent._changeCodeVirtualContainer(builder, parameterName, buildCode);
} else {
final classDescription = this.classDescription;
if (classDescription == null) {
return;
}
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 Function(DartEditBuilder builder) writeArgumentValue,
) {
final virtualContainer = this.virtualContainer;
if (virtualContainer == null) {
return;
}
var parentCreation = virtualContainer._parentCreation;
if (parentCreation != null) {
// `new Padding(...)` -> `Container(...)`
builder.addReplacement(
range.startEnd(
parentCreation,
parentCreation.constructorName,
),
(builder) {
builder.writeReference(virtualContainer.containerElement);
},
);
var existingArgument = virtualContainer._parentArgumentToMove;
if (existingArgument == null) {
return;
}
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() {
final parent = this.parent;
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}';
}
var code = value.doubleValue?.toStringAsFixed(1);
if (code != null) {
if (code.endsWith('.0')) {
code = code.substring(0, code.length - 2);
}
return code;
}
if (value.intValue != null) {
return '${value.intValue}';
}
code = value.stringValue;
if (code != null) {
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) {
final flutter = this.flutter;
var constructor = propertyExpression.constructorName.staticElement;
if (flutter != null &&
constructor != null &&
constructor.enclosingElement == classEdgeInsets) {
var arguments = propertyExpression.argumentList.arguments;
var constructorName = constructor.name;
if (constructorName == 'all') {
var expression = flutter.argumentByIndex(arguments, 0);
leftExpression = expression;
topExpression = expression;
rightExpression = expression;
bottomExpression = expression;
} else if (constructorName == 'fromLTRB') {
leftExpression = flutter.argumentByIndex(arguments, 0);
topExpression = flutter.argumentByIndex(arguments, 1);
rightExpression = flutter.argumentByIndex(arguments, 2);
bottomExpression = flutter.argumentByIndex(arguments, 3);
} else if (constructorName == '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 (constructorName == '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 builder = ChangeBuilder(session: property.resolvedUnit.session);
await builder.addDartFileEdit(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 builder.sourceChange;
}
PropertyDescription _addNestedProperty({
required String name,
required Expression? expression,
required 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;
} else if (expression is IntegerLiteral) {
return expression.value?.toDouble();
}
return null;
}
}