| // Copyright (c) 2017, 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/services/correction/strings.dart'; |
| import 'package:analyzer/dart/analysis/results.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/source.dart'; |
| import 'package:analyzer_plugin/utilities/change_builder/change_builder_dart.dart'; |
| |
| class Flutter { |
| static const _nameAlign = 'Align'; |
| static const _nameCenter = 'Center'; |
| static const _nameContainer = 'Container'; |
| static const _namePadding = 'Padding'; |
| static const _nameState = 'State'; |
| static const _nameStatefulWidget = 'StatefulWidget'; |
| static const _nameStatelessWidget = 'StatelessWidget'; |
| static const _nameStreamBuilder = 'StreamBuilder'; |
| static const _nameWidget = 'Widget'; |
| |
| static final mobile = Flutter._('flutter', 'package:flutter'); |
| static final web = Flutter._('flutter_web', 'package:flutter_web'); |
| |
| static final _uriFlutterMobileWidgets = |
| Uri.parse('package:flutter/widgets.dart'); |
| |
| static final _uriFlutterWebWidgets = |
| Uri.parse('package:flutter_web/widgets.dart'); |
| |
| final String packageName; |
| final String widgetsUri; |
| |
| final Uri _uriAlignment; |
| final Uri _uriAsync; |
| final Uri _uriBasic; |
| final Uri _uriContainer; |
| final Uri _uriEdgeInsets; |
| final Uri _uriFramework; |
| final Uri _uriWidgetsIcon; |
| final Uri _uriWidgetsText; |
| |
| factory Flutter.of(ResolvedUnitResult resolvedUnit) { |
| var uriConverter = resolvedUnit.session.uriConverter; |
| var isMobile = uriConverter.uriToPath(_uriFlutterMobileWidgets) != null; |
| var isWeb = uriConverter.uriToPath(_uriFlutterWebWidgets) != null; |
| |
| if (isMobile && isWeb) { |
| var visitor = _IdentifyMobileOrWeb(); |
| resolvedUnit.unit.accept(visitor); |
| isMobile = visitor.isMobile; |
| isWeb = visitor.isWeb; |
| } |
| |
| if (isMobile) { |
| return mobile; |
| } |
| |
| if (isWeb) { |
| return web; |
| } |
| |
| return mobile; |
| } |
| |
| Flutter._(this.packageName, String uriPrefix) |
| : widgetsUri = '$uriPrefix/widgets.dart', |
| _uriAlignment = Uri.parse('$uriPrefix/src/painting/alignment.dart'), |
| _uriAsync = Uri.parse('$uriPrefix/src/widgets/async.dart'), |
| _uriBasic = Uri.parse('$uriPrefix/src/widgets/basic.dart'), |
| _uriContainer = Uri.parse('$uriPrefix/src/widgets/container.dart'), |
| _uriEdgeInsets = Uri.parse('$uriPrefix/src/painting/edge_insets.dart'), |
| _uriFramework = Uri.parse('$uriPrefix/src/widgets/framework.dart'), |
| _uriWidgetsIcon = Uri.parse('$uriPrefix/src/widgets/icon.dart'), |
| _uriWidgetsText = Uri.parse('$uriPrefix/src/widgets/text.dart'); |
| |
| /** |
| * Return the argument with the given [index], or `null` if none. |
| */ |
| Expression argumentByIndex(List<Expression> arguments, int index) { |
| if (index < arguments.length) { |
| return arguments[index]; |
| } |
| return null; |
| } |
| |
| /** |
| * Return the named expression with the given [name], or `null` if none. |
| */ |
| NamedExpression argumentByName(List<Expression> arguments, String name) { |
| for (var argument in arguments) { |
| if (argument is NamedExpression && argument.name.label.name == name) { |
| return argument; |
| } |
| } |
| return null; |
| } |
| |
| void convertChildToChildren( |
| InstanceCreationExpression childArg, |
| NamedExpression namedExp, |
| String eol, |
| Function getNodeText, |
| Function getLinePrefix, |
| Function getIndent, |
| Function getText, |
| Function _addInsertEdit, |
| Function _addRemoveEdit, |
| Function _addReplaceEdit, |
| Function rangeNode) { |
| int childLoc = namedExp.offset + 'child'.length; |
| _addInsertEdit(childLoc, 'ren'); |
| int listLoc = childArg.offset; |
| String childArgSrc = getNodeText(childArg); |
| if (!childArgSrc.contains(eol)) { |
| _addInsertEdit(listLoc, '<Widget>['); |
| _addInsertEdit(listLoc + childArg.length, ']'); |
| } else { |
| int newlineLoc = childArgSrc.lastIndexOf(eol); |
| if (newlineLoc == childArgSrc.length) { |
| newlineLoc -= 1; |
| } |
| String indentOld = getLinePrefix(childArg.offset + 1 + newlineLoc); |
| String indentNew = '$indentOld${getIndent(1)}'; |
| // The separator includes 'child:' but that has no newlines. |
| String separator = |
| getText(namedExp.offset, childArg.offset - namedExp.offset); |
| String prefix = separator.contains(eol) ? "" : "$eol$indentNew"; |
| if (prefix.isEmpty) { |
| _addInsertEdit(namedExp.offset + 'child:'.length, ' <Widget>['); |
| _addRemoveEdit(new SourceRange(childArg.offset - 2, 2)); |
| } else { |
| _addInsertEdit(listLoc, '<Widget>['); |
| } |
| String newChildArgSrc = childArgSrc.replaceAll( |
| new RegExp("^$indentOld", multiLine: true), "$indentNew"); |
| newChildArgSrc = "$prefix$newChildArgSrc,$eol$indentOld]"; |
| _addReplaceEdit(rangeNode(childArg), newChildArgSrc); |
| } |
| } |
| |
| void convertChildToChildren2( |
| DartFileEditBuilder builder, |
| Expression childArg, |
| NamedExpression namedExp, |
| String eol, |
| Function getNodeText, |
| Function getLinePrefix, |
| Function getIndent, |
| Function getText, |
| Function rangeNode) { |
| int childLoc = namedExp.offset + 'child'.length; |
| builder.addSimpleInsertion(childLoc, 'ren'); |
| int listLoc = childArg.offset; |
| String childArgSrc = getNodeText(childArg); |
| if (!childArgSrc.contains(eol)) { |
| builder.addSimpleInsertion(listLoc, '<Widget>['); |
| builder.addSimpleInsertion(listLoc + childArg.length, ']'); |
| } else { |
| int newlineLoc = childArgSrc.lastIndexOf(eol); |
| if (newlineLoc == childArgSrc.length) { |
| newlineLoc -= 1; |
| } |
| String indentOld = getLinePrefix(childArg.offset + 1 + newlineLoc); |
| String indentNew = '$indentOld${getIndent(1)}'; |
| // The separator includes 'child:' but that has no newlines. |
| String separator = |
| getText(namedExp.offset, childArg.offset - namedExp.offset); |
| String prefix = separator.contains(eol) ? "" : "$eol$indentNew"; |
| if (prefix.isEmpty) { |
| builder.addSimpleInsertion( |
| namedExp.offset + 'child:'.length, ' <Widget>['); |
| builder.addDeletion(new SourceRange(childArg.offset - 2, 2)); |
| } else { |
| builder.addSimpleInsertion(listLoc, '<Widget>['); |
| } |
| String newChildArgSrc = childArgSrc.replaceAll( |
| new RegExp("^$indentOld", multiLine: true), "$indentNew"); |
| newChildArgSrc = "$prefix$newChildArgSrc,$eol$indentOld]"; |
| builder.addSimpleReplacement(rangeNode(childArg), newChildArgSrc); |
| } |
| } |
| |
| /** |
| * Return the named expression representing the `child` argument of the given |
| * [newExpr], or `null` if none. |
| */ |
| NamedExpression findChildArgument(InstanceCreationExpression newExpr) => |
| newExpr.argumentList.arguments |
| .firstWhere(isChildArgument, orElse: () => null); |
| |
| /** |
| * Return the named expression representing the `children` argument of the |
| * given [newExpr], or `null` if none. |
| */ |
| NamedExpression findChildrenArgument(InstanceCreationExpression newExpr) => |
| newExpr.argumentList.arguments |
| .firstWhere(isChildrenArgument, orElse: () => null); |
| |
| /** |
| * Return the Flutter instance creation expression that is the value of the |
| * 'child' argument of the given [newExpr], or null if none. |
| */ |
| InstanceCreationExpression findChildWidget( |
| InstanceCreationExpression newExpr) { |
| NamedExpression child = findChildArgument(newExpr); |
| return getChildWidget(child); |
| } |
| |
| /** |
| * Return the named expression with the given [name], or `null` if none. |
| */ |
| NamedExpression findNamedArgument( |
| InstanceCreationExpression creation, |
| String name, |
| ) { |
| var arguments = creation.argumentList.arguments; |
| return argumentByName(arguments, name); |
| } |
| |
| /** |
| * If the given [node] is a simple identifier, find the named expression whose |
| * name is the given [name] that is an argument to a Flutter instance creation |
| * expression. Return null if any condition cannot be satisfied. |
| */ |
| NamedExpression findNamedExpression(AstNode node, String name) { |
| if (node is! SimpleIdentifier) { |
| return null; |
| } |
| SimpleIdentifier namedArg = node; |
| NamedExpression namedExp; |
| if (namedArg.parent is Label && namedArg.parent.parent is NamedExpression) { |
| namedExp = namedArg.parent.parent; |
| if (namedArg.name != name || namedExp.expression == null) { |
| return null; |
| } |
| } else { |
| return null; |
| } |
| if (namedExp.parent?.parent is! InstanceCreationExpression) { |
| return null; |
| } |
| InstanceCreationExpression newExpr = namedExp.parent.parent; |
| if (newExpr == null || !isWidgetCreation(newExpr)) { |
| return null; |
| } |
| return namedExp; |
| } |
| |
| /** |
| * Return the expression that is a Flutter Widget that is the value of the |
| * given [child], or null if none. |
| */ |
| Expression getChildWidget(NamedExpression child) { |
| Expression expression = child?.expression; |
| if (isWidgetExpression(expression)) { |
| return expression; |
| } |
| return null; |
| } |
| |
| /** |
| * Return the presentation for the given Flutter `Widget` creation [node]. |
| */ |
| String getWidgetPresentationText(InstanceCreationExpression node) { |
| ClassElement element = node.staticElement?.enclosingElement; |
| if (!isWidget(element)) { |
| return null; |
| } |
| List<Expression> arguments = node.argumentList.arguments; |
| if (_isExactWidget(element, 'Icon', _uriWidgetsIcon)) { |
| if (arguments.isNotEmpty) { |
| String text = arguments[0].toString(); |
| String arg = shorten(text, 32); |
| return 'Icon($arg)'; |
| } else { |
| return 'Icon'; |
| } |
| } |
| if (_isExactWidget(element, 'Text', _uriWidgetsText)) { |
| if (arguments.isNotEmpty) { |
| String text = arguments[0].toString(); |
| String arg = shorten(text, 32); |
| return 'Text($arg)'; |
| } else { |
| return 'Text'; |
| } |
| } |
| return element.name; |
| } |
| |
| /** |
| * Return the instance creation expression that surrounds the given |
| * [node], if any, else null. The [node] may be the instance creation |
| * expression itself or the identifier that names the constructor. |
| */ |
| InstanceCreationExpression identifyNewExpression(AstNode node) { |
| InstanceCreationExpression newExpr; |
| if (node is SimpleIdentifier) { |
| if (node.parent is ConstructorName && |
| node.parent.parent is InstanceCreationExpression) { |
| newExpr = node.parent.parent; |
| } else if (node.parent?.parent is ConstructorName && |
| node.parent.parent?.parent is InstanceCreationExpression) { |
| newExpr = node.parent.parent.parent; |
| } |
| } else if (node is InstanceCreationExpression) { |
| newExpr = node; |
| } |
| return newExpr; |
| } |
| |
| /** |
| * Attempt to find and return the closest expression that encloses the [node] |
| * and is an independent Flutter `Widget`. Return `null` if nothing found. |
| */ |
| Expression identifyWidgetExpression(AstNode node) { |
| for (; node != null; node = node.parent) { |
| if (isWidgetExpression(node)) { |
| var parent = node.parent; |
| |
| if (node is AssignmentExpression) { |
| return null; |
| } |
| if (parent is AssignmentExpression) { |
| if (parent.rightHandSide == node) { |
| return node; |
| } |
| return null; |
| } |
| |
| if (parent is ArgumentList || |
| parent is ExpressionFunctionBody && parent.expression == node || |
| parent is ForElement && parent.body == node || |
| parent is IfElement && parent.thenElement == node || |
| parent is IfElement && parent.elseElement == node || |
| parent is ListLiteral || |
| parent is NamedExpression && parent.expression == node || |
| parent is Statement) { |
| return node; |
| } |
| } |
| if (node is ArgumentList || node is Statement || node is FunctionBody) { |
| return null; |
| } |
| } |
| return null; |
| } |
| |
| /** |
| * Return `true` is the given [argument] is the `child` argument. |
| */ |
| bool isChildArgument(Expression argument) => |
| argument is NamedExpression && argument.name.label.name == 'child'; |
| |
| /** |
| * Return `true` is the given [argument] is the `child` argument. |
| */ |
| bool isChildrenArgument(Expression argument) => |
| argument is NamedExpression && argument.name.label.name == 'children'; |
| |
| /** |
| * Return `true` if the given [type] is the dart.ui class `Color`, or its |
| * subtype. |
| */ |
| bool isColor(DartType type) { |
| if (type is! InterfaceType) { |
| return false; |
| } |
| |
| bool isColorElement(ClassElement element) { |
| if (element == null) { |
| return false; |
| } |
| |
| bool isExactColor(ClassElement element) => |
| element?.name == 'Color' && element.library.name == 'dart.ui'; |
| if (isExactColor(element)) { |
| return true; |
| } |
| for (var type in element.allSupertypes) { |
| if (isExactColor(type.element)) { |
| return true; |
| } |
| } |
| return false; |
| } |
| |
| return isColorElement(type.element); |
| } |
| |
| /// Return `true` if the [element] is the Flutter class `Alignment`. |
| bool isExactAlignment(ClassElement element) { |
| return _isExactWidget(element, 'Alignment', _uriAlignment); |
| } |
| |
| /// Return `true` if the [element] is the Flutter class `AlignmentDirectional`. |
| bool isExactAlignmentDirectional(ClassElement element) { |
| return _isExactWidget(element, 'AlignmentDirectional', _uriAlignment); |
| } |
| |
| /// Return `true` if the [element] is the Flutter class `AlignmentGeometry`. |
| bool isExactAlignmentGeometry(ClassElement element) { |
| return _isExactWidget(element, 'AlignmentGeometry', _uriAlignment); |
| } |
| |
| /// Return `true` if the [type] is the Flutter type `EdgeInsetsGeometry`. |
| bool isExactEdgeInsetsGeometryType(DartType type) { |
| return type is InterfaceType && |
| _isExactWidget(type.element, 'EdgeInsetsGeometry', _uriEdgeInsets); |
| } |
| |
| /** |
| * Return `true` if the [node] is creation of `Align`. |
| */ |
| bool isExactlyAlignCreation(InstanceCreationExpression node) { |
| var type = node?.staticType; |
| return isExactWidgetTypeAlign(type); |
| } |
| |
| /** |
| * Return `true` if the [node] is creation of `Container`. |
| */ |
| bool isExactlyContainerCreation(InstanceCreationExpression node) { |
| var type = node?.staticType; |
| return isExactWidgetTypeContainer(type); |
| } |
| |
| /** |
| * Return `true` if the [node] is creation of `Padding`. |
| */ |
| bool isExactlyPaddingCreation(InstanceCreationExpression node) { |
| var type = node?.staticType; |
| return isExactWidgetTypePadding(type); |
| } |
| |
| /** |
| * Return `true` if the given [type] is the Flutter class `StatefulWidget`. |
| */ |
| bool isExactlyStatefulWidgetType(DartType type) { |
| return type is InterfaceType && |
| _isExactWidget(type.element, _nameStatefulWidget, _uriFramework); |
| } |
| |
| /** |
| * Return `true` if the given [type] is the Flutter class `StatelessWidget`. |
| */ |
| bool isExactlyStatelessWidgetType(DartType type) { |
| return type is InterfaceType && |
| _isExactWidget(type.element, _nameStatelessWidget, _uriFramework); |
| } |
| |
| /// Return `true` if the given [element] is the Flutter class `State`. |
| bool isExactState(ClassElement element) { |
| return _isExactWidget(element, _nameState, _uriFramework); |
| } |
| |
| /** |
| * Return `true` if the given [type] is the Flutter class `Align`. |
| */ |
| bool isExactWidgetTypeAlign(DartType type) { |
| return type is InterfaceType && |
| _isExactWidget(type.element, _nameAlign, _uriBasic); |
| } |
| |
| /** |
| * Return `true` if the given [type] is the Flutter class `Center`. |
| */ |
| bool isExactWidgetTypeCenter(DartType type) { |
| return type is InterfaceType && |
| _isExactWidget(type.element, _nameCenter, _uriBasic); |
| } |
| |
| /** |
| * Return `true` if the given [type] is the Flutter class `Container`. |
| */ |
| bool isExactWidgetTypeContainer(DartType type) { |
| return type is InterfaceType && |
| _isExactWidget(type.element, _nameContainer, _uriContainer); |
| } |
| |
| /** |
| * Return `true` if the given [type] is the Flutter class `Padding`. |
| */ |
| bool isExactWidgetTypePadding(DartType type) { |
| return type is InterfaceType && |
| _isExactWidget(type.element, _namePadding, _uriBasic); |
| } |
| |
| /** |
| * Return `true` if the given [type] is the Flutter class `StreamBuilder`. |
| */ |
| bool isExactWidgetTypeStreamBuilder(DartType type) { |
| return type is InterfaceType && |
| _isExactWidget(type.element, _nameStreamBuilder, _uriAsync); |
| } |
| |
| /** |
| * Return `true` if the given [type] is the Flutter class `Widget`, or its |
| * subtype. |
| */ |
| bool isListOfWidgetsType(DartType type) { |
| return type is InterfaceType && |
| type.element.library.isDartCore && |
| type.element.name == 'List' && |
| type.typeArguments.length == 1 && |
| isWidgetType(type.typeArguments[0]); |
| } |
| |
| /** |
| * Return `true` if the given [type] is the dart.ui class `Color`, or its |
| * subtype. |
| */ |
| bool isMatrix4(DartType type) { |
| if (type is! InterfaceType) { |
| return false; |
| } |
| |
| bool isMatrix4Element(ClassElement element) { |
| if (element == null) { |
| return false; |
| } |
| |
| bool isExactMatrix4(ClassElement element) => |
| element?.name == 'Matrix4' && |
| element.library.name == 'vector_math_64'; |
| if (isExactMatrix4(element)) { |
| return true; |
| } |
| for (var type in element.allSupertypes) { |
| if (isExactMatrix4(type.element)) { |
| return true; |
| } |
| } |
| return false; |
| } |
| |
| return isMatrix4Element(type.element); |
| } |
| |
| /// Return `true` if the given [element] has the Flutter class `State` as |
| /// a superclass. |
| bool isState(ClassElement element) { |
| return _hasSupertype(element, _uriFramework, _nameState); |
| } |
| |
| /** |
| * Return `true` if the given [element] is a [ClassElement] that extends |
| * the Flutter class `StatefulWidget`. |
| */ |
| bool isStatefulWidgetDeclaration(Element element) { |
| if (element is ClassElement) { |
| return isExactlyStatefulWidgetType(element.supertype); |
| } |
| return false; |
| } |
| |
| /** |
| * Return `true` if the given [element] is the Flutter class `Widget`, or its |
| * subtype. |
| */ |
| bool isWidget(ClassElement element) { |
| if (element == null) { |
| return false; |
| } |
| if (_isExactWidget(element, _nameWidget, _uriFramework)) { |
| return true; |
| } |
| for (InterfaceType type in element.allSupertypes) { |
| if (_isExactWidget(type.element, _nameWidget, _uriFramework)) { |
| return true; |
| } |
| } |
| return false; |
| } |
| |
| /** |
| * Return `true` if the given [expr] is a constructor invocation for a |
| * class that has the Flutter class `Widget` as a superclass. |
| */ |
| bool isWidgetCreation(InstanceCreationExpression expr) { |
| ClassElement element = expr?.staticElement?.enclosingElement; |
| return isWidget(element); |
| } |
| |
| /** |
| * Return `true` if the given [node] is the Flutter class `Widget`, or its |
| * subtype. |
| */ |
| bool isWidgetExpression(AstNode node) { |
| if (node == null) { |
| return false; |
| } |
| if (node.parent is TypeName || node.parent?.parent is TypeName) { |
| return false; |
| } |
| if (node.parent is ConstructorName) { |
| return false; |
| } |
| if (node is NamedExpression) { |
| return false; |
| } |
| if (node is Expression) { |
| return isWidgetType(node.staticType); |
| } |
| return false; |
| } |
| |
| /** |
| * Return `true` if the given [type] is the Flutter class `Widget`, or its |
| * subtype. |
| */ |
| bool isWidgetType(DartType type) { |
| return type is InterfaceType && isWidget(type.element); |
| } |
| |
| /// Return `true` if the given [element] has a supertype with the [requiredName] |
| /// defined in the file with the [requiredUri]. |
| bool _hasSupertype( |
| ClassElement element, Uri requiredUri, String requiredName) { |
| if (element == null) { |
| return false; |
| } |
| for (InterfaceType type in element.allSupertypes) { |
| if (type.name == requiredName) { |
| Uri uri = type.element.source.uri; |
| if (uri == requiredUri) { |
| return true; |
| } |
| } |
| } |
| return false; |
| } |
| |
| /** |
| * Return `true` if the given [element] is the exact [type] defined in the |
| * file with the given [uri]. |
| */ |
| bool _isExactWidget(ClassElement element, String type, Uri uri) { |
| return element != null && element.name == type && element.source.uri == uri; |
| } |
| } |
| |
| class _IdentifyMobileOrWeb extends GeneralizingAstVisitor<void> { |
| bool isMobile = false; |
| bool isWeb = false; |
| |
| @override |
| void visitExpression(Expression node) { |
| if (isMobile || isWeb) { |
| return; |
| } |
| |
| if (Flutter.mobile.isWidgetExpression(node)) { |
| isMobile = true; |
| return; |
| } |
| |
| if (Flutter.web.isWidgetExpression(node)) { |
| isWeb = true; |
| return; |
| } |
| |
| super.visitExpression(node); |
| } |
| } |