blob: e337213c840b8e1e04e6ebc7c257736fa368c9ea [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:math';
import 'package:analysis_server/src/protocol_server.dart' hide Element;
import 'package:analysis_server/src/services/correction/source_buffer.dart';
import 'package:analysis_server/src/services/correction/util.dart';
import 'package:analyzer/dart/analysis/results.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/error/error.dart';
import 'package:analyzer/error/error.dart' as engine;
import 'package:analyzer/src/dart/ast/utilities.dart';
import 'package:analyzer/src/dart/error/syntactic_errors.dart';
import 'package:analyzer/src/error/codes.dart';
import 'package:analyzer/src/generated/java_core.dart';
import 'package:analyzer/src/generated/source.dart';
import 'package:analyzer_plugin/utilities/range_factory.dart';
import 'package:collection/collection.dart';
/// An enumeration of possible statement completion kinds.
class DartStatementCompletion {
static const NO_COMPLETION =
StatementCompletionKind('No_COMPLETION', 'No completion available');
static const SIMPLE_ENTER = StatementCompletionKind(
'SIMPLE_ENTER', 'Insert a newline at the end of the current line');
static const SIMPLE_SEMICOLON = StatementCompletionKind(
'SIMPLE_SEMICOLON', 'Add a semicolon and newline');
static const COMPLETE_CLASS_DECLARATION = StatementCompletionKind(
'COMPLETE_CLASS_DECLARATION', 'Complete class declaration');
static const COMPLETE_CONTROL_FLOW_BLOCK = StatementCompletionKind(
'COMPLETE_CONTROL_FLOW_BLOCK', 'Complete control flow block');
static const COMPLETE_DO_STMT =
StatementCompletionKind('COMPLETE_DO_STMT', 'Complete do-statement');
static const COMPLETE_IF_STMT =
StatementCompletionKind('COMPLETE_IF_STMT', 'Complete if-statement');
static const COMPLETE_FOR_STMT =
StatementCompletionKind('COMPLETE_FOR_STMT', 'Complete for-statement');
static const COMPLETE_FOR_EACH_STMT = StatementCompletionKind(
'COMPLETE_FOR_EACH_STMT', 'Complete for-each-statement');
static const COMPLETE_FUNCTION_DECLARATION = StatementCompletionKind(
'COMPLETE_FUNCTION_DECLARATION', 'Complete function declaration');
static const COMPLETE_SWITCH_STMT = StatementCompletionKind(
'COMPLETE_SWITCH_STMT', 'Complete switch-statement');
static const COMPLETE_TRY_STMT =
StatementCompletionKind('COMPLETE_TRY_STMT', 'Complete try-statement');
static const COMPLETE_VARIABLE_DECLARATION = StatementCompletionKind(
'COMPLETE_VARIABLE_DECLARATION', 'Complete variable declaration');
static const COMPLETE_WHILE_STMT = StatementCompletionKind(
'COMPLETE_WHILE_STMT', 'Complete while-statement');
}
/// A description of a statement completion.
///
/// Clients may not extend, implement or mix-in this class.
class StatementCompletion {
/// A description of the assist being proposed.
final StatementCompletionKind 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].
StatementCompletion(this.kind, this.change);
}
/// The context for computing a statement completion.
class StatementCompletionContext {
final ResolvedUnitResult resolveResult;
final int selectionOffset;
StatementCompletionContext(this.resolveResult, this.selectionOffset);
}
/// A description of a class of statement completions. Instances are intended to
/// hold the information that is common across a number of completions and to be
/// shared by those completions.
///
/// Clients may not extend, implement or mix-in this class.
class StatementCompletionKind {
/// The name of this kind of statement completion, used for debugging.
final String name;
/// A human-readable description of the changes that will be applied by this
/// kind of statement completion.
final String message;
/// Initialize a newly created kind of statement completion to have the given
/// [name] and [message].
const StatementCompletionKind(this.name, this.message);
@override
String toString() => name;
}
/// The computer for Dart statement completions.
class StatementCompletionProcessor {
static final NO_COMPLETION = StatementCompletion(
DartStatementCompletion.NO_COMPLETION, SourceChange('', edits: []));
final StatementCompletionContext statementContext;
final CorrectionUtils utils;
/// TODO(brianwilkerson) Refactor the code so that the completion is returned
/// from the methods in which it's computed rather than being a field that we
/// have to test.
StatementCompletion? completion;
SourceChange change = SourceChange('statement-completion');
List<engine.AnalysisError> errors = [];
final Map<String, LinkedEditGroup> linkedPositionGroups =
<String, LinkedEditGroup>{};
Position? exitPosition;
StatementCompletionProcessor(this.statementContext)
: utils = CorrectionUtils(statementContext.resolveResult);
String get eol => utils.endOfLine;
String get file => statementContext.resolveResult.path;
LineInfo get lineInfo => statementContext.resolveResult.lineInfo;
int get selectionOffset => statementContext.selectionOffset;
Source get source => unitElement.source;
CompilationUnit get unit => statementContext.resolveResult.unit;
CompilationUnitElement get unitElement => unit.declaredElement!;
Future<StatementCompletion> compute() async {
var node = _selectedNode();
if (node == null) {
return NO_COMPLETION;
}
node = node.thisOrAncestorMatching(
(n) => n is Statement || _isNonStatementDeclaration(n));
if (node == null) {
return _complete_simpleEnter() ? completion! : NO_COMPLETION;
}
if (node is Block) {
if (node.statements.isNotEmpty) {
node = node.statements.last;
}
}
if (_isEmptyStatementOrEmptyBlock(node)) {
node = node.parent!;
}
for (var error in statementContext.resolveResult.errors) {
if (error.offset >= node.offset && error.offset <= node.end) {
if (error.errorCode is! HintCode) {
errors.add(error);
}
}
}
_checkExpressions(node);
if (node is Statement) {
if (errors.isEmpty) {
if (_complete_ifStatement(node) ||
_complete_forStatement2(node) ||
_complete_whileStatement(node) ||
_complete_controlFlowBlock(node)) {
return completion!;
}
} else {
if (_complete_ifStatement(node) ||
_complete_doStatement(node) ||
_complete_forStatement2(node) ||
_complete_functionDeclarationStatement(node) ||
_complete_switchStatement(node) ||
_complete_tryStatement(node) ||
_complete_whileStatement(node) ||
_complete_controlFlowBlock(node) ||
_complete_simpleSemicolon(node) ||
_complete_methodCall(node)) {
return completion!;
}
}
} else if (node is Declaration) {
if (errors.isNotEmpty) {
if (_complete_classDeclaration(node) ||
_complete_variableDeclaration(node) ||
_complete_simpleSemicolon(node) ||
_complete_functionDeclaration(node)) {
return completion!;
}
}
}
if (_complete_simpleEnter()) {
return completion!;
}
return NO_COMPLETION;
}
void _addInsertEdit(int offset, String text) {
var edit = SourceEdit(offset, 0, text);
doSourceChange_addElementEdit(change, unitElement, edit);
}
void _addReplaceEdit(SourceRange range, String text) {
var edit = SourceEdit(range.offset, range.length, text);
// TODO(brianwilkerson) The commented out function call has been inlined in
// order to work around a situation in which _complete_doStatement creates
// a conflicting edit that happens to work because of the order in which
// the edits are applied. The implementation needs to be cleaned up in
// order to prevent the conflicting edit from being generated.
// doSourceChange_addElementEdit(change, unitElement, edit);
var fileEdit = change.getFileEdit(unitElement.source.fullName);
if (fileEdit == null) {
fileEdit = SourceFileEdit(file, 0);
change.addFileEdit(fileEdit);
}
var edits = fileEdit.edits;
var length = edits.length;
var index = 0;
while (index < length && edits[index].offset > edit.offset) {
index++;
}
edits.insert(index, edit);
}
void _appendEmptyBraces(SourceBuilder sb, [bool needsExitMark = false]) {
sb.append('{');
sb.append(eol);
var indent = utils.getLinePrefix(selectionOffset);
sb.append(indent);
sb.append(utils.getIndent(1));
if (needsExitMark && sb.exitOffset == null) {
sb.setExitOffset();
}
sb.append(eol);
sb.append(indent);
sb.append('}');
}
int _appendNewlinePlusIndent() {
return _appendNewlinePlusIndentAt(selectionOffset);
}
int _appendNewlinePlusIndentAt(int offset) {
// Append a newline plus proper indent and another newline.
// Return the position before the second newline.
var indent = utils.getLinePrefix(offset);
var loc = utils.getLineNext(offset);
_addInsertEdit(loc, indent + eol);
return loc + indent.length;
}
String _baseNodeText(AstNode astNode) {
var text = utils.getNodeText(astNode);
if (text.endsWith(eol)) {
text = text.substring(0, text.length - eol.length);
}
return text;
}
void _checkExpressions(AstNode node) {
// Note: This may queue edits that have to be accounted for later.
// See _lengthOfInsertions().
AstNode? errorMatching(errorCode, {partialMatch}) {
var error = _findError(errorCode, partialMatch: partialMatch);
if (error == null) {
return null;
}
var expr = _selectedNode();
return (expr?.thisOrAncestorOfType<StringInterpolation>() == null)
? expr
: null;
}
var expr = errorMatching(ScannerErrorCode.UNTERMINATED_STRING_LITERAL);
if (expr != null) {
var source = utils.getNodeText(expr);
var content = source;
var char = content.codeUnitAt(0);
if (char == 'r'.codeUnitAt(0)) {
content = source.substring(1);
char = content.codeUnitAt(0);
}
String delimiter;
int loc;
if (content.length >= 3 &&
char == content.codeUnitAt(1) &&
char == content.codeUnitAt(2)) {
// multi-line string
delimiter = content.substring(0, 3);
var newlineLoc = source.indexOf(eol, selectionOffset - expr.offset);
if (newlineLoc < 0) {
newlineLoc = source.length;
}
loc = newlineLoc + expr.offset;
} else {
// add first char of src
delimiter = content.substring(0, 1);
loc = expr.offset + source.length;
}
_removeError(ScannerErrorCode.UNTERMINATED_STRING_LITERAL);
_addInsertEdit(loc, delimiter);
}
expr = errorMatching(ParserErrorCode.EXPECTED_TOKEN, partialMatch: "']'") ??
errorMatching(ScannerErrorCode.EXPECTED_TOKEN, partialMatch: "']'");
if (expr != null) {
expr = expr.thisOrAncestorOfType<ListLiteral>();
if (expr is ListLiteral) {
if (expr.rightBracket.isSynthetic) {
var src = utils.getNodeText(expr).trim();
var loc = expr.offset + src.length;
if (src.contains(eol)) {
var indent = utils.getNodePrefix(node);
_addInsertEdit(loc, ',' + eol + indent + ']');
} else {
_addInsertEdit(loc, ']');
}
_removeError(ParserErrorCode.EXPECTED_TOKEN, partialMatch: "']'");
_removeError(ScannerErrorCode.EXPECTED_TOKEN, partialMatch: "']'");
}
}
}
// The following code is similar to the code for ']' but does not work well.
// A closing brace is recognized as belong to the map even if it is intended
// to close a block of code.
/*
expr = errorMatching(ParserErrorCode.EXPECTED_TOKEN, partialMatch: "'}'");
if (expr != null) {
expr = expr.getAncestor((n) => n is MapLiteral);
if (expr != null) {
MapLiteral lit = expr;
String src = utils.getNodeText(expr).trim();
int loc = expr.offset + src.length;
if (lit.entries.last.separator.isSynthetic) {
_addInsertEdit(loc, ': ');
}
if (!src.endsWith('}')/*lit.rightBracket.isSynthetic*/) {
_addInsertEdit(loc, '}');
}
_removeError(ParserErrorCode.EXPECTED_TOKEN, partialMatch: "'}'");
var ms =
_findError(ParserErrorCode.EXPECTED_TOKEN, partialMatch: "';'");
if (ms != null) {
// Ensure the semicolon gets inserted in the correct location.
ms.offset = loc - 1;
}
}
}
*/
}
bool _complete_classDeclaration(AstNode node) {
if (node is! ClassDeclaration) {
return false;
}
if (node.leftBracket.isSynthetic && errors.length == 1) {
// The space before the left brace is assumed to exist, even if it does not.
var sb = SourceBuilder(file, node.end - 1);
sb.append(' ');
_appendEmptyBraces(sb, true);
_insertBuilder(sb);
_setCompletion(DartStatementCompletion.COMPLETE_CLASS_DECLARATION);
return true;
}
return false;
}
bool _complete_controlFlowBlock(AstNode node) {
var expr = (node is ExpressionStatement)
? node.expression
: (node is ReturnStatement ? node.expression : null);
if (!(node is ReturnStatement || expr is ThrowExpression)) {
return false;
}
var parent = node.parent;
if (parent is! Block) {
return false;
}
var outer = parent.parent;
if (!(outer is DoStatement ||
outer is ForStatement ||
outer is IfStatement ||
outer is WhileStatement)) {
return false;
}
var previousInsertions = _lengthOfInsertions();
var delta = 0;
if (errors.isNotEmpty) {
var error =
_findError(ParserErrorCode.EXPECTED_TOKEN, partialMatch: "';'");
if (error != null) {
int insertOffset;
// Fasta scanner reports unterminated string literal errors
// and generates a synthetic string token with non-zero length.
// Because of this, check for length == 0 rather than isSynthetic.
if (expr == null || expr.length == 0) {
if (node is ReturnStatement) {
insertOffset = node.returnKeyword.end;
} else if (node is ExpressionStatement) {
insertOffset =
(node.expression as ThrowExpression).throwKeyword.end;
} else {
insertOffset = node.end; // Not reached.
}
} else {
insertOffset = expr.end;
}
//TODO(messick) Uncomment the following line when error location is fixed.
//insertOffset = error.offset + error.length;
_addInsertEdit(insertOffset, ';');
delta = 1;
}
}
var offset = _appendNewlinePlusIndentAt(parent.end);
exitPosition = Position(file, offset + delta + previousInsertions);
_setCompletion(DartStatementCompletion.COMPLETE_CONTROL_FLOW_BLOCK);
return true;
}
bool _complete_doStatement(AstNode node) {
if (node is! DoStatement) {
return false;
}
var sb = _sourceBuilderAfterKeyword(node, node.doKeyword);
var hasWhileKeyword = !node.whileKeyword.isSynthetic;
var exitDelta = 0;
if (!_statementHasValidBody(node.doKeyword, node.body)) {
var text = utils.getNodeText(node.body);
var delta = 0;
if (text.startsWith(';')) {
delta = 1;
_addReplaceEdit(range.startLength(node.body, delta), '');
if (hasWhileKeyword) {
text = utils.getNodeText(node);
if (text.indexOf(RegExp(r'do\s*;\s*while')) == 0) {
var end = text.indexOf('while');
var start = text.indexOf(';') + 1;
delta += end - start - 1;
_addReplaceEdit(SourceRange(start + node.offset, end - start), ' ');
}
}
sb = SourceBuilder(file, sb.offset + delta);
sb.append(' ');
}
_appendEmptyBraces(
sb, !(hasWhileKeyword && _isSyntheticExpression(node.condition)));
if (delta != 0) {
exitDelta = sb.length - delta;
}
} else if (_isEmptyBlock(node.body)) {
sb = SourceBuilder(sb.file, node.body.end);
}
SourceBuilder? sb2;
if (hasWhileKeyword) {
var stmt = _KeywordConditionBlockStructure(node.whileKeyword,
node.leftParenthesis, node.condition, node.rightParenthesis, null);
sb2 = _complete_keywordCondition(node, stmt);
if (sb2 == null) {
return false;
}
if (sb2.length == 0) {
// true if condition is '()'
final exitPosition = this.exitPosition;
if (exitPosition != null) {
if (node.semicolon.isSynthetic) {
_insertBuilder(sb);
sb = SourceBuilder(file, exitPosition.offset + 1);
sb.append(';');
}
}
} else {
if (sb.exitOffset == null && sb2.exitOffset != null) {
_insertBuilder(sb);
sb = sb2;
sb.append(';');
} else {
sb.append(sb2.toString());
}
}
} else {
sb.append(' while (');
sb.setExitOffset();
sb.append(');');
}
_insertBuilder(sb);
if (exitDelta != 0) {
exitPosition =
Position(exitPosition!.file, exitPosition!.offset + exitDelta);
}
_setCompletion(DartStatementCompletion.COMPLETE_DO_STMT);
return true;
}
bool _complete_forEachStatement(
ForStatement forNode, ForEachParts forEachParts) {
AstNode name;
if (forEachParts is ForEachPartsWithIdentifier) {
name = forEachParts.identifier;
} else if (forEachParts is ForEachPartsWithDeclaration) {
name = forEachParts.loopVariable;
} else {
throw StateError('Unrecognized for loop parts');
}
return _complete_forEachStatementRest(
forNode,
forNode.forKeyword,
forNode.leftParenthesis,
name,
forEachParts.inKeyword,
forEachParts.iterable,
forNode.rightParenthesis,
forNode.body);
}
bool _complete_forEachStatementRest(
AstNode node,
Token forKeyword,
Token leftParenthesis,
AstNode? name,
Token inKeyword,
Expression? iterable,
Token rightParenthesis,
Statement body) {
if (inKeyword.isSynthetic) {
return false; // Can't happen -- would be parsed as a for-statement.
}
var sb = SourceBuilder(file, rightParenthesis.offset + 1);
var src = utils.getNodeText(node);
if (name == null) {
exitPosition = Position(file, leftParenthesis.offset + 1);
src = src.substring(leftParenthesis.offset - node.offset);
if (src.startsWith(RegExp(r'\(\s*in\s*\)'))) {
_addReplaceEdit(
range.startOffsetEndOffset(
leftParenthesis.offset + 1, rightParenthesis.offset),
' in ');
} else if (src.startsWith(RegExp(r'\(\s*in'))) {
_addReplaceEdit(
range.startOffsetEndOffset(
leftParenthesis.offset + 1, inKeyword.offset),
' ');
}
} else if (iterable != null && _isSyntheticExpression(iterable)) {
exitPosition = Position(file, rightParenthesis.offset + 1);
src = src.substring(inKeyword.offset - node.offset);
if (src.startsWith(RegExp(r'in\s*\)'))) {
_addReplaceEdit(
range.startOffsetEndOffset(
inKeyword.offset + inKeyword.length, rightParenthesis.offset),
' ');
}
}
if (!_statementHasValidBody(forKeyword, body)) {
sb.append(' ');
_appendEmptyBraces(sb, exitPosition == null);
}
_insertBuilder(sb);
_setCompletion(DartStatementCompletion.COMPLETE_FOR_EACH_STMT);
return true;
}
bool _complete_forStatement(ForStatement forNode, ForParts forParts) {
SourceBuilder sb;
var replacementLength = 0;
if (forNode.leftParenthesis.isSynthetic) {
if (!forNode.rightParenthesis.isSynthetic) {
return false;
}
// keywordOnly (unit test name suffix that exercises this branch)
sb = _sourceBuilderAfterKeyword(forNode, forNode.forKeyword);
sb.append('(');
sb.setExitOffset();
sb.append(')');
} else {
if (!forParts.rightSeparator.isSynthetic) {
// Fully-defined init, cond, updaters so nothing more needed here.
// emptyParts, noError
sb = SourceBuilder(file, forNode.rightParenthesis.offset + 1);
} else if (!forParts.leftSeparator.isSynthetic) {
if (_isSyntheticExpression(forParts.condition)) {
var text = utils
.getNodeText(forNode)
.substring(forParts.leftSeparator.offset - forNode.offset);
var match = RegExp(r';\s*(/\*.*\*/\s*)?\)[ \t]*').matchAsPrefix(text);
if (match != null) {
// emptyCondition, emptyInitializersEmptyCondition
replacementLength = match.end - match.start;
sb = SourceBuilder(file, forParts.leftSeparator.offset);
sb.append('; ${match.group(1) ?? ''}; )');
var suffix = text.substring(match.end);
if (suffix.trim().isNotEmpty) {
sb.append(' ');
sb.append(suffix.trim());
replacementLength += suffix.length;
if (suffix.endsWith(eol)) {
// emptyCondition
replacementLength -= eol.length;
}
}
exitPosition = _newPosition(forParts.leftSeparator.offset + 2);
} else {
return false; // Line comment in condition
}
} else {
// emptyUpdaters
sb = SourceBuilder(file, forNode.rightParenthesis.offset);
replacementLength = 1;
sb.append('; )');
exitPosition = _newPosition(forParts.rightSeparator.offset + 2);
}
} else if (forParts is ForPartsWithExpression &&
_isSyntheticExpression(forParts.initialization)) {
// emptyInitializers
exitPosition = _newPosition(forNode.rightParenthesis.offset);
sb = SourceBuilder(file, forNode.rightParenthesis.offset);
} else if (forParts is ForPartsWithExpression &&
forParts.initialization is SimpleIdentifier &&
forParts.initialization!.beginToken.lexeme == 'in') {
// looks like a for/each statement missing the loop variable
return _complete_forEachStatementRest(
forNode,
forNode.forKeyword,
forNode.leftParenthesis,
null,
forParts.initialization!.beginToken,
null,
forNode.rightParenthesis,
forNode.body);
} else {
var start = forParts.condition!.offset + forParts.condition!.length;
var text = utils.getNodeText(forNode).substring(start - forNode.offset);
if (text.startsWith(RegExp(r'\s*\)'))) {
// missingLeftSeparator
var end = text.indexOf(')');
sb = SourceBuilder(file, start);
_addReplaceEdit(SourceRange(start, end), '; ; ');
exitPosition = Position(file, start - (end - '; '.length));
} else {
// Not possible; any comment following init is attached to init.
exitPosition = _newPosition(forNode.rightParenthesis.offset);
sb = SourceBuilder(file, forNode.rightParenthesis.offset);
}
}
}
var body = forNode.body;
if (!_statementHasValidBody(forNode.forKeyword, body)) {
// keywordOnly, noError
sb.append(' ');
_appendEmptyBraces(sb, true /*exitPosition == null*/);
} else if (body is Block) {
if (body.rightBracket.end <= selectionOffset) {
// emptyInitializersAfterBody
errors = []; // Ignore errors; they are for previous statement.
return false; // If cursor is after closing brace just add newline.
}
}
_insertBuilder(sb, replacementLength);
_setCompletion(DartStatementCompletion.COMPLETE_FOR_STMT);
return true;
}
bool _complete_forStatement2(AstNode node) {
if (node is ForStatement) {
var forLoopParts = node.forLoopParts;
if (forLoopParts is ForParts) {
return _complete_forStatement(node, forLoopParts);
} else if (forLoopParts is ForEachParts) {
return _complete_forEachStatement(node, forLoopParts);
}
}
return false;
}
bool _complete_functionDeclaration(AstNode node) {
if (node is! MethodDeclaration && node is! FunctionDeclaration) {
return false;
}
var needsParen = false;
int computeExitPos(FormalParameterList parameters) {
if (needsParen = parameters.rightParenthesis.isSynthetic) {
var error = _findError(ParserErrorCode.MISSING_CLOSING_PARENTHESIS);
if (error != null) {
return error.offset - 1;
}
}
return node.end - 1;
}
int paramListEnd;
if (node is FunctionDeclaration) {
var parameters = node.functionExpression.parameters;
if (parameters == null) {
return false;
}
paramListEnd = computeExitPos(parameters);
} else if (node is MethodDeclaration) {
var parameters = node.parameters;
if (parameters == null) {
return false;
}
paramListEnd = computeExitPos(parameters);
} else {
return false;
}
var sb = SourceBuilder(file, paramListEnd);
if (needsParen) {
sb.append(')');
}
sb.append(' ');
_appendEmptyBraces(sb, true);
_insertBuilder(sb);
_setCompletion(DartStatementCompletion.COMPLETE_FUNCTION_DECLARATION);
return true;
}
bool _complete_functionDeclarationStatement(AstNode node) {
if (node is! FunctionDeclarationStatement) {
return false;
}
var error = _findError(ParserErrorCode.EXPECTED_TOKEN, partialMatch: "';'");
if (error != null) {
var src = utils.getNodeText(node);
var insertOffset = node.functionDeclaration.end - 1;
var body = node.functionDeclaration.functionExpression.body;
if (body is ExpressionFunctionBody) {
var fnbOffset = body.functionDefinition.offset;
var fnSrc = src.substring(fnbOffset - node.offset);
if (!fnSrc.startsWith('=>')) {
return false;
}
var delta = 0;
if (body.expression.isSynthetic) {
if (!fnSrc.startsWith('=> ')) {
_addInsertEdit(insertOffset, ' ');
delta = 1;
}
_addInsertEdit(insertOffset, ';');
_appendNewlinePlusIndentAt(insertOffset);
} else {
delta = 1;
_addInsertEdit(insertOffset, ';');
insertOffset = _appendNewlinePlusIndent();
}
_setCompletionAt(
DartStatementCompletion.SIMPLE_SEMICOLON, insertOffset + delta);
return true;
}
}
return false;
}
bool _complete_ifOrWhileStatement(AstNode node,
_KeywordConditionBlockStructure statement, StatementCompletionKind kind) {
if (_statementHasValidBody(statement.keyword, statement.block!)) {
return false;
}
var sb = _complete_keywordCondition(node, statement);
if (sb == null) {
return false;
}
var overshoot = _lengthOfDeletions();
sb.append(' ');
_appendEmptyBraces(sb, exitPosition == null);
_insertBuilder(sb);
if (overshoot != 0) {
exitPosition = _newPosition(exitPosition!.offset - overshoot);
}
_setCompletion(kind);
return true;
}
bool _complete_ifStatement(AstNode node) {
if (node is! IfStatement) {
return false;
}
var elseKeyword = node.elseKeyword;
if (elseKeyword != null) {
if (selectionOffset >= elseKeyword.end &&
_isEmptyStatement(node.elseStatement)) {
var sb = SourceBuilder(file, selectionOffset);
var src = utils.getNodeText(node);
if (!src
.substring(elseKeyword.end - node.offset)
.startsWith(RegExp(r'[ \t]'))) {
sb.append(' ');
}
_appendEmptyBraces(sb, true);
_insertBuilder(sb);
_setCompletion(DartStatementCompletion.COMPLETE_IF_STMT);
return true;
}
return false;
}
var stmt = _KeywordConditionBlockStructure(
node.ifKeyword,
node.leftParenthesis,
node.condition,
node.rightParenthesis,
node.thenStatement);
return _complete_ifOrWhileStatement(
node, stmt, DartStatementCompletion.COMPLETE_IF_STMT);
}
SourceBuilder? _complete_keywordCondition(
AstNode node, _KeywordConditionBlockStructure statement) {
SourceBuilder sb;
if (statement.leftParenthesis.isSynthetic) {
if (!statement.rightParenthesis.isSynthetic) {
// Quite unlikely to see this so don't try to fix it.
return null;
}
sb = _sourceBuilderAfterKeyword(node, statement.keyword);
sb.append('(');
sb.setExitOffset();
sb.append(')');
} else {
if (_isSyntheticExpression(statement.condition)) {
exitPosition = _newPosition(statement.leftParenthesis.offset + 1);
sb = SourceBuilder(file, statement.rightParenthesis.offset + 1);
} else if (statement.rightParenthesis.isSynthetic) {
sb = SourceBuilder(file, statement.condition.end);
sb.append(')');
} else {
var afterParen = statement.rightParenthesis.offset + 1;
if (utils
.getNodeText(node)
.substring(afterParen - node.offset)
.startsWith(RegExp(r'[ \t]'))) {
_addReplaceEdit(SourceRange(afterParen, 1), '');
sb = SourceBuilder(file, afterParen + 1);
} else {
sb = SourceBuilder(file, afterParen);
}
}
}
return sb;
}
bool _complete_methodCall(AstNode node) {
var parenError =
_findError(ParserErrorCode.EXPECTED_TOKEN, partialMatch: "')'") ??
_findError(ScannerErrorCode.EXPECTED_TOKEN, partialMatch: "')'");
if (parenError == null) {
return false;
}
var argList = _selectedNode(at: selectionOffset)
?.thisOrAncestorOfType<ArgumentList>();
argList ??= _selectedNode(at: parenError.offset)
?.thisOrAncestorOfType<ArgumentList>();
if (argList == null ||
argList.thisOrAncestorMatching((n) => n == node) == null) {
return false;
}
var previousInsertions = _lengthOfInsertions();
var loc = min(selectionOffset, argList.end - 1);
var delta = 1;
var semicolonError =
_findError(ParserErrorCode.EXPECTED_TOKEN, partialMatch: "';'");
if (semicolonError == null) {
loc += 1;
delta = 0;
}
_addInsertEdit(loc, ')');
if (semicolonError != null) {
_addInsertEdit(loc, ';');
}
var indent = utils.getLinePrefix(selectionOffset);
var exit = utils.getLineNext(selectionOffset);
_addInsertEdit(exit, indent + eol);
exit += indent.length + eol.length + previousInsertions;
_setCompletionAt(DartStatementCompletion.SIMPLE_ENTER, exit + delta);
return true;
}
bool _complete_simpleEnter() {
int offset;
if (errors.isNotEmpty) {
offset = selectionOffset;
} else {
var indent = utils.getLinePrefix(selectionOffset);
var loc = utils.getLineNext(selectionOffset);
_addInsertEdit(loc, indent + eol);
offset = loc + indent.length;
}
_setCompletionAt(DartStatementCompletion.SIMPLE_ENTER, offset);
return true;
}
bool _complete_simpleSemicolon(AstNode node) {
if (errors.length != 1) {
return false;
}
var error = _findError(ParserErrorCode.EXPECTED_TOKEN, partialMatch: "';'");
if (error != null) {
var previousInsertions = _lengthOfInsertions();
// TODO(messick) Fix this to find the correct place in all cases.
var insertOffset = error.offset + error.length;
_addInsertEdit(insertOffset, ';');
var offset = _appendNewlinePlusIndent() + 1 /*';'*/ + previousInsertions;
_setCompletionAt(DartStatementCompletion.SIMPLE_SEMICOLON, offset);
return true;
}
return false;
}
bool _complete_switchStatement(AstNode node) {
if (node is! SwitchStatement) {
return false;
}
SourceBuilder sb;
if (node.leftParenthesis.isSynthetic && node.rightParenthesis.isSynthetic) {
exitPosition = Position(file, node.switchKeyword.end + 2);
var src = utils.getNodeText(node);
if (src
.substring(node.switchKeyword.end - node.offset)
.startsWith(RegExp(r'[ \t]+'))) {
sb = SourceBuilder(file, node.switchKeyword.end + 1);
} else {
sb = SourceBuilder(file, node.switchKeyword.end);
sb.append(' ');
}
sb.append('()');
} else if (node.leftParenthesis.isSynthetic ||
node.rightParenthesis.isSynthetic) {
return false;
} else {
sb = SourceBuilder(file, node.rightParenthesis.offset + 1);
if (_isSyntheticExpression(node.expression)) {
exitPosition = Position(file, node.leftParenthesis.offset + 1);
}
}
if (node
.leftBracket.isSynthetic /*&& switchNode.rightBracket.isSynthetic*/) {
// See https://github.com/dart-lang/sdk/issues/29391
sb.append(' ');
_appendEmptyBraces(sb, exitPosition == null);
} else {
var member = _findInvalidElement(node.members);
if (member != null) {
if (member.colon.isSynthetic) {
var loc =
member is SwitchCase ? member.expression.end : member.keyword.end;
sb = SourceBuilder(file, loc);
sb.append(': ');
exitPosition = Position(file, loc + 2);
}
}
}
_insertBuilder(sb);
_setCompletion(DartStatementCompletion.COMPLETE_SWITCH_STMT);
return true;
}
bool _complete_tryStatement(AstNode node) {
if (node is! TryStatement) {
return false;
}
var addSpace = true;
if (node.body.leftBracket.isSynthetic) {
var src = utils.getNodeText(node);
SourceBuilder sb;
if (src
.substring(node.tryKeyword.end - node.offset)
.startsWith(RegExp(r'[ \t]+'))) {
// keywordSpace
sb = SourceBuilder(file, node.tryKeyword.end + 1);
} else {
// keywordOnly
sb = SourceBuilder(file, node.tryKeyword.end);
sb.append(' ');
}
_appendEmptyBraces(sb, true);
_insertBuilder(sb);
} else {
SourceBuilder? sb;
var catchNode = _findInvalidElement(node.catchClauses);
if (catchNode != null) {
var onKeyword = catchNode.onKeyword;
var exceptionType = catchNode.exceptionType;
if (onKeyword != null && exceptionType != null) {
if (exceptionType.length == 0 ||
_findError(CompileTimeErrorCode.NON_TYPE_IN_CATCH_CLAUSE,
partialMatch: "name 'catch") !=
null) {
var src = utils.getNodeText(catchNode);
if (src.startsWith(RegExp(r'on[ \t]+'))) {
if (src.startsWith(RegExp(r'on[ \t][ \t]+'))) {
// onSpaces
exitPosition = Position(file, onKeyword.end + 1);
sb = SourceBuilder(file, onKeyword.end + 2);
addSpace = false;
} else {
// onSpace
sb = SourceBuilder(file, onKeyword.end + 1);
sb.setExitOffset();
}
} else {
// onOnly
sb = SourceBuilder(file, onKeyword.end);
sb.append(' ');
sb.setExitOffset();
}
} else {
// onType
sb = SourceBuilder(file, exceptionType.end);
}
}
var catchKeyword = catchNode.catchKeyword;
if (catchKeyword != null) {
// catchOnly
var struct = _KeywordConditionBlockStructure(
catchKeyword,
catchNode.leftParenthesis!,
catchNode.exceptionParameter!,
catchNode.rightParenthesis!,
catchNode.body);
if (sb != null) {
// onCatch
_insertBuilder(sb);
}
sb = _complete_keywordCondition(node, struct);
if (sb == null) {
return false;
}
}
if (sb != null) {
if (catchNode.body.leftBracket.isSynthetic) {
// onOnly and others
if (addSpace) {
sb.append(' ');
}
_appendEmptyBraces(sb, exitPosition == null);
}
_insertBuilder(sb);
}
} else if (node.finallyKeyword != null) {
if (node.finallyBlock!.leftBracket.isSynthetic) {
// finallyOnly
sb = SourceBuilder(file, node.finallyKeyword!.end);
sb.append(' ');
_appendEmptyBraces(sb, true);
_insertBuilder(sb);
}
}
}
_setCompletion(DartStatementCompletion.COMPLETE_TRY_STMT);
return true;
}
bool _complete_variableDeclaration(AstNode node) {
if (node is! VariableDeclaration) {
return false;
}
_addInsertEdit(node.end, ';');
exitPosition = Position(file, _appendNewlinePlusIndentAt(node.end) + 1);
_setCompletion(DartStatementCompletion.COMPLETE_VARIABLE_DECLARATION);
return true;
}
bool _complete_whileStatement(AstNode node) {
if (node is! WhileStatement) {
return false;
}
var stmt = _KeywordConditionBlockStructure(node.whileKeyword,
node.leftParenthesis, node.condition, node.rightParenthesis, node.body);
return _complete_ifOrWhileStatement(
node, stmt, DartStatementCompletion.COMPLETE_WHILE_STMT);
}
engine.AnalysisError? _findError(ErrorCode code, {partialMatch}) {
return errors.firstWhereOrNull((err) =>
err.errorCode == code &&
(partialMatch == null ? true : err.message.contains(partialMatch)));
}
T? _findInvalidElement<T extends AstNode>(NodeList<T> list) {
return list.firstWhereOrNull((item) =>
selectionOffset >= item.offset && selectionOffset <= item.end);
}
void _insertBuilder(SourceBuilder builder, [int length = 0]) {
{
var range = SourceRange(builder.offset, length);
var text = builder.toString();
_addReplaceEdit(range, text);
}
// add exit position
{
var exitOffset = builder.exitOffset;
if (exitOffset != null) {
exitPosition = _newPosition(exitOffset);
}
}
}
bool _isEmptyBlock(AstNode stmt) {
return stmt is Block && stmt.statements.isEmpty;
}
bool _isEmptyStatement(AstNode? stmt) {
if (stmt is ExpressionStatement) {
var expression = stmt.expression;
if (expression is SimpleIdentifier) {
return expression.token.isSynthetic;
}
}
return stmt is EmptyStatement;
}
bool _isEmptyStatementOrEmptyBlock(AstNode stmt) {
return _isEmptyStatement(stmt) || _isEmptyBlock(stmt);
}
bool _isNonStatementDeclaration(AstNode n) {
if (n is! Declaration) {
return false;
}
if (n is! VariableDeclaration && n is! FunctionDeclaration) {
return true;
}
var p = n.parent;
return p is! Statement &&
p?.parent is! Statement &&
p?.parent?.parent is! Statement;
}
bool _isSyntheticExpression(Expression? expr) {
return expr is SimpleIdentifier && expr.isSynthetic;
}
int _lengthOfDeletions() {
if (change.edits.isEmpty) {
return 0;
}
var length = 0;
for (var edit in change.edits) {
for (var srcEdit in edit.edits) {
if (srcEdit.length > 0) {
length += srcEdit.length - srcEdit.replacement.length;
}
}
}
return length;
}
int _lengthOfInsertions() {
// Any _complete_*() that may follow changes made by _checkExpressions()
// must cache the result of this method and add that value to its
// exit position. That's assuming all edits are done in increasing position.
// There are currently no editing sequences that produce both insertions and
// deletions, but if there were this approach would have to be generalized.
if (change.edits.isEmpty) {
return 0;
}
var length = 0;
for (var edit in change.edits) {
for (var srcEdit in edit.edits) {
if (srcEdit.length == 0) {
length += srcEdit.replacement.length;
}
}
}
return length;
}
Position _newPosition(int offset) {
return Position(file, offset);
}
void _removeError(errorCode, {partialMatch}) {
var error = _findError(errorCode, partialMatch: partialMatch);
if (error != null) {
errors.remove(error);
}
}
AstNode? _selectedNode({int? at}) =>
NodeLocator(at ?? selectionOffset).searchWithin(unit);
void _setCompletion(StatementCompletionKind kind, [List? args]) {
assert(exitPosition != null);
change.selection = exitPosition;
change.message = formatList(kind.message, args);
linkedPositionGroups.values
.forEach((group) => change.addLinkedEditGroup(group));
completion = StatementCompletion(kind, change);
}
void _setCompletionAt(StatementCompletionKind kind, int offset,
[List? args]) {
exitPosition = _newPosition(offset);
_setCompletion(kind, args);
}
SourceBuilder _sourceBuilderAfterKeyword(AstNode node, Token keyword) {
SourceBuilder sb;
var text = _baseNodeText(node);
text = text.substring(keyword.offset - node.offset);
var len = keyword.length;
if (text.length == len || // onCatchComment
!text.substring(len, len + 1).contains(RegExp(r'[ \t]'))) {
sb = SourceBuilder(file, keyword.offset + len);
sb.append(' ');
} else {
sb = SourceBuilder(file, keyword.offset + len + 1);
}
return sb;
}
bool _statementHasValidBody(Token keyword, Statement body) {
// A "valid" body is either a non-synthetic block or a single statement
// on the same line as the parent statement, similar to dart_style.
if (body.isSynthetic) {
return false;
}
if (body is Block) {
var block = body;
return !block.leftBracket.isSynthetic;
}
return lineInfo.getLocation(keyword.offset) ==
lineInfo.getLocation(body.offset);
}
}
// Encapsulate common structure of if-statement and while-statement.
class _KeywordConditionBlockStructure {
final Token keyword;
final Token leftParenthesis, rightParenthesis;
final Expression condition;
final Statement? block;
_KeywordConditionBlockStructure(this.keyword, this.leftParenthesis,
this.condition, this.rightParenthesis, this.block);
int get offset => keyword.offset;
}