blob: 66bd21909edcf5ce5288987e89d8e8593cdbb121 [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 '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/dart/top_level_declarations.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/ast/token.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';
import 'package:meta/meta.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);
if (result.node is ClassOrMixinDeclaration) {
return result.node;
}
return null;
}
/// Return the extension declaration for the given [element].
Future<ExtensionDeclaration> getExtensionDeclaration(
ExtensionElement element) async {
var result = await sessionHelper.getElementDeclaration(element);
if (result.node is ExtensionDeclaration) {
return result.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;
if (rhs != null) {
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;
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;
AstNode _node;
CorrectionProducerContext({
@required this.resolvedResult,
@required this.workspace,
this.applyingBulkFixes = false,
this.dartFixContext,
this.diagnostic,
this.selectionOffset = -1,
this.selectionLength = 0,
}) : file = resolvedResult.path,
session = resolvedResult.session,
sessionHelper = AnalysisSessionHelper(resolvedResult.session),
typeProvider = resolvedResult.typeProvider,
selectionEnd = (selectionOffset ?? 0) + (selectionLength ?? 0),
unit = resolvedResult.unit,
utils = CorrectionUtils(resolvedResult);
AstNode get node => _node;
/// Return `true` if the lint with the given [name] is enabled.
bool isLintEnabled(String name) {
var analysisOptions = session.analysisContext.analysisOptions;
return analysisOptions.isLintEnabled(name);
}
bool setupCompute() {
final locator = NodeLocator(selectionOffset, selectionEnd);
_node = locator.searchWithin(resolvedResult.unit);
return _node != null;
}
}
/// 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.
Iterable<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 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?.message;
/// 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;
}
/// The behavior shared by [CorrectionProducer] and [MultiCorrectionProducer].
abstract class _AbstractCorrectionProducer {
/// The context used to produce corrections.
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();
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) {
var 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;
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 top-level declarations with the [name] in libraries that are
/// available to this context.
List<TopLevelDeclaration> getTopLevelDeclarations(String name) =>
_context.dartFixContext.getTopLevelDeclarations(name);
/// 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 `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;
}
/// Return `true` if the [node] might be a type name.
bool mightBeTypeIdentifier(AstNode node) {
if (node is SimpleIdentifier) {
var parent = node.parent;
if (parent is TypeName) {
return true;
}
return _isNameOfType(node.name);
}
return false;
}
/// 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;
}
}
}