| // Copyright (c) 2014, the Dart project authors. Please see the AUTHORS file |
| // for details. All rights reserved. Use of this source code is governed by a |
| // BSD-style license that can be found in the LICENSE file. |
| |
| import 'package:analysis_server/src/protocol_server.dart' hide Element; |
| import 'package:analysis_server/src/services/correction/name_suggestion.dart'; |
| import 'package:analysis_server/src/services/correction/selection_analyzer.dart'; |
| import 'package:analysis_server/src/services/correction/statement_analyzer.dart'; |
| import 'package:analysis_server/src/services/correction/status.dart'; |
| import 'package:analysis_server/src/services/correction/util.dart'; |
| import 'package:analysis_server/src/services/refactoring/naming_conventions.dart'; |
| import 'package:analysis_server/src/services/refactoring/refactoring.dart'; |
| import 'package:analysis_server/src/services/refactoring/refactoring_internal.dart'; |
| import 'package:analysis_server/src/services/refactoring/rename_class_member.dart'; |
| import 'package:analysis_server/src/services/refactoring/rename_unit_member.dart'; |
| import 'package:analysis_server/src/services/refactoring/visible_ranges_computer.dart'; |
| import 'package:analysis_server/src/services/search/search_engine.dart'; |
| import 'package:analysis_server/src/utilities/extensions/ast.dart'; |
| import 'package:analyzer/dart/analysis/features.dart'; |
| import 'package:analyzer/dart/analysis/results.dart'; |
| import 'package:analyzer/dart/ast/ast.dart'; |
| import 'package:analyzer/dart/ast/visitor.dart'; |
| import 'package:analyzer/dart/element/element.dart'; |
| import 'package:analyzer/dart/element/type.dart'; |
| import 'package:analyzer/dart/element/type_system.dart'; |
| import 'package:analyzer/src/dart/analysis/session_helper.dart'; |
| import 'package:analyzer/src/dart/ast/extensions.dart'; |
| import 'package:analyzer/src/dart/ast/utilities.dart'; |
| import 'package:analyzer/src/dart/resolver/exit_detector.dart'; |
| import 'package:analyzer/src/generated/java_core.dart'; |
| import 'package:analyzer/src/generated/source.dart'; |
| import 'package:analyzer_plugin/utilities/range_factory.dart'; |
| |
| const String _TOKEN_SEPARATOR = '\uFFFF'; |
| |
| Element? _getLocalElement(SimpleIdentifier node) { |
| var element = node.writeOrReadElement; |
| if (element is LocalVariableElement || |
| element is ParameterElement || |
| element is FunctionElement && |
| element.enclosingElement is! CompilationUnitElement) { |
| return element; |
| } |
| return null; |
| } |
| |
| /// Returns the "normalized" version of the given source, which is reconstructed |
| /// from tokens, so ignores all the comments and spaces. |
| String _getNormalizedSource(String src, FeatureSet featureSet) { |
| var selectionTokens = TokenUtils.getTokens(src, featureSet); |
| return selectionTokens.join(_TOKEN_SEPARATOR); |
| } |
| |
| /// Returns the [Map] which maps [map] values to their keys. |
| Map<String, String> _inverseMap(Map<String, String> map) { |
| var result = <String, String>{}; |
| map.forEach((String key, String value) { |
| result[value] = key; |
| }); |
| return result; |
| } |
| |
| /// [ExtractMethodRefactoring] implementation. |
| class ExtractMethodRefactoringImpl extends RefactoringImpl |
| implements ExtractMethodRefactoring { |
| static const ERROR_EXITS = |
| 'Selected statements contain a return statement, but not all possible ' |
| 'execution flows exit. Semantics may not be preserved.'; |
| |
| final SearchEngine searchEngine; |
| final ResolvedUnitResult resolveResult; |
| final int selectionOffset; |
| final int selectionLength; |
| late SourceRange selectionRange; |
| late CorrectionUtils utils; |
| final Set<Source> librariesToImport = <Source>{}; |
| |
| @override |
| String returnType = ''; |
| String? variableType; |
| late String name; |
| bool extractAll = true; |
| @override |
| bool canCreateGetter = false; |
| bool createGetter = false; |
| @override |
| final List<String> names = <String>[]; |
| @override |
| final List<int> offsets = <int>[]; |
| @override |
| final List<int> lengths = <int>[]; |
| |
| /// The map of local elements to their visibility ranges. |
| late Map<LocalElement, SourceRange> _visibleRangeMap; |
| |
| /// The map of local names to their visibility ranges. |
| final Map<String, List<SourceRange>> _localNames = |
| <String, List<SourceRange>>{}; |
| |
| /// The set of names that are referenced without any qualifier. |
| final Set<String> _unqualifiedNames = <String>{}; |
| |
| final Set<String> _excludedNames = <String>{}; |
| List<RefactoringMethodParameter> _parameters = <RefactoringMethodParameter>[]; |
| final Map<String, RefactoringMethodParameter> _parametersMap = |
| <String, RefactoringMethodParameter>{}; |
| final Map<String, List<SourceRange>> _parameterReferencesMap = |
| <String, List<SourceRange>>{}; |
| bool _hasAwait = false; |
| DartType? _returnType; |
| String? _returnVariableName; |
| AstNode? _parentMember; |
| Expression? _selectionExpression; |
| FunctionExpression? _selectionFunctionExpression; |
| List<Statement>? _selectionStatements; |
| final List<_Occurrence> _occurrences = []; |
| bool _staticContext = false; |
| |
| ExtractMethodRefactoringImpl(this.searchEngine, this.resolveResult, |
| this.selectionOffset, this.selectionLength) { |
| selectionRange = SourceRange(selectionOffset, selectionLength); |
| utils = CorrectionUtils(resolveResult); |
| } |
| |
| @override |
| List<RefactoringMethodParameter> get parameters => _parameters; |
| |
| @override |
| set parameters(List<RefactoringMethodParameter> parameters) { |
| _parameters = parameters.toList(); |
| } |
| |
| @override |
| String get refactoringName { |
| var node = NodeLocator(selectionOffset).searchWithin(resolveResult.unit); |
| if (node != null && node.thisOrAncestorOfType<ClassDeclaration>() != null) { |
| return 'Extract Method'; |
| } |
| return 'Extract Function'; |
| } |
| |
| String get signature { |
| var sb = StringBuffer(); |
| if (createGetter) { |
| sb.write('get '); |
| sb.write(name); |
| } else { |
| sb.write(name); |
| sb.write('('); |
| // add all parameters |
| var firstParameter = true; |
| for (var parameter in _parameters) { |
| // may be comma |
| if (firstParameter) { |
| firstParameter = false; |
| } else { |
| sb.write(', '); |
| } |
| // type |
| { |
| var typeSource = parameter.type; |
| if ('dynamic' != typeSource && '' != typeSource) { |
| sb.write(typeSource); |
| sb.write(' '); |
| } |
| } |
| // name |
| sb.write(parameter.name); |
| // optional function-typed parameter parameters |
| if (parameter.parameters != null) { |
| sb.write(parameter.parameters); |
| } |
| } |
| sb.write(')'); |
| } |
| // done |
| return sb.toString(); |
| } |
| |
| @override |
| Future<RefactoringStatus> checkFinalConditions() async { |
| var result = RefactoringStatus(); |
| result.addStatus(validateMethodName(name)); |
| result.addStatus(_checkParameterNames()); |
| var status = await _checkPossibleConflicts(); |
| result.addStatus(status); |
| return result; |
| } |
| |
| @override |
| Future<RefactoringStatus> checkInitialConditions() async { |
| var result = RefactoringStatus(); |
| // selection |
| result.addStatus(_checkSelection()); |
| if (result.hasFatalError) { |
| return result; |
| } |
| // prepare parts |
| var status = await _initializeParameters(); |
| result.addStatus(status); |
| _initializeHasAwait(); |
| await _initializeReturnType(); |
| // occurrences |
| _initializeOccurrences(); |
| _prepareOffsetsLengths(); |
| // getter |
| canCreateGetter = _computeCanCreateGetter(); |
| createGetter = |
| canCreateGetter && _isExpressionForGetter(_selectionExpression); |
| // names |
| _prepareExcludedNames(); |
| _prepareNames(); |
| // closure cannot have parameters |
| if (_selectionFunctionExpression != null && _parameters.isNotEmpty) { |
| var message = format( |
| 'Cannot extract closure as method, it references {0} external variable{1}.', |
| _parameters.length, |
| _parameters.length == 1 ? '' : 's'); |
| return RefactoringStatus.fatal(message); |
| } |
| return result; |
| } |
| |
| @override |
| RefactoringStatus checkName() { |
| return validateMethodName(name); |
| } |
| |
| @override |
| Future<SourceChange> createChange() async { |
| var change = SourceChange(refactoringName); |
| // replace occurrences with method invocation |
| for (var occurrence in _occurrences) { |
| var range = occurrence.range; |
| // may be replacement of duplicates disabled |
| if (!extractAll && !occurrence.isSelection) { |
| continue; |
| } |
| // prepare invocation source |
| String invocationSource; |
| if (_selectionFunctionExpression != null) { |
| invocationSource = name; |
| } else { |
| var sb = StringBuffer(); |
| // may be returns value |
| if (_selectionStatements != null && variableType != null) { |
| // single variable assignment / return statement |
| if (_returnVariableName != null) { |
| var occurrenceName = |
| occurrence._parameterOldToOccurrenceName[_returnVariableName]; |
| // may be declare variable |
| if (!_parametersMap.containsKey(_returnVariableName)) { |
| if (variableType!.isEmpty) { |
| sb.write('var '); |
| } else { |
| sb.write(variableType); |
| sb.write(' '); |
| } |
| } |
| // assign the return value |
| sb.write(occurrenceName); |
| sb.write(' = '); |
| } else { |
| sb.write('return '); |
| } |
| } |
| // await |
| if (_hasAwait) { |
| sb.write('await '); |
| } |
| // invocation itself |
| sb.write(name); |
| if (!createGetter) { |
| sb.write('('); |
| var firstParameter = true; |
| for (var parameter in _parameters) { |
| // may be comma |
| if (firstParameter) { |
| firstParameter = false; |
| } else { |
| sb.write(', '); |
| } |
| // argument name |
| { |
| var argumentName = |
| occurrence._parameterOldToOccurrenceName[parameter.id]; |
| sb.write(argumentName); |
| } |
| } |
| sb.write(')'); |
| } |
| invocationSource = sb.toString(); |
| // statements as extracted with their ";", so add new after invocation |
| if (_selectionStatements != null) { |
| invocationSource += ';'; |
| } |
| } |
| // add replace edit |
| var edit = newSourceEdit_range(range, invocationSource); |
| doSourceChange_addElementEdit( |
| change, resolveResult.unit.declaredElement!, edit); |
| } |
| // add method declaration |
| { |
| // prepare environment |
| var prefix = utils.getNodePrefix(_parentMember!); |
| var eol = utils.endOfLine; |
| // prepare annotations |
| var annotations = ''; |
| { |
| // may be "static" |
| if (_staticContext) { |
| annotations = 'static '; |
| } |
| } |
| // prepare declaration source |
| String? declarationSource; |
| { |
| var returnExpressionSource = _getMethodBodySource(); |
| // closure |
| final selectionFunctionExpression = _selectionFunctionExpression; |
| if (selectionFunctionExpression != null) { |
| var returnTypeCode = _getExpectedClosureReturnTypeCode(); |
| declarationSource = '$returnTypeCode$name$returnExpressionSource'; |
| if (selectionFunctionExpression.body is ExpressionFunctionBody) { |
| declarationSource += ';'; |
| } |
| } |
| // optional 'async' body modifier |
| var asyncKeyword = _hasAwait ? ' async' : ''; |
| // expression |
| if (_selectionExpression != null) { |
| var isMultiLine = returnExpressionSource.contains(eol); |
| |
| // We generate the method body using the shorthand syntax if it fits |
| // into a single line and use the regular method syntax otherwise. |
| if (!isMultiLine) { |
| // add return type |
| if (returnType.isNotEmpty) { |
| annotations += '$returnType '; |
| } |
| // just return expression |
| declarationSource = '$annotations$signature$asyncKeyword => '; |
| declarationSource += '$returnExpressionSource;'; |
| } else { |
| // Left indent once; returnExpressionSource was indented for method |
| // shorthands. |
| returnExpressionSource = utils |
| .indentSourceLeftRight('${returnExpressionSource.trim()};') |
| .trim(); |
| |
| // add return type |
| if (returnType.isNotEmpty) { |
| annotations += '$returnType '; |
| } |
| declarationSource = '$annotations$signature$asyncKeyword {$eol'; |
| declarationSource += '$prefix '; |
| if (returnType.isNotEmpty) { |
| declarationSource += 'return '; |
| } |
| declarationSource += '$returnExpressionSource$eol$prefix}'; |
| } |
| } |
| // statements |
| if (_selectionStatements != null) { |
| if (returnType.isNotEmpty) { |
| annotations += returnType + ' '; |
| } |
| declarationSource = '$annotations$signature$asyncKeyword {$eol'; |
| declarationSource += returnExpressionSource; |
| if (_returnVariableName != null) { |
| declarationSource += '$prefix return $_returnVariableName;$eol'; |
| } |
| declarationSource += '$prefix}'; |
| } |
| } |
| // insert declaration |
| if (declarationSource != null) { |
| var offset = _parentMember!.end; |
| var edit = SourceEdit(offset, 0, '$eol$eol$prefix$declarationSource'); |
| doSourceChange_addElementEdit( |
| change, resolveResult.unit.declaredElement!, edit); |
| } |
| } |
| // done |
| await addLibraryImports(resolveResult.session, change, |
| resolveResult.libraryElement, librariesToImport); |
| return change; |
| } |
| |
| @override |
| bool isAvailable() { |
| return !_checkSelection().hasFatalError; |
| } |
| |
| /// Adds a new reference to the parameter with the given name. |
| void _addParameterReference(String name, SourceRange range) { |
| var references = _parameterReferencesMap[name]; |
| if (references == null) { |
| references = []; |
| _parameterReferencesMap[name] = references; |
| } |
| references.add(range); |
| } |
| |
| RefactoringStatus _checkParameterNames() { |
| var result = RefactoringStatus(); |
| for (var parameter in _parameters) { |
| result.addStatus(validateParameterName(parameter.name)); |
| for (var other in _parameters) { |
| if (!identical(parameter, other) && other.name == parameter.name) { |
| result.addError( |
| format("Parameter '{0}' already exists", parameter.name)); |
| return result; |
| } |
| } |
| if (_isParameterNameConflictWithBody(parameter)) { |
| result.addError(format( |
| "'{0}' is already used as a name in the selected code", |
| parameter.name)); |
| return result; |
| } |
| } |
| return result; |
| } |
| |
| /// Checks if created method will shadow or will be shadowed by other |
| /// elements. |
| Future<RefactoringStatus> _checkPossibleConflicts() async { |
| var result = RefactoringStatus(); |
| var parent = _parentMember!.parent; |
| // top-level function |
| if (parent is CompilationUnit) { |
| var libraryElement = parent.declaredElement!.library; |
| return validateCreateFunction(searchEngine, libraryElement, name); |
| } |
| // method of class |
| if (parent is ClassDeclaration) { |
| var classElement = parent.declaredElement!; |
| return validateCreateMethod(searchEngine, |
| AnalysisSessionHelper(resolveResult.session), classElement, name); |
| } |
| // OK |
| return Future<RefactoringStatus>.value(result); |
| } |
| |
| /// Checks if [selectionRange] selects [Expression] which can be extracted, |
| /// and location of this [DartExpression] in AST allows extracting. |
| RefactoringStatus _checkSelection() { |
| if (selectionOffset <= 0) { |
| return RefactoringStatus.fatal( |
| 'The selection offset must be greater than zero.'); |
| } |
| if (selectionOffset + selectionLength >= resolveResult.content.length) { |
| return RefactoringStatus.fatal( |
| 'The selection end offset must be less then the length of the file.'); |
| } |
| |
| // Check for implicitly selected closure. |
| { |
| var function = _findFunctionExpression(); |
| if (function != null) { |
| _selectionFunctionExpression = function; |
| selectionRange = range.node(function); |
| _parentMember = getEnclosingClassOrUnitMember(function); |
| return RefactoringStatus(); |
| } |
| } |
| |
| var analyzer = _ExtractMethodAnalyzer(resolveResult, selectionRange); |
| analyzer.analyze(); |
| // May be a fatal error. |
| { |
| if (analyzer.status.hasFatalError) { |
| return analyzer.status; |
| } |
| } |
| |
| var selectedNodes = analyzer.selectedNodes; |
| |
| // If no selected nodes, extract the smallest covering expression. |
| if (selectedNodes.isEmpty) { |
| for (var node = analyzer.coveringNode; node != null; node = node.parent) { |
| if (node is Statement) { |
| break; |
| } |
| if (node is Expression && _isExtractable(range.node(node))) { |
| selectedNodes.add(node); |
| selectionRange = range.node(node); |
| break; |
| } |
| } |
| } |
| |
| // Check selected nodes. |
| if (selectedNodes.isNotEmpty) { |
| var selectedNode = selectedNodes.first; |
| _parentMember = getEnclosingClassOrUnitMember(selectedNode); |
| // single expression selected |
| if (selectedNodes.length == 1) { |
| if (!utils.selectionIncludesNonWhitespaceOutsideNode( |
| selectionRange, selectedNode)) { |
| if (selectedNode is Expression) { |
| _selectionExpression = selectedNode; |
| // additional check for closure |
| if (_selectionExpression is FunctionExpression) { |
| _selectionFunctionExpression = |
| _selectionExpression as FunctionExpression; |
| _selectionExpression = null; |
| } |
| // OK |
| return RefactoringStatus(); |
| } |
| } |
| } |
| // statements selected |
| { |
| var selectedStatements = <Statement>[]; |
| for (var selectedNode in selectedNodes) { |
| if (selectedNode is Statement) { |
| selectedStatements.add(selectedNode); |
| } |
| } |
| if (selectedStatements.length == selectedNodes.length) { |
| _selectionStatements = selectedStatements; |
| return RefactoringStatus(); |
| } |
| } |
| } |
| // invalid selection |
| return RefactoringStatus.fatal( |
| 'Can only extract a single expression or a set of statements.'); |
| } |
| |
| /// Initializes [canCreateGetter] flag. |
| bool _computeCanCreateGetter() { |
| // is a function expression |
| if (_selectionFunctionExpression != null) { |
| return false; |
| } |
| // has parameters |
| if (parameters.isNotEmpty) { |
| return false; |
| } |
| // is assignment |
| if (_selectionExpression != null) { |
| if (_selectionExpression is AssignmentExpression) { |
| return false; |
| } |
| } |
| // doesn't return a value |
| if (_selectionStatements != null) { |
| return returnType != 'void'; |
| } |
| // OK |
| return true; |
| } |
| |
| /// If the [selectionRange] is associated with a [FunctionExpression], return |
| /// this [FunctionExpression]. |
| FunctionExpression? _findFunctionExpression() { |
| if (selectionRange.length != 0) { |
| return null; |
| } |
| var offset = selectionRange.offset; |
| var node = NodeLocator2(offset, offset).searchWithin(resolveResult.unit); |
| |
| // Check for the parameter list of a FunctionExpression. |
| { |
| var function = node?.thisOrAncestorOfType<FunctionExpression>(); |
| if (function != null) { |
| var parameters = function.parameters; |
| if (parameters != null && range.node(parameters).contains(offset)) { |
| return function; |
| } |
| } |
| } |
| |
| // Check for the name of the named argument with the closure expression. |
| if (node is SimpleIdentifier) { |
| var label = node.parent; |
| if (label is Label) { |
| var namedExpression = label.parent; |
| if (namedExpression is NamedExpression) { |
| var expression = namedExpression.expression; |
| if (expression is FunctionExpression) { |
| return expression; |
| } |
| } |
| } |
| } |
| |
| return null; |
| } |
| |
| /// If the selected closure (i.e. [_selectionFunctionExpression]) is an |
| /// argument for a function typed parameter (as it should be), and the |
| /// function type has the return type specified, return this return type's |
| /// code. Otherwise return the empty string. |
| String _getExpectedClosureReturnTypeCode() { |
| Expression argument = _selectionFunctionExpression!; |
| if (argument.parent is NamedExpression) { |
| argument = argument.parent as NamedExpression; |
| } |
| var parameter = argument.staticParameterElement; |
| if (parameter != null) { |
| var parameterType = parameter.type; |
| if (parameterType is FunctionType) { |
| var typeCode = _getTypeCode(parameterType.returnType); |
| if (typeCode != 'dynamic') { |
| return typeCode + ' '; |
| } |
| } |
| } |
| return ''; |
| } |
| |
| /// Returns the selected [Expression] source, with applying new parameter |
| /// names. |
| String _getMethodBodySource() { |
| var source = utils.getRangeText(selectionRange); |
| // prepare operations to replace variables with parameters |
| var replaceEdits = <SourceEdit>[]; |
| for (var parameter in _parameters) { |
| var ranges = _parameterReferencesMap[parameter.id]; |
| if (ranges != null) { |
| for (var range in ranges) { |
| replaceEdits.add(SourceEdit(range.offset - selectionRange.offset, |
| range.length, parameter.name)); |
| } |
| } |
| } |
| replaceEdits.sort((a, b) => b.offset - a.offset); |
| // apply replacements |
| source = SourceEdit.applySequence(source, replaceEdits); |
| // change indentation |
| final selectionFunctionExpression = _selectionFunctionExpression; |
| if (selectionFunctionExpression != null) { |
| var baseNode = |
| selectionFunctionExpression.thisOrAncestorOfType<Statement>(); |
| if (baseNode != null) { |
| var baseIndent = utils.getNodePrefix(baseNode); |
| var targetIndent = utils.getNodePrefix(_parentMember!); |
| source = utils.replaceSourceIndent(source, baseIndent, targetIndent); |
| source = source.trim(); |
| } |
| } |
| final selectionStatements = _selectionStatements; |
| if (selectionStatements != null) { |
| var selectionIndent = utils.getNodePrefix(selectionStatements[0]); |
| var targetIndent = utils.getNodePrefix(_parentMember!) + ' '; |
| source = utils.replaceSourceIndent(source, selectionIndent, targetIndent); |
| } |
| // done |
| return source; |
| } |
| |
| _SourcePattern _getSourcePattern(SourceRange range) { |
| var originalSource = utils.getText(range.offset, range.length); |
| var pattern = _SourcePattern(); |
| var replaceEdits = <SourceEdit>[]; |
| resolveResult.unit |
| .accept(_GetSourcePatternVisitor(range, pattern, replaceEdits)); |
| replaceEdits = replaceEdits.reversed.toList(); |
| var source = SourceEdit.applySequence(originalSource, replaceEdits); |
| pattern.normalizedSource = |
| _getNormalizedSource(source, resolveResult.unit.featureSet); |
| return pattern; |
| } |
| |
| String _getTypeCode(DartType type) { |
| return utils.getTypeSource(type, librariesToImport)!; |
| } |
| |
| void _initializeHasAwait() { |
| var visitor = _HasAwaitVisitor(); |
| if (_selectionExpression != null) { |
| _selectionExpression!.accept(visitor); |
| } else if (_selectionStatements != null) { |
| _selectionStatements!.forEach((statement) { |
| statement.accept(visitor); |
| }); |
| } |
| _hasAwait = visitor.result; |
| } |
| |
| /// Fills [_occurrences] field. |
| void _initializeOccurrences() { |
| _occurrences.clear(); |
| // prepare selection |
| var selectionPattern = _getSourcePattern(selectionRange); |
| var patternToSelectionName = |
| _inverseMap(selectionPattern.originalToPatternNames); |
| // prepare an enclosing parent - class or unit |
| var enclosingMemberParent = _parentMember!.parent!; |
| // visit nodes which will able to access extracted method |
| enclosingMemberParent.accept(_InitializeOccurrencesVisitor( |
| this, selectionPattern, patternToSelectionName)); |
| } |
| |
| /// Prepares information about used variables, which should be turned into |
| /// parameters. |
| Future<RefactoringStatus> _initializeParameters() async { |
| _parameters.clear(); |
| _parametersMap.clear(); |
| _parameterReferencesMap.clear(); |
| var result = RefactoringStatus(); |
| var assignedUsedVariables = <VariableElement>[]; |
| |
| var unit = resolveResult.unit; |
| _visibleRangeMap = VisibleRangesComputer.forNode(unit); |
| unit.accept( |
| _InitializeParametersVisitor(this, assignedUsedVariables), |
| ); |
| |
| // single expression |
| final selectionExpression = _selectionExpression; |
| if (selectionExpression != null) { |
| _returnType = selectionExpression.typeOrThrow; |
| } |
| // verify that none or all execution flows end with a "return" |
| final selectionStatements = _selectionStatements; |
| if (selectionStatements != null) { |
| var hasReturn = selectionStatements.any(_mayEndWithReturnStatement); |
| if (hasReturn && !ExitDetector.exits(selectionStatements.last)) { |
| result.addError(ERROR_EXITS); |
| } |
| } |
| // maybe ends with "return" statement |
| if (selectionStatements != null) { |
| var typeSystem = resolveResult.typeSystem; |
| var returnTypeComputer = _ReturnTypeComputer(typeSystem); |
| selectionStatements.forEach((statement) { |
| statement.accept(returnTypeComputer); |
| }); |
| _returnType = returnTypeComputer.returnType; |
| } |
| // maybe single variable to return |
| if (assignedUsedVariables.length == 1) { |
| // we cannot both return variable and have explicit return statement |
| if (_returnType != null) { |
| result.addFatalError( |
| 'Ambiguous return value: Selected block contains assignment(s) to ' |
| 'local variables and return statement.'); |
| return result; |
| } |
| // prepare to return an assigned variable |
| var returnVariable = assignedUsedVariables[0]; |
| _returnType = returnVariable.type; |
| _returnVariableName = returnVariable.displayName; |
| } |
| // fatal, if multiple variables assigned and used after selection |
| if (assignedUsedVariables.length > 1) { |
| var sb = StringBuffer(); |
| for (var variable in assignedUsedVariables) { |
| sb.write(variable.displayName); |
| sb.write('\n'); |
| } |
| result.addFatalError(format( |
| 'Ambiguous return value: Selected block contains more than one ' |
| 'assignment to local variables. Affected variables are:\n\n{0}', |
| sb.toString().trim())); |
| } |
| // done |
| return result; |
| } |
| |
| Future<void> _initializeReturnType() async { |
| var typeProvider = resolveResult.typeProvider; |
| final returnTypeObj = _returnType; |
| if (_selectionFunctionExpression != null) { |
| variableType = ''; |
| returnType = ''; |
| } else if (returnTypeObj == null) { |
| variableType = null; |
| if (_hasAwait) { |
| var futureVoid = typeProvider.futureType(typeProvider.voidType); |
| returnType = _getTypeCode(futureVoid); |
| } else { |
| returnType = 'void'; |
| } |
| } else if (returnTypeObj.isDynamic) { |
| variableType = ''; |
| if (_hasAwait) { |
| returnType = _getTypeCode(typeProvider.futureDynamicType); |
| } else { |
| returnType = ''; |
| } |
| } else { |
| variableType = _getTypeCode(returnTypeObj); |
| if (_hasAwait) { |
| if (returnTypeObj.element != typeProvider.futureElement) { |
| returnType = _getTypeCode(typeProvider.futureType(returnTypeObj)); |
| } |
| } else { |
| returnType = variableType!; |
| } |
| } |
| } |
| |
| /// Checks if the given [element] is declared in [selectionRange]. |
| bool _isDeclaredInSelection(Element element) { |
| return selectionRange.contains(element.nameOffset); |
| } |
| |
| /// Checks if it is OK to extract the node with the given [SourceRange]. |
| bool _isExtractable(SourceRange range) { |
| var analyzer = _ExtractMethodAnalyzer(resolveResult, range); |
| analyzer.analyze(); |
| return analyzer.status.isOK; |
| } |
| |
| bool _isParameterNameConflictWithBody(RefactoringMethodParameter parameter) { |
| var id = parameter.id; |
| var name = parameter.name; |
| var parameterRanges = _parameterReferencesMap[id]; |
| var otherRanges = _localNames[name]; |
| for (var parameterRange in parameterRanges!) { |
| if (otherRanges != null) { |
| for (var otherRange in otherRanges) { |
| if (parameterRange.intersects(otherRange)) { |
| return true; |
| } |
| } |
| } |
| } |
| if (_unqualifiedNames.contains(name)) { |
| return true; |
| } |
| return false; |
| } |
| |
| /// Checks if [element] is referenced after [selectionRange]. |
| bool _isUsedAfterSelection(Element element) { |
| var visitor = _IsUsedAfterSelectionVisitor(this, element); |
| _parentMember!.accept(visitor); |
| return visitor.result; |
| } |
| |
| /// Prepare names that are used in the enclosing function, so should not be |
| /// proposed as names of the extracted method. |
| void _prepareExcludedNames() { |
| _excludedNames.clear(); |
| var localElements = getDefinedLocalElements(_parentMember!); |
| _excludedNames.addAll(localElements.map((e) => e.name!)); |
| } |
| |
| void _prepareNames() { |
| names.clear(); |
| final selectionExpression = _selectionExpression; |
| if (selectionExpression != null) { |
| names.addAll(getVariableNameSuggestionsForExpression( |
| selectionExpression.typeOrThrow, selectionExpression, _excludedNames, |
| isMethod: true)); |
| } |
| } |
| |
| void _prepareOffsetsLengths() { |
| offsets.clear(); |
| lengths.clear(); |
| for (var occurrence in _occurrences) { |
| offsets.add(occurrence.range.offset); |
| lengths.add(occurrence.range.length); |
| } |
| } |
| |
| /// Checks if the given [expression] is reasonable to extract as a getter. |
| static bool _isExpressionForGetter(Expression? expression) { |
| if (expression is BinaryExpression) { |
| return _isExpressionForGetter(expression.leftOperand) && |
| _isExpressionForGetter(expression.rightOperand); |
| } |
| if (expression is Literal) { |
| return true; |
| } |
| if (expression is PrefixExpression) { |
| return _isExpressionForGetter(expression.operand); |
| } |
| if (expression is PrefixedIdentifier) { |
| return _isExpressionForGetter(expression.prefix); |
| } |
| if (expression is PropertyAccess) { |
| return _isExpressionForGetter(expression.target); |
| } |
| if (expression is SimpleIdentifier) { |
| return true; |
| } |
| return false; |
| } |
| |
| /// Returns `true` if the given [statement] may end with a [ReturnStatement]. |
| static bool _mayEndWithReturnStatement(Statement statement) { |
| var visitor = _HasReturnStatementVisitor(); |
| statement.accept(visitor); |
| return visitor.hasReturn; |
| } |
| } |
| |
| /// [SelectionAnalyzer] for [ExtractMethodRefactoringImpl]. |
| class _ExtractMethodAnalyzer extends StatementAnalyzer { |
| _ExtractMethodAnalyzer( |
| ResolvedUnitResult resolveResult, SourceRange selection) |
| : super(resolveResult, selection); |
| |
| @override |
| void handleNextSelectedNode(AstNode node) { |
| super.handleNextSelectedNode(node); |
| _checkParent(node); |
| } |
| |
| @override |
| void handleSelectionEndsIn(AstNode node) { |
| super.handleSelectionEndsIn(node); |
| invalidSelection( |
| 'The selection does not cover a set of statements or an expression. ' |
| 'Extend selection to a valid range.'); |
| } |
| |
| @override |
| void visitAssignmentExpression(AssignmentExpression node) { |
| super.visitAssignmentExpression(node); |
| var lhs = node.leftHandSide; |
| if (_isFirstSelectedNode(lhs)) { |
| invalidSelection('Cannot extract the left-hand side of an assignment.', |
| newLocation_fromNode(lhs)); |
| } |
| } |
| |
| @override |
| void visitConstructorInitializer(ConstructorInitializer node) { |
| super.visitConstructorInitializer(node); |
| if (_isFirstSelectedNode(node)) { |
| invalidSelection( |
| 'Cannot extract a constructor initializer. ' |
| 'Select expression part of initializer.', |
| newLocation_fromNode(node)); |
| } |
| } |
| |
| @override |
| void visitForParts(ForParts node) { |
| node.visitChildren(this); |
| } |
| |
| @override |
| void visitForStatement(ForStatement node) { |
| super.visitForStatement(node); |
| var forLoopParts = node.forLoopParts; |
| if (forLoopParts is ForParts) { |
| if (forLoopParts is ForPartsWithDeclarations && |
| identical(forLoopParts.variables, firstSelectedNode)) { |
| invalidSelection( |
| "Cannot extract initialization part of a 'for' statement."); |
| } else if (forLoopParts.updaters.contains(lastSelectedNode)) { |
| invalidSelection("Cannot extract increment part of a 'for' statement."); |
| } |
| } |
| } |
| |
| @override |
| void visitGenericFunctionType(GenericFunctionType node) { |
| super.visitGenericFunctionType(node); |
| if (_isFirstSelectedNode(node)) { |
| invalidSelection('Cannot extract a single type reference.'); |
| } |
| } |
| |
| @override |
| void visitNamedType(NamedType node) { |
| super.visitNamedType(node); |
| if (_isFirstSelectedNode(node)) { |
| invalidSelection('Cannot extract a single type reference.'); |
| } |
| } |
| |
| @override |
| void visitSimpleIdentifier(SimpleIdentifier node) { |
| super.visitSimpleIdentifier(node); |
| if (_isFirstSelectedNode(node)) { |
| // name of declaration |
| if (node.inDeclarationContext()) { |
| invalidSelection('Cannot extract the name part of a declaration.'); |
| } |
| // method name |
| var element = node.writeOrReadElement; |
| if (element is FunctionElement || element is MethodElement) { |
| invalidSelection('Cannot extract a single method name.'); |
| } |
| // name in property access |
| if (node.parent is PrefixedIdentifier && |
| (node.parent as PrefixedIdentifier).identifier == node) { |
| invalidSelection('Can not extract name part of a property access.'); |
| } |
| } |
| } |
| |
| @override |
| void visitVariableDeclaration(VariableDeclaration node) { |
| super.visitVariableDeclaration(node); |
| if (_isFirstSelectedNode(node)) { |
| invalidSelection( |
| 'Cannot extract a variable declaration fragment. ' |
| 'Select whole declaration statement.', |
| newLocation_fromNode(node)); |
| } |
| } |
| |
| void _checkParent(AstNode node) { |
| var firstParent = firstSelectedNode!.parent; |
| for (var parent in node.withParents) { |
| if (identical(parent, firstParent)) { |
| return; |
| } |
| } |
| invalidSelection( |
| 'Not all selected statements are enclosed by the same parent statement.'); |
| } |
| |
| bool _isFirstSelectedNode(AstNode node) => identical(firstSelectedNode, node); |
| } |
| |
| class _GetSourcePatternVisitor extends GeneralizingAstVisitor<void> { |
| final SourceRange partRange; |
| final _SourcePattern pattern; |
| final List<SourceEdit> replaceEdits; |
| |
| _GetSourcePatternVisitor(this.partRange, this.pattern, this.replaceEdits); |
| |
| @override |
| void visitSimpleIdentifier(SimpleIdentifier node) { |
| var nodeRange = range.node(node); |
| if (partRange.covers(nodeRange)) { |
| var element = _getLocalElement(node); |
| if (element != null) { |
| // name of a named expression |
| if (isNamedExpressionName(node)) { |
| return; |
| } |
| // continue |
| var originalName = element.displayName; |
| var patternName = pattern.originalToPatternNames[originalName]; |
| if (patternName == null) { |
| var parameterType = _getElementType(element); |
| pattern.parameterTypes.add(parameterType); |
| patternName = '__refVar${pattern.originalToPatternNames.length}'; |
| pattern.originalToPatternNames[originalName] = patternName; |
| } |
| replaceEdits.add(SourceEdit(nodeRange.offset - partRange.offset, |
| nodeRange.length, patternName)); |
| } |
| } |
| } |
| |
| DartType _getElementType(Element element) { |
| if (element is VariableElement) { |
| return element.type; |
| } |
| if (element is FunctionElement) { |
| return element.type; |
| } |
| throw StateError('Unknown element type: ${element.runtimeType}'); |
| } |
| } |
| |
| class _HasAwaitVisitor extends GeneralizingAstVisitor<void> { |
| bool result = false; |
| |
| @override |
| void visitAwaitExpression(AwaitExpression node) { |
| result = true; |
| } |
| |
| @override |
| void visitForStatement(ForStatement node) { |
| if (node.awaitKeyword != null) { |
| result = true; |
| } |
| super.visitForStatement(node); |
| } |
| } |
| |
| class _HasReturnStatementVisitor extends RecursiveAstVisitor<void> { |
| bool hasReturn = false; |
| |
| @override |
| void visitBlockFunctionBody(BlockFunctionBody node) {} |
| |
| @override |
| void visitReturnStatement(ReturnStatement node) { |
| hasReturn = true; |
| } |
| } |
| |
| class _InitializeOccurrencesVisitor extends GeneralizingAstVisitor<void> { |
| final ExtractMethodRefactoringImpl ref; |
| final _SourcePattern selectionPattern; |
| final Map<String, String> patternToSelectionName; |
| |
| bool forceStatic = false; |
| |
| _InitializeOccurrencesVisitor( |
| this.ref, this.selectionPattern, this.patternToSelectionName); |
| |
| @override |
| void visitBlock(Block node) { |
| if (ref._selectionStatements != null) { |
| _visitStatements(node.statements); |
| } |
| super.visitBlock(node); |
| } |
| |
| @override |
| void visitConstructorInitializer(ConstructorInitializer node) { |
| forceStatic = true; |
| try { |
| super.visitConstructorInitializer(node); |
| } finally { |
| forceStatic = false; |
| } |
| } |
| |
| @override |
| void visitExpression(Expression node) { |
| if (ref._selectionFunctionExpression != null || |
| ref._selectionExpression != null && |
| node.runtimeType == ref._selectionExpression.runtimeType) { |
| var nodeRange = range.node(node); |
| _tryToFindOccurrence(nodeRange); |
| } |
| super.visitExpression(node); |
| } |
| |
| @override |
| void visitMethodDeclaration(MethodDeclaration node) { |
| forceStatic = node.isStatic; |
| try { |
| super.visitMethodDeclaration(node); |
| } finally { |
| forceStatic = false; |
| } |
| } |
| |
| @override |
| void visitSwitchMember(SwitchMember node) { |
| if (ref._selectionStatements != null) { |
| _visitStatements(node.statements); |
| } |
| super.visitSwitchMember(node); |
| } |
| |
| /// Checks if given [SourceRange] matched selection source and adds |
| /// [_Occurrence]. |
| bool _tryToFindOccurrence(SourceRange nodeRange) { |
| // check if can be extracted |
| if (!ref._isExtractable(nodeRange)) { |
| return false; |
| } |
| // prepare node source |
| var nodePattern = ref._getSourcePattern(nodeRange); |
| // if matches normalized node source, then add as occurrence |
| if (selectionPattern.isCompatible(nodePattern)) { |
| var occurrence = |
| _Occurrence(nodeRange, ref.selectionRange.intersects(nodeRange)); |
| ref._occurrences.add(occurrence); |
| // prepare mapping of parameter names to the occurrence variables |
| nodePattern.originalToPatternNames |
| .forEach((String originalName, String patternName) { |
| var selectionName = patternToSelectionName[patternName]!; |
| occurrence._parameterOldToOccurrenceName[selectionName] = originalName; |
| }); |
| // update static |
| if (forceStatic) { |
| ref._staticContext = true; |
| } |
| // we have match |
| return true; |
| } |
| // no match |
| return false; |
| } |
| |
| void _visitStatements(List<Statement> statements) { |
| var beginStatementIndex = 0; |
| var selectionCount = ref._selectionStatements!.length; |
| while (beginStatementIndex + selectionCount <= statements.length) { |
| var nodeRange = range.startEnd(statements[beginStatementIndex], |
| statements[beginStatementIndex + selectionCount - 1]); |
| var found = _tryToFindOccurrence(nodeRange); |
| // next statement |
| if (found) { |
| beginStatementIndex += selectionCount; |
| } else { |
| beginStatementIndex++; |
| } |
| } |
| } |
| } |
| |
| class _InitializeParametersVisitor extends GeneralizingAstVisitor { |
| final ExtractMethodRefactoringImpl ref; |
| final List<VariableElement> assignedUsedVariables; |
| |
| _InitializeParametersVisitor(this.ref, this.assignedUsedVariables); |
| |
| @override |
| void visitSimpleIdentifier(SimpleIdentifier node) { |
| var nodeRange = range.node(node); |
| if (!ref.selectionRange.covers(nodeRange)) { |
| return; |
| } |
| var name = node.name; |
| // analyze local element |
| var element = _getLocalElement(node); |
| if (element != null) { |
| // name of the named expression |
| if (isNamedExpressionName(node)) { |
| return; |
| } |
| // if declared outside, add parameter |
| if (!ref._isDeclaredInSelection(element)) { |
| // add parameter |
| var parameter = ref._parametersMap[name]; |
| if (parameter == null) { |
| var parameterType = node.writeOrReadType!; |
| var parametersBuffer = StringBuffer(); |
| var parameterTypeCode = ref.utils.getTypeSource( |
| parameterType, ref.librariesToImport, |
| parametersBuffer: parametersBuffer); |
| if (parameterTypeCode == null) { |
| return; |
| } |
| var parametersCode = |
| parametersBuffer.isNotEmpty ? parametersBuffer.toString() : null; |
| parameter = RefactoringMethodParameter( |
| RefactoringMethodParameterKind.REQUIRED, parameterTypeCode, name, |
| parameters: parametersCode, id: name); |
| ref._parameters.add(parameter); |
| ref._parametersMap[name] = parameter; |
| } |
| // add reference to parameter |
| ref._addParameterReference(name, nodeRange); |
| } |
| // remember, if assigned and used after selection |
| if (isLeftHandOfAssignment(node) && ref._isUsedAfterSelection(element)) { |
| if (element is VariableElement && |
| !assignedUsedVariables.contains(element)) { |
| assignedUsedVariables.add(element); |
| } |
| } |
| } |
| // remember information for conflicts checking |
| if (element is LocalElement) { |
| // declared local elements |
| if (node.inDeclarationContext()) { |
| var range = ref._visibleRangeMap[element]; |
| if (range != null) { |
| var ranges = ref._localNames.putIfAbsent(name, () => <SourceRange>[]); |
| ranges.add(range); |
| } |
| } |
| } else { |
| // unqualified non-local names |
| if (!node.isQualified) { |
| ref._unqualifiedNames.add(name); |
| } |
| } |
| } |
| } |
| |
| class _IsUsedAfterSelectionVisitor extends GeneralizingAstVisitor<void> { |
| final ExtractMethodRefactoringImpl ref; |
| final Element element; |
| bool result = false; |
| |
| _IsUsedAfterSelectionVisitor(this.ref, this.element); |
| |
| @override |
| void visitSimpleIdentifier(SimpleIdentifier node) { |
| var nodeElement = node.writeOrReadElement; |
| if (identical(nodeElement, element)) { |
| var nodeOffset = node.offset; |
| if (nodeOffset > ref.selectionRange.end) { |
| result = true; |
| } |
| } |
| } |
| } |
| |
| /// Description of a single occurrence of the selected expression or set of |
| /// statements. |
| class _Occurrence { |
| final SourceRange range; |
| final bool isSelection; |
| |
| final Map<String, String> _parameterOldToOccurrenceName = <String, String>{}; |
| |
| _Occurrence(this.range, this.isSelection); |
| } |
| |
| class _ReturnTypeComputer extends RecursiveAstVisitor<void> { |
| final TypeSystem typeSystem; |
| |
| DartType? returnType; |
| |
| _ReturnTypeComputer(this.typeSystem); |
| |
| @override |
| void visitBlockFunctionBody(BlockFunctionBody node) {} |
| |
| @override |
| void visitReturnStatement(ReturnStatement node) { |
| // prepare expression |
| var expression = node.expression; |
| if (expression == null) { |
| return; |
| } |
| // prepare type |
| var type = expression.typeOrThrow; |
| if (type.isBottom) { |
| return; |
| } |
| // combine types |
| returnType = _combine(returnType, type); |
| } |
| |
| DartType _combine(DartType? returnType, DartType type) { |
| if (returnType == null) { |
| return type; |
| } else { |
| return typeSystem.leastUpperBound(returnType, type); |
| } |
| } |
| } |
| |
| /// Generalized version of some source, in which references to the specific |
| /// variables are replaced with pattern variables, with back mapping from the |
| /// pattern to the original variable names. |
| class _SourcePattern { |
| final List<DartType> parameterTypes = <DartType>[]; |
| late String normalizedSource; |
| final Map<String, String> originalToPatternNames = {}; |
| |
| bool isCompatible(_SourcePattern other) { |
| if (other.normalizedSource != normalizedSource) { |
| return false; |
| } |
| if (other.parameterTypes.length != parameterTypes.length) { |
| return false; |
| } |
| for (var i = 0; i < parameterTypes.length; i++) { |
| if (other.parameterTypes[i] != parameterTypes[i]) { |
| return false; |
| } |
| } |
| return true; |
| } |
| } |