blob: 988a42d1d5f90c8e002914badca46828fbbb7841 [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/services/correction/util.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/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/generated/source.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.
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, withOn: false);
}
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(_) => false;
static Future<PostfixCompletion?> _null(_, __) 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.
class PostfixCompletionProcessor {
static final NO_COMPLETION = PostfixCompletion(
DartPostfixCompletion.NO_TEMPLATE, SourceChange('', edits: []));
final PostfixCompletionContext completionContext;
final CorrectionUtils utils;
AstNode? node;
PostfixCompletion? completion;
SourceChange change = SourceChange('postfix-completion');
final Map<String, LinkedEditGroup> linkedPositionGroups = {};
Position? exitPosition;
PostfixCompletionProcessor(this.completionContext)
: utils = CorrectionUtils(completionContext.resolveResult);
String get eol => utils.endOfLine;
String get file => completionContext.resolveResult.path;
String get key => completionContext.key;
LineInfo get lineInfo => completionContext.resolveResult.lineInfo;
int get selectionOffset => completionContext.selectionOffset;
AnalysisSession get session => completionContext.resolveResult.session;
TypeProvider get typeProvider => completionContext.resolveResult.typeProvider;
TypeSystem get typeSystem => completionContext.resolveResult.typeSystem;
Future<PostfixCompletion> compute() async {
node = _selectedNode();
if (node == null) {
return NO_COMPLETION;
}
var completer = DartPostfixCompletion.forKey(key);
if (completer == null) {
return NO_COMPLETION;
}
return await completer.computer(this, completer) ?? NO_COMPLETION;
}
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.getIndent(1));
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) {
// 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(src.replaceAll(RegExp('^$indent', multiLine: true),
'$indent${utils.getIndent(1)}'));
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.getIndent(1));
builder.write('print(s);');
builder.write(eol);
builder.write(indent);
builder.write('}');
builder.write(eol);
});
});
_setCompletionFromBuilder(changeBuilder, kind);
return completion;
}
Expression? findAssertExpression() {
final node = this.node;
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.objectType);
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 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';
}
// Only print nullability for non-legacy types in non-legacy libraries.
var showNullability = type.nullabilitySuffix != NullabilitySuffix.star &&
(astNode.root as CompilationUnit)
.declaredElement!
.library
.isNonNullableByDefault;
// Can't catch nullable types, strip `?`s now that we've checked for `*`s.
return type
.withNullability(NullabilitySuffix.none)
.getDisplayString(withNullability: showNullability);
}
return 'Exception';
}
String newVariable(String base) {
var name = base;
var i = 1;
var vars = utils.findPossibleLocalVariableConflicts(selectionOffset);
while (vars.contains(name)) {
name = '$base${i++}';
}
return name;
}
Expression? _findOuterExpression(AstNode? start, InterfaceType builtInType) {
if (start is SimpleIdentifier && start.staticElement is PrefixElement) {
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(completionContext.resolveResult.unit);
void _setCompletionFromBuilder(
ChangeBuilder builder, PostfixCompletionKind kind,
[List? args]) {
var change = builder.sourceChange;
if (change.edits.isEmpty) {
completion = null;
return;
}
change.message = formatList(kind.message, args);
completion = PostfixCompletion(kind, change);
}
}