| // 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 '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/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/hint_codes.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'; |
| |
| /** |
| * An enumeration of possible statement completion kinds. |
| */ |
| class DartStatementCompletion { |
| static const NO_COMPLETION = |
| const StatementCompletionKind('No_COMPLETION', 'No completion available'); |
| static const SIMPLE_ENTER = const StatementCompletionKind( |
| 'SIMPLE_ENTER', "Insert a newline at the end of the current line"); |
| static const SIMPLE_SEMICOLON = const StatementCompletionKind( |
| 'SIMPLE_SEMICOLON', "Add a semicolon and newline"); |
| static const COMPLETE_CLASS_DECLARATION = const StatementCompletionKind( |
| 'COMPLETE_CLASS_DECLARATION', "Complete class declaration"); |
| static const COMPLETE_CONTROL_FLOW_BLOCK = const StatementCompletionKind( |
| 'COMPLETE_CONTROL_FLOW_BLOCK', "Complete control flow block"); |
| static const COMPLETE_DO_STMT = const StatementCompletionKind( |
| 'COMPLETE_DO_STMT', "Complete do-statement"); |
| static const COMPLETE_IF_STMT = const StatementCompletionKind( |
| 'COMPLETE_IF_STMT', "Complete if-statement"); |
| static const COMPLETE_FOR_STMT = const StatementCompletionKind( |
| 'COMPLETE_FOR_STMT', "Complete for-statement"); |
| static const COMPLETE_FOR_EACH_STMT = const StatementCompletionKind( |
| 'COMPLETE_FOR_EACH_STMT', "Complete for-each-statement"); |
| static const COMPLETE_FUNCTION_DECLARATION = const StatementCompletionKind( |
| 'COMPLETE_FUNCTION_DECLARATION', "Complete function declaration"); |
| static const COMPLETE_SWITCH_STMT = const StatementCompletionKind( |
| 'COMPLETE_SWITCH_STMT', "Complete switch-statement"); |
| static const COMPLETE_TRY_STMT = const StatementCompletionKind( |
| 'COMPLETE_TRY_STMT', "Complete try-statement"); |
| static const COMPLETE_VARIABLE_DECLARATION = const StatementCompletionKind( |
| 'COMPLETE_VARIABLE_DECLARATION', "Complete variable declaration"); |
| static const COMPLETE_WHILE_STMT = const 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 String file; |
| final LineInfo lineInfo; |
| final int selectionOffset; |
| final CompilationUnit unit; |
| final CompilationUnitElement unitElement; |
| final List<engine.AnalysisError> errors; |
| |
| StatementCompletionContext(this.file, this.lineInfo, this.selectionOffset, |
| this.unit, this.unitElement, this.errors) { |
| if (unitElement.context == null) { |
| throw new Error(); // not reached; see getStatementCompletion() |
| } |
| } |
| } |
| |
| /** |
| * 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 = new StatementCompletion( |
| DartStatementCompletion.NO_COMPLETION, new SourceChange("", edits: [])); |
| |
| final StatementCompletionContext statementContext; |
| final CorrectionUtils utils; |
| AstNode node; |
| StatementCompletion completion; |
| SourceChange change = new SourceChange('statement-completion'); |
| List<engine.AnalysisError> errors = []; |
| final Map<String, LinkedEditGroup> linkedPositionGroups = |
| <String, LinkedEditGroup>{}; |
| Position exitPosition = null; |
| |
| StatementCompletionProcessor(this.statementContext) |
| : utils = new CorrectionUtils(statementContext.unit); |
| |
| String get eol => utils.endOfLine; |
| |
| String get file => statementContext.file; |
| |
| LineInfo get lineInfo => statementContext.lineInfo; |
| |
| int get selectionOffset => statementContext.selectionOffset; |
| |
| Source get source => statementContext.unitElement.source; |
| |
| CompilationUnit get unit => statementContext.unit; |
| |
| CompilationUnitElement get unitElement => statementContext.unitElement; |
| |
| Future<StatementCompletion> compute() async { |
| // TODO(brianwilkerson) Determine whether this await is necessary. |
| await null; |
| node = _selectedNode(); |
| if (node == null) { |
| return NO_COMPLETION; |
| } |
| node = node |
| .getAncestor((n) => n is Statement || _isNonStatementDeclaration(n)); |
| if (node == null) { |
| return _complete_simpleEnter() ? completion : NO_COMPLETION; |
| } |
| if (node is Block) { |
| Block blockNode = node; |
| if (blockNode.statements.isNotEmpty) { |
| node = blockNode.statements.last; |
| } |
| } |
| if (_isEmptyStatementOrEmptyBlock(node)) { |
| node = node.parent; |
| } |
| for (engine.AnalysisError error in statementContext.errors) { |
| if (error.offset >= node.offset && error.offset <= node.end) { |
| if (error.errorCode is! HintCode) { |
| errors.add(error); |
| } |
| } |
| } |
| |
| _checkExpressions(); |
| if (node is Statement) { |
| if (errors.isEmpty) { |
| if (_complete_ifStatement() || |
| _complete_forStatement() || |
| _complete_forEachStatement() || |
| _complete_whileStatement() || |
| _complete_controlFlowBlock()) { |
| return completion; |
| } |
| } else { |
| if (_complete_ifStatement() || |
| _complete_doStatement() || |
| _complete_forStatement() || |
| _complete_forEachStatement() || |
| _complete_functionDeclarationStatement() || |
| _complete_switchStatement() || |
| _complete_tryStatement() || |
| _complete_whileStatement() || |
| _complete_controlFlowBlock() || |
| _complete_simpleSemicolon() || |
| _complete_methodCall()) { |
| return completion; |
| } |
| } |
| } else if (node is Declaration) { |
| if (errors.isNotEmpty) { |
| if (_complete_classDeclaration() || |
| _complete_functionDeclaration() || |
| _complete_variableDeclaration()) { |
| return completion; |
| } |
| } |
| } |
| if (_complete_simpleEnter()) { |
| return completion; |
| } |
| return NO_COMPLETION; |
| } |
| |
| void _addInsertEdit(int offset, String text) { |
| SourceEdit edit = new SourceEdit(offset, 0, text); |
| doSourceChange_addElementEdit(change, unitElement, edit); |
| } |
| |
| void _addReplaceEdit(SourceRange range, String text) { |
| SourceEdit edit = new SourceEdit(range.offset, range.length, text); |
| doSourceChange_addElementEdit(change, unitElement, edit); |
| } |
| |
| void _appendEmptyBraces(SourceBuilder sb, [bool needsExitMark = false]) { |
| sb.append('{'); |
| sb.append(eol); |
| String 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. |
| String indent = utils.getLinePrefix(offset); |
| int loc = utils.getLineNext(offset); |
| _addInsertEdit(loc, indent + eol); |
| return loc + indent.length; |
| } |
| |
| String _baseNodeText(AstNode astNode) { |
| String text = utils.getNodeText(astNode); |
| if (text.endsWith(eol)) { |
| text = text.substring(0, text.length - eol.length); |
| } |
| return text; |
| } |
| |
| void _checkExpressions() { |
| // Note: This may queue edits that have to be accounted for later. |
| // See _lengthOfInsertions(). |
| AstNode errorMatching(errorCode, {partialMatch = null}) { |
| var error = _findError(errorCode, partialMatch: partialMatch); |
| if (error == null) { |
| return null; |
| } |
| AstNode expr = _selectedNode(); |
| return (expr.getAncestor((n) => n is StringInterpolation) == null) |
| ? expr |
| : null; |
| } |
| |
| var expr = errorMatching(ScannerErrorCode.UNTERMINATED_STRING_LITERAL); |
| if (expr != null) { |
| String source = utils.getNodeText(expr); |
| String content = source; |
| int 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); |
| int 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.getAncestor((n) => n is ListLiteral); |
| if (expr != null) { |
| ListLiteral lit = expr; |
| if (lit.rightBracket.isSynthetic) { |
| String src = utils.getNodeText(expr).trim(); |
| int loc = expr.offset + src.length; |
| if (src.contains(eol)) { |
| String indent = utils.getNodePrefix(node); |
| _addInsertEdit(loc, ',' + eol + indent + ']'); |
| } else { |
| _addInsertEdit(loc, ']'); |
| } |
| _removeError(ParserErrorCode.EXPECTED_TOKEN, partialMatch: "']'"); |
| _removeError(ScannerErrorCode.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; |
| } |
| } |
| } |
| } |
| // 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() { |
| if (node is! ClassDeclaration) { |
| return false; |
| } |
| ClassDeclaration decl = node; |
| if (decl.leftBracket.isSynthetic && errors.length == 1) { |
| // The space before the left brace is assumed to exist, even if it does not. |
| SourceBuilder sb = new SourceBuilder(file, decl.end - 1); |
| sb.append(' '); |
| _appendEmptyBraces(sb, true); |
| _insertBuilder(sb); |
| _setCompletion(DartStatementCompletion.COMPLETE_CLASS_DECLARATION); |
| return true; |
| } |
| return false; |
| } |
| |
| bool _complete_controlFlowBlock() { |
| Expression expr = (node is ExpressionStatement) |
| ? (node as ExpressionStatement).expression |
| : (node is ReturnStatement |
| ? (node as ReturnStatement).expression |
| : null); |
| if (!(node is ReturnStatement || expr is ThrowExpression)) { |
| return false; |
| } |
| if (node.parent is! Block) { |
| return false; |
| } |
| AstNode outer = node.parent.parent; |
| if (!(outer is DoStatement || |
| outer is ForStatement || |
| outer is ForEachStatement || |
| outer is IfStatement || |
| outer is WhileStatement)) { |
| return false; |
| } |
| int previousInsertions = _lengthOfInsertions(); |
| int 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 as ReturnStatement).returnKeyword.end; |
| } else if (node is ExpressionStatement) { |
| insertOffset = |
| ((node as ExpressionStatement).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; |
| } |
| } |
| int offset = _appendNewlinePlusIndentAt(node.parent.end); |
| exitPosition = new Position(file, offset + delta + previousInsertions); |
| _setCompletion(DartStatementCompletion.COMPLETE_CONTROL_FLOW_BLOCK); |
| return true; |
| } |
| |
| bool _complete_doStatement() { |
| if (node is! DoStatement) { |
| return false; |
| } |
| DoStatement statement = node; |
| SourceBuilder sb = _sourceBuilderAfterKeyword(statement.doKeyword); |
| // I modified the code and ran the tests with both the old and new parser. |
| // Apparently the old parser sometimes sticks something other than 'while' |
| // into the whileKeyword field, which causes statement completion to throw |
| // an exception further downstream. |
| // TODO(danrubel): change `statement.whileKeyword?.lexeme == "while"` |
| // to `statement.whileKeyword != null` once the fasta parser is the default. |
| bool hasWhileKeyword = statement.whileKeyword?.lexeme == "while" && |
| !statement.whileKeyword.isSynthetic; |
| int exitDelta = 0; |
| if (!_statementHasValidBody(statement.doKeyword, statement.body)) { |
| String text = utils.getNodeText(statement.body); |
| int delta = 0; |
| if (text.startsWith(';')) { |
| delta = 1; |
| _addReplaceEdit(range.startLength(statement.body, delta), ''); |
| if (hasWhileKeyword) { |
| text = utils.getNodeText(statement); |
| if (text.indexOf(new RegExp(r'do\s*;\s*while')) == 0) { |
| int end = text.indexOf('while'); |
| int start = text.indexOf(';') + 1; |
| delta += end - start - 1; |
| _addReplaceEdit( |
| new SourceRange(start + statement.offset, end - start), ' '); |
| } |
| } |
| sb = new SourceBuilder(file, sb.offset + delta); |
| sb.append(' '); |
| } |
| _appendEmptyBraces(sb, |
| !(hasWhileKeyword && _isSyntheticExpression(statement.condition))); |
| if (delta != 0) { |
| exitDelta = sb.length - delta; |
| } |
| } else if (_isEmptyBlock(statement.body)) { |
| sb = new SourceBuilder(sb.file, statement.body.end); |
| } |
| SourceBuilder sb2; |
| if (hasWhileKeyword) { |
| var stmt = new _KeywordConditionBlockStructure( |
| statement.whileKeyword, |
| statement.leftParenthesis, |
| statement.condition, |
| statement.rightParenthesis, |
| null); |
| sb2 = _complete_keywordCondition(stmt); |
| if (sb2 == null) { |
| return false; |
| } |
| if (sb2.length == 0) { |
| // true if condition is '()' |
| if (exitPosition != null) { |
| if (statement.semicolon.isSynthetic) { |
| _insertBuilder(sb); |
| sb = new 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 = |
| new Position(exitPosition.file, exitPosition.offset + exitDelta); |
| } |
| _setCompletion(DartStatementCompletion.COMPLETE_DO_STMT); |
| return true; |
| } |
| |
| bool _complete_forEachStatement() { |
| if (node is! ForEachStatement) { |
| return false; |
| } |
| ForEachStatement forNode = node; |
| return _complete_forEachStatementRest( |
| forNode.forKeyword, |
| forNode.leftParenthesis, |
| forNode.identifier ?? forNode.loopVariable, |
| forNode.inKeyword, |
| forNode.iterable, |
| forNode.rightParenthesis, |
| forNode.body); |
| } |
| |
| bool _complete_forEachStatementRest( |
| 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. |
| } |
| SourceBuilder sb = new SourceBuilder(file, rightParenthesis.offset + 1); |
| String src = utils.getNodeText(node); |
| if (name == null) { |
| exitPosition = new Position(file, leftParenthesis.offset + 1); |
| src = src.substring(leftParenthesis.offset - node.offset); |
| if (src.startsWith(new RegExp(r'\(\s*in\s*\)'))) { |
| _addReplaceEdit( |
| range.startOffsetEndOffset( |
| leftParenthesis.offset + 1, rightParenthesis.offset), |
| ' in '); |
| } else if (src.startsWith(new RegExp(r'\(\s*in'))) { |
| _addReplaceEdit( |
| range.startOffsetEndOffset( |
| leftParenthesis.offset + 1, inKeyword.offset), |
| ' '); |
| } |
| } else if (iterable != null && _isSyntheticExpression(iterable)) { |
| exitPosition = new Position(file, rightParenthesis.offset + 1); |
| src = src.substring(inKeyword.offset - node.offset); |
| if (src.startsWith(new 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() { |
| if (node is! ForStatement) { |
| return false; |
| } |
| ForStatement forNode = node; |
| SourceBuilder sb; |
| int replacementLength = 0; |
| if (forNode.leftParenthesis.isSynthetic) { |
| if (!forNode.rightParenthesis.isSynthetic) { |
| return false; |
| } |
| // keywordOnly (unit test name suffix that exercises this branch) |
| sb = _sourceBuilderAfterKeyword(forNode.forKeyword); |
| sb.append('('); |
| sb.setExitOffset(); |
| sb.append(')'); |
| } else { |
| if (!forNode.rightSeparator.isSynthetic) { |
| // Fully-defined init, cond, updaters so nothing more needed here. |
| // emptyParts, noError |
| sb = new SourceBuilder(file, forNode.rightParenthesis.offset + 1); |
| } else if (!forNode.leftSeparator.isSynthetic) { |
| if (_isSyntheticExpression(forNode.condition)) { |
| String text = utils |
| .getNodeText(forNode) |
| .substring(forNode.leftSeparator.offset - forNode.offset); |
| Match match = |
| new RegExp(r';\s*(/\*.*\*/\s*)?\)[ \t]*').matchAsPrefix(text); |
| if (match != null) { |
| // emptyCondition, emptyInitializersEmptyCondition |
| replacementLength = match.end - match.start; |
| sb = new SourceBuilder(file, forNode.leftSeparator.offset); |
| sb.append('; ${match.group(1) == null ? '' : match.group(1)}; )'); |
| String 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(forNode.leftSeparator.offset + 2); |
| } else { |
| return false; // Line comment in condition |
| } |
| } else { |
| // emptyUpdaters |
| sb = new SourceBuilder(file, forNode.rightParenthesis.offset); |
| replacementLength = 1; |
| sb.append('; )'); |
| exitPosition = _newPosition(forNode.rightSeparator.offset + 2); |
| } |
| } else if (_isSyntheticExpression(forNode.initialization)) { |
| // emptyInitializers |
| exitPosition = _newPosition(forNode.rightParenthesis.offset); |
| sb = new SourceBuilder(file, forNode.rightParenthesis.offset); |
| } else if (forNode.initialization is SimpleIdentifier && |
| forNode.initialization.beginToken.lexeme == 'in') { |
| // looks like a for/each statement missing the loop variable |
| return _complete_forEachStatementRest( |
| forNode.forKeyword, |
| forNode.leftParenthesis, |
| null, |
| forNode.initialization.beginToken, |
| null, |
| forNode.rightParenthesis, |
| forNode.body); |
| } else { |
| int start = forNode.condition.offset + forNode.condition.length; |
| String text = |
| utils.getNodeText(forNode).substring(start - forNode.offset); |
| if (text.startsWith(new RegExp(r'\s*\)'))) { |
| // missingLeftSeparator |
| int end = text.indexOf(')'); |
| sb = new SourceBuilder(file, start); |
| _addReplaceEdit(new SourceRange(start, end), '; ; '); |
| exitPosition = new Position(file, start - (end - '; '.length)); |
| } else { |
| // Not possible; any comment following init is attached to init. |
| exitPosition = _newPosition(forNode.rightParenthesis.offset); |
| sb = new SourceBuilder(file, forNode.rightParenthesis.offset); |
| } |
| } |
| } |
| if (!_statementHasValidBody(forNode.forKeyword, forNode.body)) { |
| // keywordOnly, noError |
| sb.append(' '); |
| _appendEmptyBraces(sb, true /*exitPosition == null*/); |
| } else if (forNode.body is Block) { |
| Block body = forNode.body; |
| 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_functionDeclaration() { |
| if (node is! MethodDeclaration && node is! FunctionDeclaration) { |
| return false; |
| } |
| bool 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) { |
| FunctionDeclaration func = node; |
| paramListEnd = computeExitPos(func.functionExpression.parameters); |
| } else { |
| MethodDeclaration meth = node; |
| paramListEnd = computeExitPos(meth.parameters); |
| } |
| SourceBuilder sb = new SourceBuilder(file, paramListEnd); |
| if (needsParen) { |
| sb.append(')'); |
| } |
| sb.append(' '); |
| _appendEmptyBraces(sb, true); |
| _insertBuilder(sb); |
| _setCompletion(DartStatementCompletion.COMPLETE_FUNCTION_DECLARATION); |
| return true; |
| } |
| |
| bool _complete_functionDeclarationStatement() { |
| if (node is! FunctionDeclarationStatement) { |
| return false; |
| } |
| var error = _findError(ParserErrorCode.EXPECTED_TOKEN, partialMatch: "';'"); |
| if (error != null) { |
| FunctionDeclarationStatement stmt = node; |
| String src = utils.getNodeText(stmt); |
| int insertOffset = stmt.functionDeclaration.end - 1; |
| if (stmt.functionDeclaration.functionExpression.body |
| is ExpressionFunctionBody) { |
| ExpressionFunctionBody fnb = |
| stmt.functionDeclaration.functionExpression.body; |
| int fnbOffset = fnb.functionDefinition.offset; |
| String fnSrc = src.substring(fnbOffset - stmt.offset); |
| if (!fnSrc.startsWith('=>')) { |
| return false; |
| } |
| int delta = 0; |
| if (fnb.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( |
| _KeywordConditionBlockStructure statement, StatementCompletionKind kind) { |
| if (_statementHasValidBody(statement.keyword, statement.block)) { |
| return false; |
| } |
| SourceBuilder sb = _complete_keywordCondition(statement); |
| if (sb == null) { |
| return false; |
| } |
| int 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() { |
| if (node is! IfStatement) { |
| return false; |
| } |
| IfStatement ifNode = node; |
| if (ifNode.elseKeyword != null) { |
| if (selectionOffset >= ifNode.elseKeyword.end && |
| _isEmptyStatement(ifNode.elseStatement)) { |
| SourceBuilder sb = new SourceBuilder(file, selectionOffset); |
| String src = utils.getNodeText(ifNode); |
| if (!src |
| .substring(ifNode.elseKeyword.end - node.offset) |
| .startsWith(new RegExp(r'[ \t]'))) { |
| sb.append(' '); |
| } |
| _appendEmptyBraces(sb, true); |
| _insertBuilder(sb); |
| _setCompletion(DartStatementCompletion.COMPLETE_IF_STMT); |
| return true; |
| } |
| return false; |
| } |
| var stmt = new _KeywordConditionBlockStructure( |
| ifNode.ifKeyword, |
| ifNode.leftParenthesis, |
| ifNode.condition, |
| ifNode.rightParenthesis, |
| ifNode.thenStatement); |
| return _complete_ifOrWhileStatement( |
| stmt, DartStatementCompletion.COMPLETE_IF_STMT); |
| } |
| |
| SourceBuilder _complete_keywordCondition( |
| _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(statement.keyword); |
| sb.append('('); |
| sb.setExitOffset(); |
| sb.append(')'); |
| } else { |
| if (_isSyntheticExpression(statement.condition)) { |
| exitPosition = _newPosition(statement.leftParenthesis.offset + 1); |
| sb = new SourceBuilder(file, statement.rightParenthesis.offset + 1); |
| } else if (statement.rightParenthesis.isSynthetic) { |
| sb = new SourceBuilder(file, statement.condition.end); |
| sb.append(')'); |
| } else { |
| int afterParen = statement.rightParenthesis.offset + 1; |
| if (utils |
| .getNodeText(node) |
| .substring(afterParen - node.offset) |
| .startsWith(new RegExp(r'[ \t]'))) { |
| _addReplaceEdit(new SourceRange(afterParen, 1), ''); |
| sb = new SourceBuilder(file, afterParen + 1); |
| } else { |
| sb = new SourceBuilder(file, afterParen); |
| } |
| } |
| } |
| return sb; |
| } |
| |
| bool _complete_methodCall() { |
| var parenError = |
| _findError(ParserErrorCode.EXPECTED_TOKEN, partialMatch: "')'") ?? |
| _findError(ScannerErrorCode.EXPECTED_TOKEN, partialMatch: "')'"); |
| if (parenError == null) { |
| return false; |
| } |
| AstNode argList = _selectedNode(at: selectionOffset) |
| .getAncestor((n) => n is ArgumentList); |
| if (argList == null) { |
| argList = _selectedNode(at: parenError.offset) |
| .getAncestor((n) => n is ArgumentList); |
| } |
| if (argList?.getAncestor((n) => n == node) == null) { |
| return false; |
| } |
| int previousInsertions = _lengthOfInsertions(); |
| int loc = min(selectionOffset, argList.end - 1); |
| int delta = 1; |
| var semicolonError = |
| _findError(ParserErrorCode.EXPECTED_TOKEN, partialMatch: "';'"); |
| if (semicolonError == null) { |
| loc += 1; |
| delta = 0; |
| } |
| _addInsertEdit(loc, ')'); |
| if (semicolonError != null) { |
| _addInsertEdit(loc, ';'); |
| } |
| String indent = utils.getLinePrefix(selectionOffset); |
| int 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.isEmpty) { |
| offset = selectionOffset; |
| } else { |
| String indent = utils.getLinePrefix(selectionOffset); |
| int loc = utils.getLineNext(selectionOffset); |
| _addInsertEdit(loc, indent + eol); |
| offset = loc + indent.length; |
| } |
| _setCompletionAt(DartStatementCompletion.SIMPLE_ENTER, offset); |
| return true; |
| } |
| |
| bool _complete_simpleSemicolon() { |
| if (errors.length != 1) { |
| return false; |
| } |
| var error = _findError(ParserErrorCode.EXPECTED_TOKEN, partialMatch: "';'"); |
| if (error != null) { |
| int previousInsertions = _lengthOfInsertions(); |
| // TODO(messick) Fix this to find the correct place in all cases. |
| int insertOffset = error.offset + error.length; |
| _addInsertEdit(insertOffset, ';'); |
| int offset = _appendNewlinePlusIndent() + 1 /*';'*/ + previousInsertions; |
| _setCompletionAt(DartStatementCompletion.SIMPLE_SEMICOLON, offset); |
| return true; |
| } |
| return false; |
| } |
| |
| bool _complete_switchStatement() { |
| if (node is! SwitchStatement) { |
| return false; |
| } |
| SourceBuilder sb; |
| SwitchStatement switchNode = node; |
| if (switchNode.leftParenthesis.isSynthetic && |
| switchNode.rightParenthesis.isSynthetic) { |
| exitPosition = new Position(file, switchNode.switchKeyword.end + 2); |
| String src = utils.getNodeText(switchNode); |
| if (src |
| .substring(switchNode.switchKeyword.end - switchNode.offset) |
| .startsWith(new RegExp(r'[ \t]+'))) { |
| sb = new SourceBuilder(file, switchNode.switchKeyword.end + 1); |
| } else { |
| sb = new SourceBuilder(file, switchNode.switchKeyword.end); |
| sb.append(' '); |
| } |
| sb.append('()'); |
| } else if (switchNode.leftParenthesis.isSynthetic || |
| switchNode.rightParenthesis.isSynthetic) { |
| return false; |
| } else { |
| sb = new SourceBuilder(file, switchNode.rightParenthesis.offset + 1); |
| if (_isSyntheticExpression(switchNode.expression)) { |
| exitPosition = |
| new Position(file, switchNode.leftParenthesis.offset + 1); |
| } |
| } |
| if (switchNode |
| .leftBracket.isSynthetic /*&& switchNode.rightBracket.isSynthetic*/) { |
| // See https://github.com/dart-lang/sdk/issues/29391 |
| sb.append(' '); |
| _appendEmptyBraces(sb, exitPosition == null); |
| } else { |
| SwitchMember member = _findInvalidElement(switchNode.members); |
| if (member != null) { |
| if (member.colon.isSynthetic) { |
| int loc = |
| member is SwitchCase ? member.expression.end : member.keyword.end; |
| sb = new SourceBuilder(file, loc); |
| sb.append(': '); |
| exitPosition = new Position(file, loc + 2); |
| } |
| } |
| } |
| _insertBuilder(sb); |
| _setCompletion(DartStatementCompletion.COMPLETE_SWITCH_STMT); |
| return true; |
| } |
| |
| bool _complete_tryStatement() { |
| if (node is! TryStatement) { |
| return false; |
| } |
| TryStatement tryNode = node; |
| SourceBuilder sb; |
| CatchClause catchNode; |
| bool addSpace = true; |
| if (tryNode.body.leftBracket.isSynthetic) { |
| String src = utils.getNodeText(tryNode); |
| if (src |
| .substring(tryNode.tryKeyword.end - tryNode.offset) |
| .startsWith(new RegExp(r'[ \t]+'))) { |
| // keywordSpace |
| sb = new SourceBuilder(file, tryNode.tryKeyword.end + 1); |
| } else { |
| // keywordOnly |
| sb = new SourceBuilder(file, tryNode.tryKeyword.end); |
| sb.append(' '); |
| } |
| _appendEmptyBraces(sb, true); |
| _insertBuilder(sb); |
| sb = null; |
| } else if ((catchNode = _findInvalidElement(tryNode.catchClauses)) != |
| null) { |
| if (catchNode.onKeyword != null) { |
| if (catchNode.exceptionType.length == 0 || |
| null != |
| _findError(StaticWarningCode.NON_TYPE_IN_CATCH_CLAUSE, |
| partialMatch: "name 'catch")) { |
| String src = utils.getNodeText(catchNode); |
| if (src.startsWith(new RegExp(r'on[ \t]+'))) { |
| if (src.startsWith(new RegExp(r'on[ \t][ \t]+'))) { |
| // onSpaces |
| exitPosition = new Position(file, catchNode.onKeyword.end + 1); |
| sb = new SourceBuilder(file, catchNode.onKeyword.end + 2); |
| addSpace = false; |
| } else { |
| // onSpace |
| sb = new SourceBuilder(file, catchNode.onKeyword.end + 1); |
| sb.setExitOffset(); |
| } |
| } else { |
| // onOnly |
| sb = new SourceBuilder(file, catchNode.onKeyword.end); |
| sb.append(' '); |
| sb.setExitOffset(); |
| } |
| } else { |
| // onType |
| sb = new SourceBuilder(file, catchNode.exceptionType.end); |
| } |
| } |
| if (catchNode.catchKeyword != null) { |
| // catchOnly |
| var struct = new _KeywordConditionBlockStructure( |
| catchNode.catchKeyword, |
| catchNode.leftParenthesis, |
| catchNode.exceptionParameter, |
| catchNode.rightParenthesis, |
| catchNode.body); |
| if (sb != null) { |
| // onCatch |
| _insertBuilder(sb); |
| } |
| sb = _complete_keywordCondition(struct); |
| if (sb == null) { |
| return false; |
| } |
| } |
| if (catchNode.body.leftBracket.isSynthetic) { |
| // onOnly and others |
| if (addSpace) { |
| sb.append(' '); |
| } |
| _appendEmptyBraces(sb, exitPosition == null); |
| } |
| _insertBuilder(sb); |
| } else if (tryNode.finallyKeyword != null) { |
| if (tryNode.finallyBlock.leftBracket.isSynthetic) { |
| // finallyOnly |
| sb = new SourceBuilder(file, tryNode.finallyKeyword.end); |
| sb.append(' '); |
| _appendEmptyBraces(sb, true); |
| _insertBuilder(sb); |
| } |
| } |
| _setCompletion(DartStatementCompletion.COMPLETE_TRY_STMT); |
| return true; |
| } |
| |
| bool _complete_variableDeclaration() { |
| if (node is! VariableDeclaration) { |
| return false; |
| } |
| _addInsertEdit(node.end, ';'); |
| exitPosition = new Position(file, _appendNewlinePlusIndentAt(node.end) + 1); |
| _setCompletion(DartStatementCompletion.COMPLETE_VARIABLE_DECLARATION); |
| return true; |
| } |
| |
| bool _complete_whileStatement() { |
| if (node is! WhileStatement) { |
| return false; |
| } |
| WhileStatement whileNode = node; |
| if (whileNode != null) { |
| var stmt = new _KeywordConditionBlockStructure( |
| whileNode.whileKeyword, |
| whileNode.leftParenthesis, |
| whileNode.condition, |
| whileNode.rightParenthesis, |
| whileNode.body); |
| return _complete_ifOrWhileStatement( |
| stmt, DartStatementCompletion.COMPLETE_WHILE_STMT); |
| } |
| return false; |
| } |
| |
| engine.AnalysisError _findError(ErrorCode code, {partialMatch: null}) { |
| return errors.firstWhere( |
| (err) => |
| err.errorCode == code && |
| (partialMatch == null ? true : err.message.contains(partialMatch)), |
| orElse: () => null); |
| } |
| |
| T _findInvalidElement<T extends AstNode>(NodeList<T> list) { |
| return list.firstWhere( |
| (item) => selectionOffset >= item.offset && selectionOffset <= item.end, |
| orElse: () => null); |
| } |
| |
| LinkedEditGroup _getLinkedPosition(String groupId) { |
| LinkedEditGroup group = linkedPositionGroups[groupId]; |
| if (group == null) { |
| group = new LinkedEditGroup.empty(); |
| linkedPositionGroups[groupId] = group; |
| } |
| return group; |
| } |
| |
| void _insertBuilder(SourceBuilder builder, [int length = 0]) { |
| { |
| SourceRange range = new SourceRange(builder.offset, length); |
| String text = builder.toString(); |
| _addReplaceEdit(range, text); |
| } |
| // add linked positions |
| builder.linkedPositionGroups.forEach((String id, LinkedEditGroup group) { |
| LinkedEditGroup fixGroup = _getLinkedPosition(id); |
| group.positions.forEach((Position position) { |
| fixGroup.addPosition(position, group.length); |
| }); |
| group.suggestions.forEach((LinkedEditSuggestion suggestion) { |
| fixGroup.addSuggestion(suggestion); |
| }); |
| }); |
| // add exit position |
| { |
| int 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) { |
| Expression 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; |
| } |
| AstNode p = n.parent; |
| return p is! Statement && p?.parent is! Statement; |
| } |
| |
| bool _isSyntheticExpression(Expression expr) { |
| return expr is SimpleIdentifier && expr.isSynthetic; |
| } |
| |
| int _lengthOfDeletions() { |
| if (change.edits.isEmpty) { |
| return 0; |
| } |
| int length = 0; |
| for (SourceFileEdit edit in change.edits) { |
| for (SourceEdit 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; |
| } |
| int length = 0; |
| for (SourceFileEdit edit in change.edits) { |
| for (SourceEdit srcEdit in edit.edits) { |
| if (srcEdit.length == 0) { |
| length += srcEdit.replacement.length; |
| } |
| } |
| } |
| return length; |
| } |
| |
| Position _newPosition(int offset) { |
| return new Position(file, offset); |
| } |
| |
| void _removeError(errorCode, {partialMatch = null}) { |
| var error = _findError(errorCode, partialMatch: partialMatch); |
| if (error != null) { |
| errors.remove(error); |
| } |
| } |
| |
| AstNode _selectedNode({int at: null}) => |
| new NodeLocator(at == null ? selectionOffset : at).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 = new StatementCompletion(kind, change); |
| } |
| |
| void _setCompletionAt(StatementCompletionKind kind, int offset, [List args]) { |
| exitPosition = _newPosition(offset); |
| _setCompletion(kind, args); |
| } |
| |
| SourceBuilder _sourceBuilderAfterKeyword(Token keyword) { |
| SourceBuilder sb; |
| String text = _baseNodeText(node); |
| text = text.substring(keyword.offset - node.offset); |
| int len = keyword.length; |
| if (text.length == len || // onCatchComment |
| !text.substring(len, len + 1).contains(new RegExp(r'[ \t]'))) { |
| sb = new SourceBuilder(file, keyword.offset + len); |
| sb.append(' '); |
| } else { |
| sb = new 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) { |
| Block 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; |
| } |