blob: 25156960b81b6522ddbf45d7a3fe15a568f0e121 [file] [log] [blame]
// 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/protocol_server.dart' hide Element;
import 'package:analysis_server/src/utilities/extensions/ast.dart';
import 'package:analysis_server_plugin/edit/correction_utils.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/element2.dart';
import 'package:analyzer/dart/element/nullability_suffix.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/src/dart/ast/utilities.dart';
import 'package:analyzer/src/dart/element/type.dart';
import 'package:analyzer/src/generated/java_core.dart';
import 'package:analyzer/src/utilities/extensions/ast.dart';
import 'package:analyzer_plugin/utilities/change_builder/change_builder_core.dart';
import 'package:analyzer_plugin/utilities/range_factory.dart';
import 'package:collection/collection.dart';
/// An enumeration of possible postfix completion kinds.
abstract final class DartPostfixCompletion {
static const NO_TEMPLATE = PostfixCompletionKind(
'',
'no change',
_false,
_null,
);
static const List<PostfixCompletionKind> ALL_TEMPLATES = [
PostfixCompletionKind(
'assert',
'expr.assert -> assert(expr);',
isAssertContext,
expandAssert,
),
PostfixCompletionKind(
'fori',
'limit.fori -> for(var i = 0; i < limit; i++) {}',
isIntContext,
expandFori,
),
PostfixCompletionKind(
'for',
'values.for -> for(var value in values) {}',
isIterableContext,
expandFor,
),
PostfixCompletionKind(
'iter',
'values.iter -> for(var value in values) {}',
isIterableContext,
expandFor,
),
PostfixCompletionKind(
'not',
'bool.not -> !bool',
isBoolContext,
expandNegate,
),
PostfixCompletionKind('!', 'bool! -> !bool', isBoolContext, expandNegate),
PostfixCompletionKind(
'else',
'bool.else -> if (!bool) {}',
isBoolContext,
expandElse,
),
PostfixCompletionKind(
'if',
'bool.if -> if (bool) {}',
isBoolContext,
expandIf,
),
PostfixCompletionKind(
'nn',
'expr.nn -> if (expr != null) {}',
isObjectContext,
expandNotNull,
),
PostfixCompletionKind(
'notnull',
'expr.notnull -> if (expr != null) {}',
isObjectContext,
expandNotNull,
),
PostfixCompletionKind(
'null',
'expr.null -> if (expr == null) {}',
isObjectContext,
expandNull,
),
PostfixCompletionKind(
'par',
'expr.par -> (expr)',
isObjectContext,
expandParen,
),
PostfixCompletionKind(
'return',
'expr.return -> return expr',
isObjectContext,
expandReturn,
),
PostfixCompletionKind(
'switch',
'expr.switch -> switch (expr) {}',
isSwitchContext,
expandSwitch,
),
PostfixCompletionKind(
'try',
'stmt.try -> try {stmt} catch (e,s) {}',
isStatementContext,
expandTry,
),
PostfixCompletionKind(
'tryon',
'stmt.try -> try {stmt} on Exception catch (e,s) {}',
isStatementContext,
expandTryon,
),
PostfixCompletionKind(
'while',
'expr.while -> while (expr) {}',
isBoolContext,
expandWhile,
),
];
static Future<PostfixCompletion?> expandAssert(
PostfixCompletionProcessor processor,
PostfixCompletionKind kind,
) async {
return processor.expand(kind, processor.findAssertExpression, (expr) {
return 'assert(${processor.utils.getNodeText(expr)});';
}, withBraces: false);
}
static Future<PostfixCompletion?> expandElse(
PostfixCompletionProcessor processor,
PostfixCompletionKind kind,
) async {
return processor.expand(
kind,
processor.findBoolExpression,
(expr) => 'if (${processor.makeNegatedBoolExpr(expr)})',
);
}
static Future<PostfixCompletion?> expandFor(
PostfixCompletionProcessor processor,
PostfixCompletionKind kind,
) async {
return processor.expand(kind, processor.findIterableExpression, (expr) {
var value = processor.newVariable('value');
return 'for (var $value in ${processor.utils.getNodeText(expr)})';
});
}
static Future<PostfixCompletion?> expandFori(
PostfixCompletionProcessor processor,
PostfixCompletionKind kind,
) async {
return processor.expand(kind, processor.findIntExpression, (expr) {
var index = processor.newVariable('i');
return 'for (int $index = 0; $index < ${processor.utils.getNodeText(expr)}; $index++)';
});
}
static Future<PostfixCompletion?> expandIf(
PostfixCompletionProcessor processor,
PostfixCompletionKind kind,
) async {
return processor.expand(
kind,
processor.findBoolExpression,
(expr) => 'if (${processor.utils.getNodeText(expr)})',
);
}
static Future<PostfixCompletion?> expandNegate(
PostfixCompletionProcessor processor,
PostfixCompletionKind kind,
) async {
return processor.expand(
kind,
processor.findBoolExpression,
(expr) => processor.makeNegatedBoolExpr(expr),
withBraces: false,
);
}
static Future<PostfixCompletion?> expandNotNull(
PostfixCompletionProcessor processor,
PostfixCompletionKind kind,
) async {
return processor.expand(kind, processor.findObjectExpression, (expr) {
return expr is NullLiteral
? 'if (false)'
: 'if (${processor.utils.getNodeText(expr)} != null)';
});
}
static Future<PostfixCompletion?> expandNull(
PostfixCompletionProcessor processor,
PostfixCompletionKind kind,
) async {
return processor.expand(kind, processor.findObjectExpression, (expr) {
return expr is NullLiteral
? 'if (true)'
: 'if (${processor.utils.getNodeText(expr)} == null)';
});
}
static Future<PostfixCompletion?> expandParen(
PostfixCompletionProcessor processor,
PostfixCompletionKind kind,
) async {
return processor.expand(
kind,
processor.findObjectExpression,
(expr) => '(${processor.utils.getNodeText(expr)})',
withBraces: false,
);
}
static Future<PostfixCompletion?> expandReturn(
PostfixCompletionProcessor processor,
PostfixCompletionKind kind,
) async {
return processor.expand(
kind,
processor.findObjectExpression,
(expr) => 'return ${processor.utils.getNodeText(expr)};',
withBraces: false,
);
}
static Future<PostfixCompletion?> expandSwitch(
PostfixCompletionProcessor processor,
PostfixCompletionKind kind,
) async {
return processor.expand(
kind,
processor.findObjectExpression,
(expr) => 'switch (${processor.utils.getNodeText(expr)})',
);
}
static Future<PostfixCompletion?> expandTry(
PostfixCompletionProcessor processor,
PostfixCompletionKind kind,
) async {
return processor.expandTry(kind, processor.findStatement);
}
static Future<PostfixCompletion?> expandTryon(
PostfixCompletionProcessor processor,
PostfixCompletionKind kind,
) async {
return processor.expandTry(kind, processor.findStatement, withOn: true);
}
static Future<PostfixCompletion?> expandWhile(
PostfixCompletionProcessor processor,
PostfixCompletionKind kind,
) async {
return processor.expand(
kind,
processor.findBoolExpression,
(expr) => 'while (${processor.utils.getNodeText(expr)})',
);
}
static PostfixCompletionKind? forKey(String key) =>
ALL_TEMPLATES.firstWhereOrNull((kind) => kind.key == key);
static bool isAssertContext(PostfixCompletionProcessor processor) {
return processor.findAssertExpression() != null;
}
static bool isBoolContext(PostfixCompletionProcessor processor) {
return processor.findBoolExpression() != null;
}
static bool isIntContext(PostfixCompletionProcessor processor) {
return processor.findIntExpression() != null;
}
static bool isIterableContext(PostfixCompletionProcessor processor) {
return processor.findIterableExpression() != null;
}
static bool isObjectContext(PostfixCompletionProcessor processor) {
return processor.findObjectExpression() != null;
}
static bool isStatementContext(PostfixCompletionProcessor processor) {
return processor.findStatement() != null;
}
static bool isSwitchContext(PostfixCompletionProcessor processor) {
return processor.findObjectExpression() != null;
}
static bool _false(PostfixCompletionProcessor _) => false;
static Future<PostfixCompletion?> _null(
PostfixCompletionProcessor _,
PostfixCompletionKind _,
) async => null;
}
/// A description of a postfix completion.
///
/// Clients may not extend, implement or mix-in this class.
class PostfixCompletion {
/// A description of the assist being proposed.
final PostfixCompletionKind kind;
/// The change to be made in order to apply the assist.
final SourceChange change;
/// Initialize a newly created completion to have the given [kind] and
/// [change].
PostfixCompletion(this.kind, this.change);
}
/// The context for computing a postfix completion.
class PostfixCompletionContext {
final ResolvedUnitResult resolveResult;
final int selectionOffset;
final String key;
PostfixCompletionContext(this.resolveResult, this.selectionOffset, this.key);
}
/// A description of a template for postfix completion. Instances are intended
/// to hold the functions required to determine applicability and expand the
/// template, in addition to its name and simple example. The example is shown
/// (in IntelliJ) in a code-completion menu, so must be quite short.
///
/// Clients may not extend, implement or mix-in this class.
class PostfixCompletionKind {
final String name, example;
final bool Function(PostfixCompletionProcessor) selector;
final Future<PostfixCompletion?> Function(
PostfixCompletionProcessor,
PostfixCompletionKind,
)
computer;
const PostfixCompletionKind(
this.name,
this.example,
this.selector,
this.computer,
);
String get key => name == '!' ? name : '.$name';
String get message => 'Expand $key';
@override
String toString() => name;
}
/// The computer for Dart postfix completions.
final class PostfixCompletionProcessor {
static final _noCompletion = PostfixCompletion(
DartPostfixCompletion.NO_TEMPLATE,
SourceChange('', edits: []),
);
final PostfixCompletionContext _completionContext;
final CorrectionUtils utils;
AstNode? _node;
PostfixCompletion? _completion;
PostfixCompletionProcessor(this._completionContext)
: utils = CorrectionUtils(_completionContext.resolveResult);
String get _eol => utils.endOfLine;
String get _file => _completionContext.resolveResult.path;
String get _key => _completionContext.key;
int get _selectionOffset => _completionContext.selectionOffset;
AnalysisSession get _session => _completionContext.resolveResult.session;
TypeProvider get _typeProvider =>
_completionContext.resolveResult.typeProvider;
TypeSystem get _typeSystem => _completionContext.resolveResult.typeSystem;
CompilationUnit get _unit => _completionContext.resolveResult.unit;
Future<PostfixCompletion> compute() async {
_node = _selectedNode();
if (_node == null) {
return _noCompletion;
}
var completer = DartPostfixCompletion.forKey(_key);
if (completer == null) {
return _noCompletion;
}
return await completer.computer(this, completer) ?? _noCompletion;
}
Future<PostfixCompletion?> expand(
PostfixCompletionKind kind,
Expression? Function() contexter,
String Function(Expression) sourcer, {
bool withBraces = true,
}) async {
var expr = contexter();
if (expr == null) {
return null;
}
var changeBuilder = ChangeBuilder(session: _session);
await changeBuilder.addDartFileEdit(_file, (builder) {
builder.addReplacement(range.node(expr), (builder) {
var newSrc = sourcer(expr);
builder.write(newSrc);
if (withBraces) {
builder.write(' {');
builder.write(_eol);
var indent = utils.getNodePrefix(expr);
builder.write(indent);
builder.write(utils.oneIndent);
builder.selectHere();
builder.write(_eol);
builder.write(indent);
builder.write('}');
} else {
builder.selectHere();
}
});
});
_setCompletionFromBuilder(changeBuilder, kind);
return _completion;
}
Future<PostfixCompletion?> expandTry(
PostfixCompletionKind kind,
Statement? Function() contexter, {
bool withOn = false,
}) async {
var stmt = contexter();
if (stmt == null) {
return null;
}
var changeBuilder = ChangeBuilder(session: _session);
await changeBuilder.addDartFileEdit(_file, (builder) {
var lineInfo = _completionContext.resolveResult.lineInfo;
// Embed the full line(s) of the statement in the try block.
var startLine = lineInfo.getLocation(stmt.offset).lineNumber - 1;
var endLine = lineInfo.getLocation(stmt.end).lineNumber - 1;
if (stmt is ExpressionStatement) {
var semicolon = stmt.semicolon;
if (semicolon != null && !semicolon.isSynthetic) {
endLine += 1;
}
}
var startOffset = lineInfo.getOffsetOfLine(startLine);
var endOffset = lineInfo.getOffsetOfLine(endLine);
var src = utils.getText(startOffset, endOffset - startOffset);
var indent = utils.getLinePrefix(stmt.offset);
builder.addReplacement(
range.startOffsetEndOffset(startOffset, endOffset),
(builder) {
builder.write(indent);
builder.write('try {');
builder.write(_eol);
builder.write(
utils.replaceSourceIndent(
src,
indent,
'$indent${utils.oneIndent}',
includeLeading: true,
ensureTrailingNewline: true,
),
);
builder.selectHere();
builder.write(indent);
builder.write('}');
if (withOn) {
builder.write(' on ');
builder.addSimpleLinkedEdit('NAME', nameOfExceptionThrownBy(stmt));
}
builder.write(' catch (e, s) {');
builder.write(_eol);
builder.write(indent);
builder.write(utils.oneIndent);
builder.write('print(s);');
builder.write(_eol);
builder.write(indent);
builder.write('}');
builder.write(_eol);
},
);
});
_setCompletionFromBuilder(changeBuilder, kind);
return _completion;
}
Expression? findAssertExpression() {
if (_node is Expression) {
var boolExpr = _findOuterExpression(_node, _typeProvider.boolType);
if (boolExpr == null) {
return null;
}
var parent = boolExpr.parent;
var grandParent = parent?.parent;
if (parent is ExpressionFunctionBody &&
grandParent is FunctionExpression) {
var type = grandParent.staticType;
if (type is! FunctionType) {
return boolExpr;
}
if (type.returnType == _typeProvider.boolType) {
return grandParent;
}
}
if (boolExpr.staticType == _typeProvider.boolType) {
return boolExpr;
}
}
return null;
}
Expression? findBoolExpression() =>
_findOuterExpression(_node, _typeProvider.boolType);
Expression? findIntExpression() =>
_findOuterExpression(_node, _typeProvider.intType);
Expression? findIterableExpression() =>
_findOuterExpression(_node, _typeProvider.iterableDynamicType);
Expression? findObjectExpression() =>
_findOuterExpression(_node, _typeProvider.objectQuestionType);
Statement? findStatement() {
var astNode = _node;
while (astNode != null) {
if (astNode is Statement && astNode is! Block) {
// Disallow control-flow statements.
if (astNode is DoStatement ||
astNode is IfStatement ||
astNode is ForStatement ||
astNode is SwitchStatement ||
astNode is TryStatement ||
astNode is WhileStatement) {
return null;
}
return astNode;
}
astNode = astNode.parent;
}
return null;
}
Future<bool> isApplicable() async {
_node = _selectedNode();
if (_node == null) {
return false;
}
var offset = _completionContext.selectionOffset;
if (_node?.commentTokenCovering(offset) != null) {
return false;
}
var completer = DartPostfixCompletion.forKey(_key);
if (completer == null) {
return false;
}
return completer.selector(this);
}
String makeNegatedBoolExpr(Expression expr) {
var originalSrc = utils.getNodeText(expr);
var newSrc = utils.invertCondition(expr);
if (newSrc != originalSrc) {
return newSrc;
} else {
return '!${utils.getNodeText(expr)}';
}
}
String nameOfExceptionThrownBy(AstNode astNode) {
if (astNode is ExpressionStatement) {
astNode = astNode.expression;
}
if (astNode is ThrowExpression) {
var expr = astNode;
var type = expr.expression.staticType;
if (type is! TypeImpl) {
return 'Exception';
}
// Can't catch nullable types, strip `?`s now that we've checked for `*`s.
return type.withNullability(NullabilitySuffix.none).getDisplayString();
}
return 'Exception';
}
String newVariable(String base) {
var name = base;
var i = 1;
var vars = _unit.findPossibleLocalVariableConflicts(_selectionOffset);
while (vars.contains(name)) {
name = '$base${i++}';
}
return name;
}
Expression? _findOuterExpression(AstNode? start, InterfaceType builtInType) {
if (start is SimpleIdentifier && start.element is PrefixElement2) {
return null;
}
AstNode? parent;
if (start is Expression) {
parent = start;
} else if (start is ArgumentList) {
parent = start.parent;
}
if (parent == null) {
return null;
}
var list = <Expression>[];
while (parent is Expression) {
list.add(parent);
parent = parent.parent;
}
var expr = list.firstWhereOrNull((expr) {
var type = expr.staticType;
if (type == null) return false;
return _typeSystem.isSubtypeOf(type, builtInType);
});
var exprParent = expr?.parent;
if (expr is SimpleIdentifier && exprParent is PropertyAccess) {
expr = exprParent;
}
if (exprParent is CascadeExpression) {
expr = exprParent;
}
return expr;
}
AstNode? _selectedNode({int? at}) =>
NodeLocator(at ?? _selectionOffset).searchWithin(_unit);
void _setCompletionFromBuilder(
ChangeBuilder builder,
PostfixCompletionKind kind,
) {
var change = builder.sourceChange;
if (change.edits.isEmpty) {
_completion = null;
return;
}
change.message = formatList(kind.message, null);
_completion = PostfixCompletion(kind, change);
}
}