| // 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 'dart:math' as math; |
| |
| import 'package:_fe_analyzer_shared/src/scanner/token.dart'; |
| import 'package:analysis_server/plugin/edit/fix/fix_dart.dart'; |
| import 'package:analysis_server/src/services/correction/fix/data_driven/transform_override_set.dart'; |
| import 'package:analysis_server/src/services/correction/util.dart'; |
| import 'package:analysis_server/src/utilities/flutter.dart'; |
| import 'package:analyzer/dart/analysis/results.dart'; |
| import 'package:analyzer/dart/analysis/session.dart'; |
| import 'package:analyzer/dart/ast/ast.dart'; |
| import 'package:analyzer/dart/element/element.dart'; |
| import 'package:analyzer/dart/element/type.dart'; |
| import 'package:analyzer/dart/element/type_provider.dart'; |
| import 'package:analyzer/dart/element/type_system.dart'; |
| import 'package:analyzer/diagnostic/diagnostic.dart'; |
| import 'package:analyzer/file_system/file_system.dart'; |
| import 'package:analyzer/source/source_range.dart'; |
| import 'package:analyzer/src/dart/analysis/session_helper.dart'; |
| import 'package:analyzer/src/dart/ast/utilities.dart'; |
| import 'package:analyzer/src/dart/element/type.dart'; |
| import 'package:analyzer_plugin/utilities/assist/assist.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/change_builder/change_workspace.dart'; |
| import 'package:analyzer_plugin/utilities/fixes/fixes.dart'; |
| import 'package:analyzer_plugin/utilities/range_factory.dart'; |
| |
| /// An object that can compute a correction (fix or assist) in a Dart file. |
| abstract class CorrectionProducer extends SingleCorrectionProducer { |
| /// Return the type for the class `bool` from `dart:core`. |
| DartType get coreTypeBool => resolvedResult.typeProvider.boolType; |
| |
| /// Returns `true` if [node] is in a static context. |
| bool get inStaticContext { |
| // constructor initializer cannot reference "this" |
| if (node.thisOrAncestorOfType<ConstructorInitializer>() != null) { |
| return true; |
| } |
| // field initializer cannot reference "this" |
| if (node.thisOrAncestorOfType<FieldDeclaration>() != null) { |
| return true; |
| } |
| // static method |
| var method = node.thisOrAncestorOfType<MethodDeclaration>(); |
| return method != null && method.isStatic; |
| } |
| |
| Future<void> compute(ChangeBuilder builder); |
| |
| /// Return the class, enum or mixin declaration for the given [element]. |
| Future<ClassOrMixinDeclaration?> getClassOrMixinDeclaration( |
| ClassElement element) async { |
| var result = await sessionHelper.getElementDeclaration(element); |
| var node = result?.node; |
| if (node is ClassOrMixinDeclaration) { |
| return node; |
| } |
| return null; |
| } |
| |
| /// Return the extension declaration for the given [element]. |
| Future<ExtensionDeclaration?> getExtensionDeclaration( |
| ExtensionElement element) async { |
| var result = await sessionHelper.getElementDeclaration(element); |
| var node = result?.node; |
| if (node is ExtensionDeclaration) { |
| return node; |
| } |
| return null; |
| } |
| |
| /// Return the class element associated with the [target], or `null` if there |
| /// is no such class element. |
| ClassElement? getTargetClassElement(Expression target) { |
| var type = target.staticType; |
| if (type is InterfaceType) { |
| return type.element; |
| } else if (target is Identifier) { |
| var element = target.staticElement; |
| if (element is ClassElement) { |
| return element; |
| } |
| } |
| return null; |
| } |
| |
| /// Returns an expected [DartType] of [expression], may be `null` if cannot be |
| /// inferred. |
| DartType? inferUndefinedExpressionType(Expression expression) { |
| var parent = expression.parent; |
| // myFunction(); |
| if (parent is ExpressionStatement) { |
| if (expression is MethodInvocation) { |
| return VoidTypeImpl.instance; |
| } |
| } |
| // return myFunction(); |
| if (parent is ReturnStatement) { |
| var executable = getEnclosingExecutableElement(expression); |
| return executable?.returnType; |
| } |
| // int v = myFunction(); |
| if (parent is VariableDeclaration) { |
| var variableDeclaration = parent; |
| if (variableDeclaration.initializer == expression) { |
| var variableElement = variableDeclaration.declaredElement; |
| if (variableElement != null) { |
| return variableElement.type; |
| } |
| } |
| } |
| // myField = 42; |
| if (parent is AssignmentExpression) { |
| var assignment = parent; |
| if (assignment.leftHandSide == expression) { |
| var rhs = assignment.rightHandSide; |
| return rhs.staticType; |
| } |
| } |
| // v = myFunction(); |
| if (parent is AssignmentExpression) { |
| var assignment = parent; |
| if (assignment.rightHandSide == expression) { |
| if (assignment.operator.type == TokenType.EQ) { |
| // v = myFunction(); |
| return assignment.writeType; |
| } else { |
| // v += myFunction(); |
| var method = assignment.staticElement; |
| if (method != null) { |
| var parameters = method.parameters; |
| if (parameters.length == 1) { |
| return parameters[0].type; |
| } |
| } |
| } |
| } |
| } |
| // v + myFunction(); |
| if (parent is BinaryExpression) { |
| var binary = parent; |
| var method = binary.staticElement; |
| if (method != null) { |
| if (binary.rightOperand == expression) { |
| var parameters = method.parameters; |
| return parameters.length == 1 ? parameters[0].type : null; |
| } |
| } |
| } |
| // foo( myFunction() ); |
| if (parent is ArgumentList) { |
| var parameter = expression.staticParameterElement; |
| return parameter?.type; |
| } |
| // bool |
| { |
| // assert( myFunction() ); |
| if (parent is AssertStatement) { |
| var statement = parent; |
| if (statement.condition == expression) { |
| return coreTypeBool; |
| } |
| } |
| // if ( myFunction() ) {} |
| if (parent is IfStatement) { |
| var statement = parent; |
| if (statement.condition == expression) { |
| return coreTypeBool; |
| } |
| } |
| // while ( myFunction() ) {} |
| if (parent is WhileStatement) { |
| var statement = parent; |
| if (statement.condition == expression) { |
| return coreTypeBool; |
| } |
| } |
| // do {} while ( myFunction() ); |
| if (parent is DoStatement) { |
| var statement = parent; |
| if (statement.condition == expression) { |
| return coreTypeBool; |
| } |
| } |
| // !myFunction() |
| if (parent is PrefixExpression) { |
| var prefixExpression = parent; |
| if (prefixExpression.operator.type == TokenType.BANG) { |
| return coreTypeBool; |
| } |
| } |
| // binary expression '&&' or '||' |
| if (parent is BinaryExpression) { |
| var binaryExpression = parent; |
| var operatorType = binaryExpression.operator.type; |
| if (operatorType == TokenType.AMPERSAND_AMPERSAND || |
| operatorType == TokenType.BAR_BAR) { |
| return coreTypeBool; |
| } |
| } |
| } |
| // we don't know |
| return null; |
| } |
| } |
| |
| class CorrectionProducerContext { |
| final int selectionOffset; |
| final int selectionLength; |
| final int selectionEnd; |
| |
| final CompilationUnit unit; |
| final CorrectionUtils utils; |
| final String file; |
| |
| final TypeProvider typeProvider; |
| |
| final AnalysisSession session; |
| final AnalysisSessionHelper sessionHelper; |
| final ResolvedUnitResult resolvedResult; |
| final ChangeWorkspace workspace; |
| |
| /// TODO(migration) Make it non-nullable, specialize "fix" context? |
| final DartFixContext? dartFixContext; |
| |
| /// A flag indicating whether the correction producers will be run in the |
| /// context of applying bulk fixes. |
| final bool applyingBulkFixes; |
| |
| final Diagnostic? diagnostic; |
| |
| final TransformOverrideSet? overrideSet; |
| |
| final AstNode node; |
| |
| CorrectionProducerContext._({ |
| required this.resolvedResult, |
| required this.workspace, |
| this.applyingBulkFixes = false, |
| this.dartFixContext, |
| this.diagnostic, |
| required this.node, |
| this.overrideSet, |
| this.selectionOffset = -1, |
| this.selectionLength = 0, |
| }) : file = resolvedResult.path, |
| session = resolvedResult.session, |
| sessionHelper = AnalysisSessionHelper(resolvedResult.session), |
| typeProvider = resolvedResult.typeProvider, |
| selectionEnd = selectionOffset + selectionLength, |
| unit = resolvedResult.unit, |
| utils = CorrectionUtils(resolvedResult); |
| |
| /// Return `true` if the lint with the given [name] is enabled. |
| bool isLintEnabled(String name) { |
| var analysisOptions = session.analysisContext.analysisOptions; |
| return analysisOptions.isLintEnabled(name); |
| } |
| |
| static CorrectionProducerContext? create({ |
| required ResolvedUnitResult resolvedResult, |
| required ChangeWorkspace workspace, |
| bool applyingBulkFixes = false, |
| DartFixContext? dartFixContext, |
| Diagnostic? diagnostic, |
| TransformOverrideSet? overrideSet, |
| int selectionOffset = -1, |
| int selectionLength = 0, |
| }) { |
| var selectionEnd = selectionOffset + selectionLength; |
| var locator = NodeLocator(selectionOffset, selectionEnd); |
| var node = locator.searchWithin(resolvedResult.unit); |
| node ??= resolvedResult.unit; |
| |
| return CorrectionProducerContext._( |
| resolvedResult: resolvedResult, |
| workspace: workspace, |
| node: node, |
| applyingBulkFixes: applyingBulkFixes, |
| dartFixContext: dartFixContext, |
| diagnostic: diagnostic, |
| overrideSet: overrideSet, |
| selectionOffset: selectionOffset, |
| selectionLength: selectionLength, |
| ); |
| } |
| } |
| |
| abstract class CorrectionProducerWithDiagnostic extends CorrectionProducer { |
| /// TODO(migration) Consider providing it via constructor. |
| @override |
| Diagnostic get diagnostic => super.diagnostic!; |
| } |
| |
| /// An object that can dynamically compute multiple corrections (fixes or |
| /// assists). |
| abstract class MultiCorrectionProducer extends _AbstractCorrectionProducer { |
| /// Return each of the individual producers generated by this producer. |
| Stream<CorrectionProducer> get producers; |
| } |
| |
| /// An object that can compute a correction (fix or assist) in a Dart file. |
| abstract class SingleCorrectionProducer extends _AbstractCorrectionProducer { |
| /// Return the arguments that should be used when composing the message for an |
| /// assist, or `null` if the assist message has no parameters or if this |
| /// producer doesn't support assists. |
| List<Object>? get assistArguments => null; |
| |
| /// Return the assist kind that should be used to build an assist, or `null` |
| /// if this producer doesn't support assists. |
| AssistKind? get assistKind => null; |
| |
| /// Return `true` if this producer can be used to fix diagnostics across |
| /// multiple files. Cases where this will return `false` include fixes for |
| /// which |
| /// - the modified regions can overlap, and |
| /// - fixes that have not been tested to ensure that they can be used this |
| /// way. |
| bool get canBeAppliedInBulk => false; |
| |
| /// Return `true` if this producer can be used to fix multiple diagnostics in |
| /// the same file. Cases where this will return `false` include fixes for |
| /// which |
| /// - the modified regions can overlap, |
| /// - the fix for one diagnostic would fix all diagnostics with the same code, |
| /// and, |
| /// - fixes that have not been tested to ensure that they can be used this |
| /// way. |
| /// |
| /// Producers that return `true` should return non-null values from both |
| /// [multiFixKind] and [multiFixArguments]. |
| bool get canBeAppliedToFile => false; |
| |
| /// Return the length of the error message being fixed, or `null` if there is |
| /// no diagnostic. |
| int? get errorLength => diagnostic?.problemMessage.length; |
| |
| /// Return the text of the error message being fixed, or `null` if there is |
| /// no diagnostic. |
| String? get errorMessage => |
| diagnostic?.problemMessage.messageText(includeUrl: true); |
| |
| /// Return the offset of the error message being fixed, or `null` if there is |
| /// no diagnostic. |
| int? get errorOffset => diagnostic?.problemMessage.offset; |
| |
| /// Return the arguments that should be used when composing the message for a |
| /// fix, or `null` if the fix message has no parameters or if this producer |
| /// doesn't support fixes. |
| List<Object>? get fixArguments => null; |
| |
| /// Return the fix kind that should be used to build a fix, or `null` if this |
| /// producer doesn't support fixes. |
| FixKind? get fixKind => null; |
| |
| /// Return the arguments that should be used when composing the message for a |
| /// multi-fix, or `null` if the fix message has no parameters or if this |
| /// producer doesn't support multi-fixes. |
| List<Object>? get multiFixArguments => null; |
| |
| /// Return the fix kind that should be used to build a multi-fix, or `null` if |
| /// this producer doesn't support multi-fixes. |
| FixKind? get multiFixKind => null; |
| } |
| |
| /// The behavior shared by [CorrectionProducer] and [MultiCorrectionProducer]. |
| abstract class _AbstractCorrectionProducer { |
| /// The context used to produce corrections. |
| /// TODO(migration) Make it not `late`, require in constructor. |
| late CorrectionProducerContext _context; |
| |
| /// The most deeply nested node that completely covers the highlight region of |
| /// the diagnostic, or `null` if there is no diagnostic, such a node does not |
| /// exist, or if it hasn't been computed yet. Use [coveredNode] to access this |
| /// field. |
| AstNode? _coveredNode; |
| |
| /// Initialize a newly created producer. |
| _AbstractCorrectionProducer(); |
| |
| /// Return `true` if the fixes are being built for the bulk-fix request. |
| bool get applyingBulkFixes => _context.applyingBulkFixes; |
| |
| /// The most deeply nested node that completely covers the highlight region of |
| /// the diagnostic, or `null` if there is no diagnostic or if such a node does |
| /// not exist. |
| AstNode? get coveredNode { |
| // TODO(brianwilkerson) Consider renaming this to `coveringNode`. |
| if (_coveredNode == null) { |
| final diagnostic = this.diagnostic; |
| if (diagnostic == null) { |
| return null; |
| } |
| var errorOffset = diagnostic.problemMessage.offset; |
| var errorLength = diagnostic.problemMessage.length; |
| _coveredNode = |
| NodeLocator2(errorOffset, math.max(errorOffset + errorLength - 1, 0)) |
| .searchWithin(unit); |
| } |
| return _coveredNode; |
| } |
| |
| /// Return the diagnostic being fixed, or `null` if this producer is being |
| /// used to produce an assist. |
| Diagnostic? get diagnostic => _context.diagnostic; |
| |
| /// Returns the EOL to use for this [CompilationUnit]. |
| String get eol => utils.endOfLine; |
| |
| String get file => _context.file; |
| |
| Flutter get flutter => Flutter.instance; |
| |
| /// Return the library element for the library in which a correction is being |
| /// produced. |
| LibraryElement get libraryElement => resolvedResult.libraryElement; |
| |
| AstNode get node => _context.node; |
| |
| /// Return the set of overrides to be applied to the transform set when |
| /// running tests, or `null` if there are no overrides to apply. |
| TransformOverrideSet? get overrideSet => _context.overrideSet; |
| |
| ResolvedUnitResult get resolvedResult => _context.resolvedResult; |
| |
| /// Return the resource provider used to access the file system. |
| ResourceProvider get resourceProvider => |
| resolvedResult.session.resourceProvider; |
| |
| int get selectionEnd => _context.selectionEnd; |
| |
| int get selectionLength => _context.selectionLength; |
| |
| int get selectionOffset => _context.selectionOffset; |
| |
| AnalysisSessionHelper get sessionHelper => _context.sessionHelper; |
| |
| TypeProvider get typeProvider => _context.typeProvider; |
| |
| /// Return the type system appropriate to the library in which the correction |
| /// was requested. |
| TypeSystem get typeSystem => _context.resolvedResult.typeSystem; |
| |
| CompilationUnit get unit => _context.unit; |
| |
| CorrectionUtils get utils => _context.utils; |
| |
| /// Configure this producer based on the [context]. |
| void configure(CorrectionProducerContext context) { |
| _context = context; |
| } |
| |
| /// Return the text that should be displayed to users when referring to the |
| /// given [type]. |
| String displayStringForType(DartType type) => type.getDisplayString( |
| withNullability: libraryElement.isNonNullableByDefault); |
| |
| /// Return the function body of the most deeply nested method or function that |
| /// encloses the [node], or `null` if the node is not in a method or function. |
| FunctionBody? getEnclosingFunctionBody() { |
| var closure = node.thisOrAncestorOfType<FunctionExpression>(); |
| if (closure != null) { |
| return closure.body; |
| } |
| var function = node.thisOrAncestorOfType<FunctionDeclaration>(); |
| if (function != null) { |
| return function.functionExpression.body; |
| } |
| var constructor = node.thisOrAncestorOfType<ConstructorDeclaration>(); |
| if (constructor != null) { |
| return constructor.body; |
| } |
| var method = node.thisOrAncestorOfType<MethodDeclaration>(); |
| if (method != null) { |
| return method.body; |
| } |
| return null; |
| } |
| |
| /// Return the text of the given [range] in the unit. |
| String getRangeText(SourceRange range) { |
| return utils.getRangeText(range); |
| } |
| |
| /// Return the mapping from a library (that is available to this context) to |
| /// a top-level declaration that is exported (not necessary declared) by this |
| /// library, and has the requested base name. For getters and setters the |
| /// corresponding top-level variable is returned. |
| Future<Map<LibraryElement, Element>> getTopLevelDeclarations( |
| String baseName, |
| ) { |
| return _context.dartFixContext!.getTopLevelDeclarations(baseName); |
| } |
| |
| /// Return `true` the lint with the given [name] is enabled. |
| bool isLintEnabled(String name) { |
| return _context.isLintEnabled(name); |
| } |
| |
| /// Return `true` if the selection covers an operator of the given |
| /// [binaryExpression]. |
| bool isOperatorSelected(BinaryExpression binaryExpression) { |
| AstNode left = binaryExpression.leftOperand; |
| AstNode right = binaryExpression.rightOperand; |
| // between the nodes |
| if (selectionOffset >= left.end && |
| selectionOffset + selectionLength <= right.offset) { |
| return true; |
| } |
| // or exactly select the node (but not with infix expressions) |
| if (selectionOffset == left.offset && |
| selectionOffset + selectionLength == right.end) { |
| if (left is BinaryExpression || right is BinaryExpression) { |
| return false; |
| } |
| return true; |
| } |
| // invalid selection (part of node, etc) |
| return false; |
| } |
| |
| /// Return libraries with extensions that declare non-static public |
| /// extension members with the [memberName]. |
| Stream<LibraryElement> librariesWithExtensions(String memberName) { |
| return _context.dartFixContext!.librariesWithExtensions(memberName); |
| } |
| |
| /// Return `true` if the given [node] is in a location where an implicit |
| /// constructor invocation would be allowed. |
| bool mightBeImplicitConstructor(AstNode node) { |
| if (node is SimpleIdentifier) { |
| var parent = node.parent; |
| if (parent is MethodInvocation) { |
| return parent.realTarget == null; |
| } |
| } |
| return false; |
| } |
| |
| /// If the [node] might be a type name, return its name. |
| String? nameOfType(AstNode node) { |
| if (node is SimpleIdentifier) { |
| var name = node.name; |
| if (node.parent is NamedType || _isNameOfType(name)) { |
| return name; |
| } |
| } |
| return null; |
| } |
| |
| /// Replace all occurrences of the [oldIndent] with the [newIndent] within the |
| /// [source]. |
| String replaceSourceIndent( |
| String source, String oldIndent, String newIndent) { |
| return source.replaceAll(RegExp('^$oldIndent', multiLine: true), newIndent); |
| } |
| |
| /// Return `true` if the given [expression] should be wrapped with parenthesis |
| /// when we want to use it as operand of a logical `and` expression. |
| bool shouldWrapParenthesisBeforeAnd(Expression expression) { |
| if (expression is BinaryExpression) { |
| var binary = expression; |
| var precedence = binary.operator.type.precedence; |
| return precedence < TokenClass.LOGICAL_AND_OPERATOR.precedence; |
| } |
| return false; |
| } |
| |
| /// Return `true` if the [name] is capitalized. |
| bool _isNameOfType(String name) { |
| if (name.isEmpty) { |
| return false; |
| } |
| var firstLetter = name.substring(0, 1); |
| if (firstLetter.toUpperCase() != firstLetter) { |
| return false; |
| } |
| return true; |
| } |
| } |
| |
| extension DartFileEditBuilderExtension on DartFileEditBuilder { |
| /// Add edits to the [builder] to remove any parentheses enclosing the |
| /// [expression]. |
| // TODO(brianwilkerson) Consider moving this to DartFileEditBuilder. |
| void removeEnclosingParentheses(Expression expression) { |
| var precedence = getExpressionPrecedence(expression); |
| while (expression.parent is ParenthesizedExpression) { |
| var parenthesized = expression.parent as ParenthesizedExpression; |
| if (getExpressionParentPrecedence(parenthesized) > precedence) { |
| break; |
| } |
| addDeletion(range.token(parenthesized.leftParenthesis)); |
| addDeletion(range.token(parenthesized.rightParenthesis)); |
| expression = parenthesized; |
| } |
| } |
| } |