blob: b159f5db84e9b2926524c2f86f4c87c81132a822 [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 'dart:async';
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/type.dart';
import 'package:analyzer/src/dart/ast/utilities.dart';
import 'package:analyzer/src/generated/java_core.dart';
import 'package:analyzer/src/generated/resolver.dart';
import 'package:analyzer/src/generated/source.dart';
import 'package:analyzer_plugin/utilities/change_builder/change_builder_dart.dart';
import 'package:analyzer_plugin/utilities/range_factory.dart';
/**
* An enumeration of possible postfix completion kinds.
*/
class DartPostfixCompletion {
static const NO_TEMPLATE =
const PostfixCompletionKind('', 'no change', null, null);
static const ALL_TEMPLATES = const [
const PostfixCompletionKind("assert", "expr.assert -> assert(expr);",
isAssertContext, expandAssert),
const PostfixCompletionKind(
"fori",
"limit.fori -> for(var i = 0; i < limit; i++) {}",
isIntContext,
expandFori),
const PostfixCompletionKind(
"for",
"values.for -> for(var value in values) {}",
isIterableContext,
expandFor),
const PostfixCompletionKind(
"iter",
"values.iter -> for(var value in values) {}",
isIterableContext,
expandFor),
const PostfixCompletionKind(
"not", "bool.not -> !bool", isBoolContext, expandNegate),
const PostfixCompletionKind(
"!", "bool! -> !bool", isBoolContext, expandNegate),
const PostfixCompletionKind(
"else", "bool.else -> if (!bool) {}", isBoolContext, expandElse),
const PostfixCompletionKind(
"if", "bool.if -> if (bool) {}", isBoolContext, expandIf),
const PostfixCompletionKind("nn", "expr.nn -> if (expr != null) {}",
isObjectContext, expandNotNull),
const PostfixCompletionKind("notnull",
"expr.notnull -> if (expr != null) {}", isObjectContext, expandNotNull),
const PostfixCompletionKind("null", "expr.null -> if (expr == null) {}",
isObjectContext, expandNull),
const PostfixCompletionKind(
"par", "expr.par -> (expr)", isObjectContext, expandParen),
const PostfixCompletionKind(
"return", "expr.return -> return expr", isObjectContext, expandReturn),
const PostfixCompletionKind("switch", "expr.switch -> switch (expr) {}",
isSwitchContext, expandSwitch),
const PostfixCompletionKind("try", "stmt.try -> try {stmt} catch (e,s) {}",
isStatementContext, expandTry),
const PostfixCompletionKind(
"tryon",
"stmt.try -> try {stmt} on Exception catch (e,s) {}",
isStatementContext,
expandTryon),
const PostfixCompletionKind(
"while", "expr.while -> while (expr) {}", isBoolContext, expandWhile),
];
static Future<PostfixCompletion> expandAssert(
PostfixCompletionProcessor processor, PostfixCompletionKind kind) async {
// TODO(brianwilkerson) Determine whether this await is necessary.
await null;
return processor.expand(kind, processor.findAssertExpression, (expr) {
return "assert(${processor.utils.getNodeText(expr)});";
}, withBraces: false);
}
static Future<PostfixCompletion> expandElse(
PostfixCompletionProcessor processor, PostfixCompletionKind kind) async {
// TODO(brianwilkerson) Determine whether this await is necessary.
await null;
return processor.expand(kind, processor.findBoolExpression,
(expr) => "if (${processor.makeNegatedBoolExpr(expr)})");
}
static Future<PostfixCompletion> expandFor(
PostfixCompletionProcessor processor, PostfixCompletionKind kind) async {
// TODO(brianwilkerson) Determine whether this await is necessary.
await null;
return processor.expand(kind, processor.findIterableExpression, (expr) {
String value = processor.newVariable("value");
return "for (var $value in ${processor.utils.getNodeText(expr)})";
});
}
static Future<PostfixCompletion> expandFori(
PostfixCompletionProcessor processor, PostfixCompletionKind kind) async {
// TODO(brianwilkerson) Determine whether this await is necessary.
await null;
return processor.expand(kind, processor.findIntExpression, (expr) {
String index = processor.newVariable("i");
return "for (int $index = 0; $index < ${processor.utils.getNodeText(expr)}; $index++)";
});
}
static Future<PostfixCompletion> expandIf(
PostfixCompletionProcessor processor, PostfixCompletionKind kind) async {
// TODO(brianwilkerson) Determine whether this await is necessary.
await null;
return processor.expand(kind, processor.findBoolExpression,
(expr) => "if (${processor.utils.getNodeText(expr)})");
}
static Future<PostfixCompletion> expandNegate(
PostfixCompletionProcessor processor, PostfixCompletionKind kind) async {
// TODO(brianwilkerson) Determine whether this await is necessary.
await null;
return processor.expand(kind, processor.findBoolExpression,
(expr) => processor.makeNegatedBoolExpr(expr),
withBraces: false);
}
static Future<PostfixCompletion> expandNotNull(
PostfixCompletionProcessor processor, PostfixCompletionKind kind) async {
// TODO(brianwilkerson) Determine whether this await is necessary.
await null;
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 {
// TODO(brianwilkerson) Determine whether this await is necessary.
await null;
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 {
// TODO(brianwilkerson) Determine whether this await is necessary.
await null;
return processor.expand(kind, processor.findObjectExpression,
(expr) => "(${processor.utils.getNodeText(expr)})",
withBraces: false);
}
static Future<PostfixCompletion> expandReturn(
PostfixCompletionProcessor processor, PostfixCompletionKind kind) async {
// TODO(brianwilkerson) Determine whether this await is necessary.
await null;
return processor.expand(kind, processor.findObjectExpression,
(expr) => "return ${processor.utils.getNodeText(expr)};",
withBraces: false);
}
static Future<PostfixCompletion> expandSwitch(
PostfixCompletionProcessor processor, PostfixCompletionKind kind) async {
// TODO(brianwilkerson) Determine whether this await is necessary.
await null;
return processor.expand(kind, processor.findObjectExpression,
(expr) => "switch (${processor.utils.getNodeText(expr)})");
}
static Future<PostfixCompletion> expandTry(
PostfixCompletionProcessor processor, PostfixCompletionKind kind) async {
// TODO(brianwilkerson) Determine whether this await is necessary.
await null;
return processor.expandTry(kind, processor.findStatement, withOn: false);
}
static Future<PostfixCompletion> expandTryon(
PostfixCompletionProcessor processor, PostfixCompletionKind kind) async {
// TODO(brianwilkerson) Determine whether this await is necessary.
await null;
return processor.expandTry(kind, processor.findStatement, withOn: true);
}
static Future<PostfixCompletion> expandWhile(
PostfixCompletionProcessor processor, PostfixCompletionKind kind) async {
// TODO(brianwilkerson) Determine whether this await is necessary.
await null;
return processor.expand(kind, processor.findBoolExpression,
(expr) => "while (${processor.utils.getNodeText(expr)})");
}
static PostfixCompletionKind forKey(String key) =>
ALL_TEMPLATES.firstWhere((kind) => kind.key == key, orElse: () => null);
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;
}
}
/**
* 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 Function selector;
final Function 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 = new PostfixCompletion(
DartPostfixCompletion.NO_TEMPLATE, new SourceChange("", edits: []));
final PostfixCompletionContext completionContext;
final CorrectionUtils utils;
AstNode node;
PostfixCompletion completion;
SourceChange change = new SourceChange('postfix-completion');
final Map<String, LinkedEditGroup> linkedPositionGroups = {};
Position exitPosition = null;
PostfixCompletionProcessor(this.completionContext)
: utils = new 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;
Future<PostfixCompletion> compute() async {
// TODO(brianwilkerson) Determine whether this await is necessary.
await null;
node = _selectedNode();
if (node == null) {
return NO_COMPLETION;
}
PostfixCompletionKind completer = DartPostfixCompletion.forKey(key);
return completer?.computer(this, completer) ?? NO_COMPLETION;
}
Future<PostfixCompletion> expand(
PostfixCompletionKind kind, Function contexter, Function sourcer,
{bool withBraces: true}) async {
// TODO(brianwilkerson) Determine whether this await is necessary.
await null;
AstNode expr = contexter();
if (expr == null) {
return null;
}
DartChangeBuilder changeBuilder = new DartChangeBuilder(session);
await changeBuilder.addFileEdit(file, (DartFileEditBuilder builder) {
builder.addReplacement(range.node(expr), (DartEditBuilder builder) {
String newSrc = sourcer(expr);
if (newSrc == null) {
return null;
}
builder.write(newSrc);
if (withBraces) {
builder.write(" {");
builder.write(eol);
String 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, Function contexter,
{bool withOn: false}) async {
// TODO(brianwilkerson) Determine whether this await is necessary.
await null;
AstNode stmt = contexter();
if (stmt == null) {
return null;
}
DartChangeBuilder changeBuilder = new DartChangeBuilder(session);
await changeBuilder.addFileEdit(file, (DartFileEditBuilder 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 && !stmt.semicolon.isSynthetic) {
endLine += 1;
}
var startOffset = lineInfo.getOffsetOfLine(startLine);
var endOffset = lineInfo.getOffsetOfLine(endLine);
var src = utils.getText(startOffset, endOffset - startOffset);
String indent = utils.getLinePrefix(stmt.offset);
builder.addReplacement(range.startOffsetEndOffset(startOffset, endOffset),
(DartEditBuilder builder) {
builder.write(indent);
builder.write('try {');
builder.write(eol);
builder.write(src.replaceAll(new 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() {
if (node is Expression) {
Expression boolExpr = _findOuterExpression(node, typeProvider.boolType);
if (boolExpr == null) {
return null;
}
if (boolExpr.parent is ExpressionFunctionBody &&
boolExpr.parent.parent is FunctionExpression) {
FunctionExpression fnExpr = boolExpr.parent.parent;
var type = fnExpr.staticType;
if (type is! FunctionType) {
return boolExpr;
}
FunctionType fnType = type;
if (fnType.returnType == typeProvider.boolType) {
return fnExpr;
}
}
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.iterableType);
Expression findObjectExpression() =>
_findOuterExpression(node, typeProvider.objectType);
AstNode 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 ForStatement2 ||
astNode is SwitchStatement ||
astNode is TryStatement ||
astNode is WhileStatement) {
return null;
}
return astNode;
}
astNode = astNode.parent;
}
return null;
}
Future<bool> isApplicable() async {
// TODO(brianwilkerson) Determine whether this await is necessary.
await null;
node = _selectedNode();
if (node == null) {
return false;
}
PostfixCompletionKind completer = DartPostfixCompletion.forKey(key);
return completer?.selector(this);
}
String makeNegatedBoolExpr(Expression expr) {
String originalSrc = utils.getNodeText(expr);
String newSrc = utils.invertCondition(expr);
if (newSrc != originalSrc) {
return newSrc;
} else {
return "!${utils.getNodeText(expr)}";
}
}
String nameOfExceptionThrownBy(AstNode astNode) {
if (astNode is ExpressionStatement) {
astNode = (astNode as ExpressionStatement).expression;
}
if (astNode is ThrowExpression) {
ThrowExpression expr = astNode;
var type = expr.expression.staticType;
return type.displayName;
}
return 'Exception';
}
String newVariable(String base) {
String name = base;
int i = 1;
Set<String> 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;
}
Expression expr = list.firstWhere((expr) {
DartType type = expr.staticType;
if (type == null) return false;
if (type.isSubtypeOf(builtInType)) return true;
Element element = type.element;
if (element is TypeDefiningElement) {
TypeDefiningElement typeDefElem = element;
type = typeDefElem.type;
if (type is ParameterizedType) {
ParameterizedType pType = type;
type = pType.instantiate(new List.filled(
pType.typeParameters.length, typeProvider.dynamicType));
}
}
return type.isSubtypeOf(builtInType);
}, orElse: () => null);
if (expr is SimpleIdentifier && expr.parent is PropertyAccess) {
expr = expr.parent;
}
if (expr?.parent is CascadeExpression) {
expr = expr.parent;
}
return expr;
}
AstNode _selectedNode({int at: null}) =>
new NodeLocator(at == null ? selectionOffset : at)
.searchWithin(completionContext.resolveResult.unit);
void _setCompletionFromBuilder(
DartChangeBuilder builder, PostfixCompletionKind kind,
[List args]) {
SourceChange change = builder.sourceChange;
if (change.edits.isEmpty) {
completion = null;
return;
}
change.message = formatList(kind.message, args);
completion = new PostfixCompletion(kind, change);
}
}