| // 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; |
| } |
| } |