blob: b7324477a86133ea1ba2274e5091a7295c9b04f9 [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/correction_utils.dart';
import 'package:analysis_server_plugin/edit/fix/dart_fix_context.dart';
import 'package:analysis_server_plugin/src/utilities/selection.dart';
import 'package:analyzer/dart/analysis/code_style_options.dart';
import 'package:analyzer/dart/analysis/results.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/ast.dart';
import 'package:analyzer/src/dart/ast/utilities.dart';
import 'package:analyzer/src/dart/element/type.dart';
import 'package:analyzer/src/generated/engine.dart';
import 'package:analyzer/src/utilities/extensions/ast.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/fixes/fixes.dart';
import 'package:meta/meta.dart';
/// How broadly a [CorrectionProducer] can be applied.
///
/// Each value of this enum is cumulative, as the index increases, except for
/// [CorrectionApplicability.automaticallyButOncePerFile]. When a correctiopn
/// producer has a given applicability, it also can be applied with each
/// lower-indexed value. For example, a correction producer with an
/// applicability of [CorrectionApplicability.acrossFiles] can also be used to
/// apply corrections across a single file
/// ([CorrectionApplicability.singleLocation]) and at a single location
/// ([CorrectionApplicability.singleLocation]). The [CorrectionProducer] getters
/// reflect this property: [CorrectionProducer.canBeAppliedAcrossSingleFile],
/// [CorrectionProducer.canBeAppliedAcrossFiles],
/// [CorrectionProducer.canBeAppliedAutomatically].
///
/// Note that [CorrectionApplicability.automaticallyButOncePerFile] is the one
/// value that does not have this cumulative property.
enum CorrectionApplicability {
/// Indicates a correction can be applied only at a specific location.
///
/// A correction with this applicability is not applicable across a file,
/// across multiple files, or valid to be applied automatically.
singleLocation,
/// Indicates a correction can be applied in multiple positions in the same
/// file.
///
/// A correction with this applicability is also applicable at a specific
/// location, but not applicable across multiple files, or valid to be applied
/// automatically.
///
/// This flag is used to provide the option for a user to fix a specific
/// diagnostic across a file (such as a quick fix to "fix all _something_ in
/// this file").
acrossSingleFile,
/// Indicates a correction can be applied across in bulk across multiple files
/// and/or at the same time as applying fixes from other producers.
///
/// A correction with this applicability is also applicable at a specific
/// location, and across a file, but not valid to be applied automatically.
///
/// Cases where this is not applicable 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.
acrossFiles,
/// Indicates a correction can be applied in multiple locations, even if not
/// chosen explicitly as a tool action, and can be applied to potentially
/// incomplete code.
///
/// A correction with this applicability is also applicable at a specific
/// location, and across a file, and across multiple files.
automatically,
/// Indicates a correction can be applied in multiple files, except only one
/// location per file; the correction can be applied even if not chosen
/// explicitly as a tool action, and can be applied to potentially incomplete
/// code.
///
/// A correction with this applicability is also applicable at a specific
/// location, and across multiple files.
automaticallyButOncePerFile,
}
/// An object that can compute a correction (fix or assist) in a Dart file.
sealed class CorrectionProducer<T extends ParsedUnitResult>
extends _AbstractCorrectionProducer<T> {
/// 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 [coveringNode] to access this field.
AstNode? _coveringNode;
CorrectionProducer({required super.context});
/// The applicability of this producer.
///
/// This property is to be implemented by each subclass, but outside code must
/// use other properties to determine a producer's applicability:
/// [canBeAppliedAcrossSingleFile], [canBeAppliedAcrossFiles], and
/// [canBeAppliedAutomatically].
@protected
CorrectionApplicability get applicability;
/// 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<String>? get assistArguments => null;
/// The assist kind that should be used to build an assist, or `null` if this
/// producer doesn't support assists.
AssistKind? get assistKind => null;
/// Whether this producer can be used to apply a correction in multiple
/// positions simultaneously in bulk across multiple files and/or at the same
/// time as applying corrections from other producers.
bool get canBeAppliedAcrossFiles =>
applicability == CorrectionApplicability.acrossFiles ||
applicability == CorrectionApplicability.automatically ||
applicability == CorrectionApplicability.automaticallyButOncePerFile;
/// Whether this producer can be used to apply a correction in multiple
/// positions simultaneously across a file.
bool get canBeAppliedAcrossSingleFile =>
applicability == CorrectionApplicability.acrossSingleFile ||
applicability == CorrectionApplicability.acrossFiles ||
applicability == CorrectionApplicability.automatically;
/// Whether this producer can be used to apply a correction automatically when
/// code could be incomplete, as well as in multiple positions simultaneously
/// in bulk across multiple files and/or at the same time as applying
/// corrections from other producers.
bool get canBeAppliedAutomatically =>
applicability == CorrectionApplicability.automatically ||
applicability == CorrectionApplicability.automaticallyButOncePerFile;
/// 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 coveringNode {
if (_coveringNode == null) {
var diagnostic = this.diagnostic;
if (diagnostic == null) {
return null;
}
var errorOffset = diagnostic.problemMessage.offset;
var errorLength = diagnostic.problemMessage.length;
_coveringNode =
NodeLocator2(errorOffset, math.max(errorOffset + errorLength - 1, 0))
.searchWithin(unit);
}
return _coveringNode;
}
/// The length of the source range associated with the error message being
/// fixed, or `null` if there is no diagnostic.
int? get errorLength => diagnostic?.problemMessage.length;
/// The offset of the source range associated with the error message being
/// fixed, or `null` if there is no diagnostic.
int? get errorOffset => diagnostic?.problemMessage.offset;
/// 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<String>? get fixArguments => null;
/// 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 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<String>? get multiFixArguments => null;
/// 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;
/// Computes the changes for this producer using [builder].
///
/// This method should not modify [fixKind].
Future<void> compute(ChangeBuilder builder);
}
final class CorrectionProducerContext {
final int _selectionOffset;
final int _selectionLength;
final CorrectionUtils _utils;
final AnalysisSessionHelper _sessionHelper;
final ParsedUnitResult _unitResult;
final DartFixContext? dartFixContext;
/// Whether the correction producers are run in the context of applying bulk
/// fixes.
final bool _applyingBulkFixes;
final Diagnostic? _diagnostic;
final AstNode node;
final Token _token;
CorrectionProducerContext._({
required ParsedUnitResult unitResult,
required bool applyingBulkFixes,
required this.dartFixContext,
required Diagnostic? diagnostic,
required this.node,
required Token token,
required int selectionOffset,
required int selectionLength,
}) : _unitResult = unitResult,
_sessionHelper = AnalysisSessionHelper(unitResult.session),
_utils = CorrectionUtils(unitResult),
_applyingBulkFixes = applyingBulkFixes,
_diagnostic = diagnostic,
_token = token,
_selectionOffset = selectionOffset,
_selectionLength = selectionLength;
String get path => _unitResult.path;
int get _selectionEnd => _selectionOffset + _selectionLength;
static CorrectionProducerContext createParsed({
required ParsedUnitResult resolvedResult,
bool applyingBulkFixes = false,
DartFixContext? dartFixContext,
Diagnostic? diagnostic,
int selectionOffset = -1,
int selectionLength = 0,
}) {
var selection = resolvedResult.unit.select(
offset: selectionOffset,
length: selectionLength,
);
var node = selection?.coveringNode;
node ??= resolvedResult.unit;
var token = _tokenAt(node, selectionOffset) ?? node.beginToken;
return CorrectionProducerContext._(
unitResult: resolvedResult,
node: node,
token: token,
applyingBulkFixes: applyingBulkFixes,
dartFixContext: dartFixContext,
diagnostic: diagnostic,
selectionOffset: selectionOffset,
selectionLength: selectionLength,
);
}
static CorrectionProducerContext createResolved({
required ResolvedUnitResult resolvedResult,
bool applyingBulkFixes = false,
DartFixContext? dartFixContext,
Diagnostic? diagnostic,
int selectionOffset = -1,
int selectionLength = 0,
}) {
var selectionEnd = selectionOffset + selectionLength;
var locator = NodeLocator(selectionOffset, selectionEnd);
var node = locator.searchWithin(resolvedResult.unit);
node ??= resolvedResult.unit;
var token = _tokenAt(node, selectionOffset) ?? node.beginToken;
return CorrectionProducerContext._(
unitResult: resolvedResult,
node: node,
token: token,
applyingBulkFixes: applyingBulkFixes,
dartFixContext: dartFixContext,
diagnostic: diagnostic,
selectionOffset: selectionOffset,
selectionLength: selectionLength,
);
}
static Token? _tokenAt(AstNode node, int offset) {
for (var entity in node.childEntities) {
if (entity is AstNode) {
if (entity.offset <= offset && offset <= entity.end) {
return _tokenAt(entity, offset);
}
} else if (entity is Token) {
if (entity.offset <= offset && offset <= entity.end) {
return entity;
}
}
}
return null;
}
}
abstract class CorrectionProducerWithDiagnostic
extends ResolvedCorrectionProducer {
CorrectionProducerWithDiagnostic({required super.context});
// 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<ResolvedUnitResult> {
MultiCorrectionProducer({required super.context});
CorrectionProducerContext get context => _context;
/// The library element for the library in which a correction is being
/// produced.
LibraryElement get libraryElement => unitResult.libraryElement;
/// The individual producers generated by this producer.
Future<List<ResolvedCorrectionProducer>> get producers;
@override
ResolvedUnitResult get unitResult => super.unitResult as ResolvedUnitResult;
}
/// A correction producer that can work on non-resolved units
/// ([ParsedUnitResult]s).
abstract class ParsedCorrectionProducer
extends CorrectionProducer<ParsedUnitResult> {
ParsedCorrectionProducer({required super.context});
}
/// A [CorrectionProducer] that can compute a correction (fix or assist) in a
/// Dart file using the resolved AST.
abstract class ResolvedCorrectionProducer
extends CorrectionProducer<ResolvedUnitResult> {
ResolvedCorrectionProducer({required super.context});
AnalysisOptionsImpl get analysisOptions =>
sessionHelper.session.analysisContext
.getAnalysisOptionsForFile(unitResult.file) as AnalysisOptionsImpl;
/// Whether [node] is in a static context.
bool get inStaticContext {
// Constructor initializers cannot reference `this`.
if (node.thisOrAncestorOfType<ConstructorInitializer>() != null) {
return true;
}
// Field initializers cannot reference `this`.
var fieldDeclaration = node.thisOrAncestorOfType<FieldDeclaration>();
if (fieldDeclaration != null) {
return fieldDeclaration.isStatic || !fieldDeclaration.fields.isLate;
}
// Static method.
var method = node.thisOrAncestorOfType<MethodDeclaration>();
return method != null && method.isStatic;
}
/// The library element for the library in which a correction is being
/// produced.
LibraryElement get libraryElement => unitResult.libraryElement;
TypeProvider get typeProvider => unitResult.typeProvider;
/// The type system appropriate to the library in which the correction is
/// requested.
TypeSystem get typeSystem => unitResult.typeSystem;
@override
ResolvedUnitResult get unitResult => super.unitResult as ResolvedUnitResult;
/// The type for the class `bool` from `dart:core`.
DartType get _coreTypeBool => typeProvider.boolType;
/// Returns the class declaration for the given [element], or `null` if there
/// is no such class.
Future<ClassDeclaration?> getClassDeclaration(ClassElement element) async {
var result = await sessionHelper.getElementDeclaration(element);
var node = result?.node;
if (node is ClassDeclaration) {
return node;
}
return null;
}
/// Returns the extension declaration for the given [element], or `null` if
/// there is no such extension.
Future<ExtensionDeclaration?> getExtensionDeclaration(
ExtensionElement element) async {
var result = await sessionHelper.getElementDeclaration(element);
var node = result?.node;
if (node is ExtensionDeclaration) {
return node;
}
return null;
}
/// Returns the extension type for the given [element], or `null` if there
/// is no such extension type.
Future<ExtensionTypeDeclaration?> getExtensionTypeDeclaration(
ExtensionTypeElement element) async {
var result = await sessionHelper.getElementDeclaration(element);
var node = result?.node;
if (node is ExtensionTypeDeclaration) {
return node;
}
return null;
}
/// Returns the mixin declaration for the given [element], or `null` if there
/// is no such mixin.
Future<MixinDeclaration?> getMixinDeclaration(MixinElement element) async {
var result = await sessionHelper.getElementDeclaration(element);
var node = result?.node;
if (node is MixinDeclaration) {
return node;
}
return null;
}
/// Returns the class element associated with the [target], or `null` if there
/// is no such element.
InterfaceElement? getTargetInterfaceElement(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 InterfaceElement) {
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 (expression is MethodInvocation) {
if (parent is CascadeExpression && parent.parent is ExpressionStatement) {
return VoidTypeImpl.instance;
}
if (parent is ExpressionStatement) {
return VoidTypeImpl.instance;
}
}
// `return myFunction();`.
if (parent is ReturnStatement) {
var executable = expression.enclosingExecutableElement;
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.expression == 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;
}
}
final class StubCorrectionProducerContext implements CorrectionProducerContext {
static final instance = StubCorrectionProducerContext._();
StubCorrectionProducerContext._();
@override
dynamic noSuchMethod(Invocation invocation) =>
throw UnimplementedError('Unimplemented: ${invocation.memberName}');
}
/// The behavior shared by [CorrectionProducer] and
/// [MultiCorrectionProducer].
sealed class _AbstractCorrectionProducer<T extends ParsedUnitResult> {
/// The context used to produce corrections.
final CorrectionProducerContext _context;
_AbstractCorrectionProducer({required CorrectionProducerContext context})
: _context = context;
/// Whether the fixes are being built for the bulk-fix request.
bool get applyingBulkFixes => _context._applyingBulkFixes;
/// The diagnostic being fixed, or `null` if this producer is being
/// used to produce an assist.
Diagnostic? get diagnostic => _context._diagnostic;
/// The EOL sequence to use for this [CompilationUnit].
String get eol => utils.endOfLine;
String get file => _context.path;
AstNode get node => _context.node;
ResourceProvider get resourceProvider => unitResult.session.resourceProvider;
int get selectionEnd => _context._selectionEnd;
int get selectionLength => _context._selectionLength;
int get selectionOffset => _context._selectionOffset;
AnalysisSessionHelper get sessionHelper => _context._sessionHelper;
Token get token => _context._token;
CompilationUnit get unit => _context._unitResult.unit;
ParsedUnitResult get unitResult => _context._unitResult;
CorrectionUtils get utils => _context._utils;
CodeStyleOptions getCodeStyleOptions(File file) =>
sessionHelper.session.analysisContext
.getAnalysisOptionsForFile(file)
.codeStyleOptions;
/// Returns 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;
}
/// Returns the text of the given [range] in the unit.
String getRangeText(SourceRange range) => utils.getRangeText(range);
/// Returns 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,
) =>
_context.dartFixContext!.getTopLevelDeclarations(baseName);
/// Returns whether 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;
}
/// Returns libraries with extensions that declare non-static public
/// extension members with the [memberName].
Stream<LibraryElement> librariesWithExtensions(String memberName) {
return _context.dartFixContext!.librariesWithExtensions(memberName);
}
}