| // 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. | |
| // Many functions here are mostly camelcase, with an occasional underscore to | |
| // separate phrases. | |
| // ignore_for_file: non_constant_identifier_names | |
| 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_plugin/edit/correction_utils.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/diagnostic/diagnostic.dart' as engine; | |
| import 'package:analyzer/error/error.dart'; | |
| import 'package:analyzer/source/line_info.dart'; | |
| import 'package:analyzer/source/source.dart'; | |
| import 'package:analyzer/source/source_range.dart'; | |
| import 'package:analyzer/src/diagnostic/diagnostic.dart' as diag; | |
| import 'package:analyzer/src/generated/java_core.dart'; | |
| import 'package:analyzer_plugin/utilities/range_factory.dart'; | |
| import 'package:collection/collection.dart'; | |
| /// An enumeration of possible statement completion kinds. | |
| abstract final class DartStatementCompletion { | |
| static const _noCompletion = StatementCompletionKind( | |
| 'No_COMPLETION', | |
| 'No completion available', | |
| ); | |
| static const _simpleEnter = StatementCompletionKind( | |
| 'SIMPLE_ENTER', | |
| 'Insert a newline at the end of the current line', | |
| ); | |
| static const _simpleSemicolon = StatementCompletionKind( | |
| 'SIMPLE_SEMICOLON', | |
| 'Add a semicolon and newline', | |
| ); | |
| static const _completeClassDeclaration = StatementCompletionKind( | |
| 'COMPLETE_CLASS_DECLARATION', | |
| 'Complete class declaration', | |
| ); | |
| static const _completeControlFlowBlock = StatementCompletionKind( | |
| 'COMPLETE_CONTROL_FLOW_BLOCK', | |
| 'Complete control flow block', | |
| ); | |
| static const _completeDoStmt = StatementCompletionKind( | |
| 'COMPLETE_DO_STMT', | |
| 'Complete do-statement', | |
| ); | |
| static const _completeIfStmt = StatementCompletionKind( | |
| 'COMPLETE_IF_STMT', | |
| 'Complete if-statement', | |
| ); | |
| static const _completeForStmt = StatementCompletionKind( | |
| 'COMPLETE_FOR_STMT', | |
| 'Complete for-statement', | |
| ); | |
| static const _completeForEachStmt = StatementCompletionKind( | |
| 'COMPLETE_FOR_EACH_STMT', | |
| 'Complete for-each-statement', | |
| ); | |
| static const _completeFunctionDeclaration = StatementCompletionKind( | |
| 'COMPLETE_FUNCTION_DECLARATION', | |
| 'Complete function declaration', | |
| ); | |
| static const _completeSwitchStmt = StatementCompletionKind( | |
| 'COMPLETE_SWITCH_STMT', | |
| 'Complete switch-statement', | |
| ); | |
| static const _completeTryStmt = StatementCompletionKind( | |
| 'COMPLETE_TRY_STMT', | |
| 'Complete try-statement', | |
| ); | |
| static const _completeVariableDeclaration = StatementCompletionKind( | |
| 'COMPLETE_VARIABLE_DECLARATION', | |
| 'Complete variable declaration', | |
| ); | |
| static const _completeWhiteStmt = 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]. | |
| new(this.kind, this.change); | |
| } | |
| /// The context for computing a statement completion. | |
| class StatementCompletionContext { | |
| final ResolvedUnitResult resolveResult; | |
| final int selectionOffset; | |
| new(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 new(this.name, this.message); | |
| @override | |
| String toString() => name; | |
| } | |
| /// The computer for Dart statement completions. | |
| class StatementCompletionProcessor { | |
| static final _noCompletion = StatementCompletion( | |
| DartStatementCompletion._noCompletion, | |
| 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.Diagnostic> diagnostics = []; | |
| final Map<String, LinkedEditGroup> linkedPositionGroups = | |
| <String, LinkedEditGroup>{}; | |
| Position? exitPosition; | |
| new(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 => unitFragment.source; | |
| CompilationUnit get unit => statementContext.resolveResult.unit; | |
| LibraryFragment get unitFragment => unit.declaredFragment!; | |
| Future<StatementCompletion> compute() async { | |
| var node = _selectedNode(); | |
| if (node == null) { | |
| return _noCompletion; | |
| } | |
| node = node.thisOrAncestorMatching( | |
| (n) => n is Statement || _isNonStatementDeclaration(n), | |
| ); | |
| if (node == null) { | |
| return _complete_simpleEnter() ? completion! : _noCompletion; | |
| } | |
| if (node is Block) { | |
| if (node.statements.isNotEmpty) { | |
| node = node.statements.last; | |
| } | |
| } | |
| if (_isEmptyStatementOrEmptyBlock(node)) { | |
| node = node.parent!; | |
| } | |
| for (var diagnostic in statementContext.resolveResult.diagnostics) { | |
| if (diagnostic.offset >= node.offset && diagnostic.offset <= node.end) { | |
| if (diagnostic.diagnosticCode.type == DiagnosticType.SYNTACTIC_ERROR) { | |
| diagnostics.add(diagnostic); | |
| } | |
| } | |
| } | |
| _checkExpressions(node); | |
| if (node is Statement) { | |
| if (diagnostics.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 (diagnostics.isNotEmpty) { | |
| if (_complete_classDeclaration(node) || | |
| _complete_variableDeclaration(node) || | |
| _complete_simpleSemicolon(node) || | |
| _complete_functionDeclaration(node)) { | |
| return completion!; | |
| } | |
| } | |
| } | |
| if (_complete_simpleEnter()) { | |
| return completion!; | |
| } | |
| return _noCompletion; | |
| } | |
| void _addInsertEdit(int offset, String text) { | |
| var edit = SourceEdit(offset, 0, text); | |
| doSourceChange_addFragmentEdit(change, unitFragment, 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(unitFragment.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.oneIndent); | |
| 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? diagnosticMatching( | |
| DiagnosticCode diagnosticCode, { | |
| Pattern? partialMatch, | |
| }) { | |
| var error = _findDiagnostic(diagnosticCode, partialMatch: partialMatch); | |
| if (error == null) { | |
| return null; | |
| } | |
| var expr = _selectedNode(); | |
| return (expr?.thisOrAncestorOfType<StringInterpolation>() == null) | |
| ? expr | |
| : null; | |
| } | |
| var expr = diagnosticMatching(diag.unterminatedStringLiteral); | |
| 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(diag.unterminatedStringLiteral); | |
| _addInsertEdit(loc, delimiter); | |
| } | |
| expr = | |
| diagnosticMatching(diag.expectedToken, partialMatch: "']'") ?? | |
| diagnosticMatching(diag.expectedToken, 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(diag.expectedToken, 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; | |
| } | |
| var body = node.body; | |
| if (body is! BlockClassBody) { | |
| return false; | |
| } | |
| if (body.leftBracket.isSynthetic && diagnostics.length == 1) { | |
| // The space before the left brace is assumed to exist, even if it does not. | |
| var sb = SourceBuilder(file, node.end - eol.length); | |
| sb.append(' '); | |
| _appendEmptyBraces(sb, true); | |
| _insertBuilder(sb); | |
| _setCompletion(DartStatementCompletion._completeClassDeclaration); | |
| 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 (diagnostics.isNotEmpty) { | |
| var error = _findDiagnostic(diag.expectedToken, 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._completeControlFlowBlock); | |
| 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 '()' | |
| var 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._completeDoStmt); | |
| 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._completeForEachStmt); | |
| 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[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 | |
| diagnostics = []; // Ignore errors; they are for previous statement. | |
| return false; // If cursor is after closing brace just add newline. | |
| } | |
| } | |
| _insertBuilder(sb, replacementLength); | |
| _setCompletion(DartStatementCompletion._completeForStmt); | |
| 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 = _findDiagnostic(diag.missingClosingParenthesis); | |
| if (error != null) { | |
| return error.offset - 1; | |
| } | |
| } | |
| return node.end - eol.length; | |
| } | |
| 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._completeFunctionDeclaration); | |
| return true; | |
| } | |
| bool _complete_functionDeclarationStatement(AstNode node) { | |
| if (node is! FunctionDeclarationStatement) { | |
| return false; | |
| } | |
| var error = _findDiagnostic(diag.expectedToken, partialMatch: "';'"); | |
| if (error != null) { | |
| var src = utils.getNodeText(node); | |
| var insertOffset = node.functionDeclaration.end - eol.length; | |
| 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._simpleSemicolon, | |
| 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._completeIfStmt); | |
| return true; | |
| } | |
| return false; | |
| } | |
| var stmt = _KeywordConditionBlockStructure( | |
| node.ifKeyword, | |
| node.leftParenthesis, | |
| node.expression, | |
| node.rightParenthesis, | |
| node.thenStatement, | |
| ); | |
| return _complete_ifOrWhileStatement( | |
| node, | |
| stmt, | |
| DartStatementCompletion._completeIfStmt, | |
| ); | |
| } | |
| 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 = _findDiagnostic(diag.expectedToken, partialMatch: "')'"); | |
| if (parenError == null) { | |
| return false; | |
| } | |
| var argList = | |
| _selectedNode(at: selectionOffset) | |
| ?.thisOrAncestorOfType<ArgumentList>() ?? | |
| _selectedNode(at: parenError.offset) | |
| ?.thisOrAncestorOfType<ArgumentList>(); | |
| if (argList == null || | |
| argList.thisOrAncestorMatching((n) => n == node) == null) { | |
| return false; | |
| } | |
| var loc = min(selectionOffset, argList.end); | |
| _addInsertEdit(loc, ')'); | |
| var semicolonError = _findDiagnostic( | |
| diag.expectedToken, | |
| partialMatch: "';'", | |
| ); | |
| if (semicolonError != null) { | |
| _addInsertEdit(loc, ';'); | |
| } | |
| var indent = utils.getLinePrefix(selectionOffset); | |
| var exit = utils.getLineNext(selectionOffset); | |
| var previousInsertions = _lengthOfInsertions(); | |
| _addInsertEdit(exit, indent + eol); | |
| exit += indent.length + previousInsertions; | |
| _setCompletionAt(DartStatementCompletion._simpleEnter, exit); | |
| return true; | |
| } | |
| bool _complete_simpleEnter() { | |
| int offset; | |
| if (diagnostics.isNotEmpty) { | |
| offset = selectionOffset; | |
| } else { | |
| var indent = utils.getLinePrefix(selectionOffset); | |
| var loc = utils.getLineNext(selectionOffset); | |
| _addInsertEdit(loc, indent + eol); | |
| offset = loc + indent.length; | |
| } | |
| _setCompletionAt(DartStatementCompletion._simpleEnter, offset); | |
| return true; | |
| } | |
| bool _complete_simpleSemicolon(AstNode node) { | |
| if (diagnostics.length != 1) { | |
| return false; | |
| } | |
| var error = _findDiagnostic(diag.expectedToken, 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._simpleSemicolon, 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._completeSwitchStmt); | |
| 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 || | |
| _findDiagnostic( | |
| diag.nonTypeInCatchClause, | |
| 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._completeTryStmt); | |
| 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._completeVariableDeclaration); | |
| 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._completeWhiteStmt, | |
| ); | |
| } | |
| engine.Diagnostic? _findDiagnostic( | |
| DiagnosticCode code, { | |
| Pattern? partialMatch, | |
| }) { | |
| return diagnostics.firstWhereOrNull( | |
| (d) => | |
| d.diagnosticCode == code && | |
| (partialMatch == null || d.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(AstNode? 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(DiagnosticCode diagnosticCode, {Pattern? partialMatch}) { | |
| var error = _findDiagnostic(diagnosticCode, partialMatch: partialMatch); | |
| if (error != null) { | |
| diagnostics.remove(error); | |
| } | |
| } | |
| AstNode? _selectedNode({int? at}) => | |
| unit.nodeCovering(offset: at ?? selectionOffset); | |
| void _setCompletion(StatementCompletionKind kind) { | |
| assert(exitPosition != null); | |
| change.selection = exitPosition; | |
| change.message = formatList(kind.message, null); | |
| for (var group in linkedPositionGroups.values) { | |
| change.addLinkedEditGroup(group); | |
| } | |
| completion = StatementCompletion(kind, change); | |
| } | |
| void _setCompletionAt(StatementCompletionKind kind, int offset) { | |
| exitPosition = _newPosition(offset); | |
| _setCompletion(kind); | |
| } | |
| 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 AstNode condition; | |
| final Statement? block; | |
| new( | |
| this.keyword, | |
| this.leftParenthesis, | |
| this.condition, | |
| this.rightParenthesis, | |
| this.block, | |
| ); | |
| int get offset => keyword.offset; | |
| } |