blob: 1f66fe647e48e4cbea61e3902b1e881ac57b95ac [file] [log] [blame]
// 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/status.dart';
import 'package:analysis_server/src/services/correction/util.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/visible_ranges_computer.dart';
import 'package:analysis_server/src/services/search/hierarchy.dart';
import 'package:analysis_server/src/services/search/search_engine.dart';
import 'package:analysis_server/src/utilities/strings.dart';
import 'package:analyzer/dart/analysis/results.dart';
import 'package:analyzer/dart/ast/ast.dart';
import 'package:analyzer/dart/ast/precedence.dart';
import 'package:analyzer/dart/ast/visitor.dart';
import 'package:analyzer/dart/element/element.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/generated/source.dart';
import 'package:analyzer_plugin/utilities/range_factory.dart';
/// Returns the [SourceRange] to find conflicting locals in.
SourceRange _getLocalsConflictingRange(AstNode node) {
// maybe Block
var block = node.thisOrAncestorOfType<Block>();
if (block != null) {
return range.startEnd(node, block);
}
// maybe whole executable
var executableNode = getEnclosingExecutableNode(node);
if (executableNode != null) {
return range.node(executableNode);
}
// not a part of a declaration with locals
return SourceRange.EMPTY;
}
/// Returns the source which should replace given invocation with given
/// arguments.
String _getMethodSourceForInvocation(
RefactoringStatus status,
_SourcePart part,
CorrectionUtils utils,
AstNode contextNode,
Expression? targetExpression,
List<Expression> arguments) {
// prepare edits to replace parameters with arguments
var edits = <SourceEdit>[];
part._parameters.forEach(
(ParameterElement parameter, List<_ParameterOccurrence> occurrences) {
// prepare argument
Expression? argument;
for (var arg in arguments) {
if (arg.staticParameterElement == parameter) {
argument = arg;
break;
}
}
if (argument is NamedExpression) {
argument = argument.expression;
}
// prepare argument properties
Precedence argumentPrecedence;
String? argumentSource;
if (argument != null) {
argumentPrecedence = getExpressionPrecedence(argument);
argumentSource = utils.getNodeText(argument);
} else {
// report about a missing required parameter
if (parameter.isRequiredPositional) {
status.addError('No argument for the parameter "${parameter.name}".',
newLocation_fromNode(contextNode));
return;
}
// an optional parameter
argumentPrecedence = Precedence.none;
argumentSource = parameter.defaultValueCode;
argumentSource ??= 'null';
}
// replace all occurrences of this parameter
for (var occurrence in occurrences) {
var range = occurrence.range;
// prepare argument source to apply at this occurrence
String occurrenceArgumentSource;
if (argumentPrecedence < occurrence.parentPrecedence) {
occurrenceArgumentSource = '($argumentSource)';
} else {
occurrenceArgumentSource = argumentSource;
}
// do replace
edits.add(newSourceEdit_range(range, occurrenceArgumentSource));
}
});
// replace static field "qualifier" with invocation target
part._implicitClassNameOffsets.forEach((String className, List<int> offsets) {
for (var offset in offsets) {
// edits.add(newSourceEdit_range(range, className + '.'));
edits.add(SourceEdit(offset, 0, className + '.'));
}
});
// replace "this" references with invocation target
if (targetExpression != null) {
var targetSource = utils.getNodeText(targetExpression);
// explicit "this" references
for (var offset in part._explicitThisOffsets) {
edits.add(SourceEdit(offset, 4, targetSource));
}
// implicit "this" references
targetSource += '.';
for (var offset in part._implicitThisOffsets) {
edits.add(SourceEdit(offset, 0, targetSource));
}
}
// prepare edits to replace conflicting variables
var conflictingNames = _getNamesConflictingAt(contextNode);
part._variables.forEach((VariableElement variable, List<SourceRange> ranges) {
var originalName = variable.displayName;
// prepare unique name
String uniqueName;
{
uniqueName = originalName;
var uniqueIndex = 2;
while (conflictingNames.contains(uniqueName)) {
uniqueName = originalName + uniqueIndex.toString();
uniqueIndex++;
}
}
// update references, if name was change
if (uniqueName != originalName) {
for (var range in ranges) {
edits.add(newSourceEdit_range(range, uniqueName));
}
}
});
// prepare source with applied arguments
edits.sort((SourceEdit a, SourceEdit b) => b.offset - a.offset);
return SourceEdit.applySequence(part._source, edits);
}
/// Returns the names which will shadow or will be shadowed by any declaration
/// at [node].
Set<String> _getNamesConflictingAt(AstNode node) {
var result = <String>{};
// local variables and functions
{
var localsRange = _getLocalsConflictingRange(node);
var enclosingExecutable = getEnclosingExecutableNode(node);
if (enclosingExecutable != null) {
var visibleRangeMap = VisibleRangesComputer.forNode(enclosingExecutable);
visibleRangeMap.forEach((element, elementRange) {
if (elementRange.intersects(localsRange)) {
result.add(element.displayName);
}
});
}
}
// fields
{
var enclosingClassElement = getEnclosingClassElement(node);
if (enclosingClassElement != null) {
var elements = <ClassElement>{};
elements.add(enclosingClassElement);
elements.addAll(getSuperClasses(enclosingClassElement));
for (var classElement in elements) {
var classMembers = getChildren(classElement);
for (var classMemberElement in classMembers) {
result.add(classMemberElement.displayName);
}
}
}
}
// done
return result;
}
/// [InlineMethodRefactoring] implementation.
class InlineMethodRefactoringImpl extends RefactoringImpl
implements InlineMethodRefactoring {
final SearchEngine searchEngine;
final ResolvedUnitResult resolveResult;
final int offset;
final AnalysisSessionHelper sessionHelper;
late CorrectionUtils utils;
late SourceChange change;
@override
bool isDeclaration = false;
bool deleteSource = false;
bool inlineAll = true;
ExecutableElement? _methodElement;
late CompilationUnit _methodUnit;
late CorrectionUtils _methodUtils;
late AstNode _methodNode;
FormalParameterList? _methodParameters;
FunctionBody? _methodBody;
Expression? _methodExpression;
_SourcePart? _methodExpressionPart;
_SourcePart? _methodStatementsPart;
final List<_ReferenceProcessor> _referenceProcessors = [];
final Set<Element> _alreadyMadeAsync = <Element>{};
InlineMethodRefactoringImpl(
this.searchEngine, this.resolveResult, this.offset)
: sessionHelper = AnalysisSessionHelper(resolveResult.session) {
utils = CorrectionUtils(resolveResult);
}
@override
String? get className {
var classElement = _methodElement?.enclosingElement;
if (classElement is ClassElement) {
return classElement.displayName;
}
return null;
}
@override
String? get methodName {
return _methodElement?.displayName;
}
@override
String get refactoringName {
if (_methodElement is MethodElement) {
return 'Inline Method';
} else {
return 'Inline Function';
}
}
@override
Future<RefactoringStatus> checkFinalConditions() {
change = SourceChange(refactoringName);
var result = RefactoringStatus();
// check for compatibility of "deleteSource" and "inlineAll"
if (deleteSource && !inlineAll) {
result.addError('All references must be inlined to remove the source.');
}
// prepare changes
for (var processor in _referenceProcessors) {
processor._process(result);
}
// delete method
if (deleteSource && inlineAll) {
var methodRange = range.node(_methodNode);
var linesRange =
_methodUtils.getLinesRange(methodRange, skipLeadingEmptyLines: true);
doSourceChange_addElementEdit(
change, _methodElement!, newSourceEdit_range(linesRange, ''));
}
// done
return Future.value(result);
}
@override
Future<RefactoringStatus> checkInitialConditions() async {
var result = RefactoringStatus();
// prepare method information
result.addStatus(await _prepareMethod());
if (result.hasFatalError) {
return Future<RefactoringStatus>.value(result);
}
// maybe operator
if (_methodElement!.isOperator) {
result = RefactoringStatus.fatal('Cannot inline operator.');
return Future<RefactoringStatus>.value(result);
}
// maybe [a]sync*
if (_methodElement!.isGenerator) {
result = RefactoringStatus.fatal('Cannot inline a generator.');
return Future<RefactoringStatus>.value(result);
}
// analyze method body
result.addStatus(_prepareMethodParts());
// process references
var references = await searchEngine.searchReferences(_methodElement!);
_referenceProcessors.clear();
for (var reference in references) {
var processor = _ReferenceProcessor(this, reference);
await processor.init();
_referenceProcessors.add(processor);
}
return result;
}
@override
Future<SourceChange> createChange() {
return Future.value(change);
}
@override
bool isAvailable() {
return !_checkOffset().hasFatalError;
}
/// Checks if [offset] is a method that can be inlined.
RefactoringStatus _checkOffset() {
var fatalStatus = RefactoringStatus.fatal(
'Method declaration or reference must be selected to activate this refactoring.');
var identifier = NodeLocator(offset).searchWithin(resolveResult.unit);
if (identifier is! SimpleIdentifier) {
return fatalStatus;
}
var element = identifier.writeOrReadElement;
if (element is! ExecutableElement) {
return fatalStatus;
}
if (element.isSynthetic) {
return fatalStatus;
}
// maybe operator
if (element.isOperator) {
return RefactoringStatus.fatal('Cannot inline operator.');
}
// maybe [a]sync*
if (element.isGenerator) {
return RefactoringStatus.fatal('Cannot inline a generator.');
}
return RefactoringStatus();
}
_SourcePart _createSourcePart(SourceRange range) {
var source = _methodUtils.getRangeText(range);
var prefix = getLinePrefix(source);
var result = _SourcePart(range.offset, source, prefix);
// remember parameters and variables occurrences
_methodUnit.accept(_VariablesVisitor(_methodElement!, range, result));
// done
return result;
}
/// Initializes [_methodElement] and related fields.
Future<RefactoringStatus> _prepareMethod() async {
_methodElement = null;
_methodParameters = null;
_methodBody = null;
deleteSource = false;
inlineAll = false;
// prepare for failure
var fatalStatus = RefactoringStatus.fatal(
'Method declaration or reference must be selected to activate this refactoring.');
// prepare selected SimpleIdentifier
var identifier = NodeLocator(offset).searchWithin(resolveResult.unit);
if (identifier is! SimpleIdentifier) {
return fatalStatus;
}
// prepare selected ExecutableElement
var element = identifier.writeOrReadElement;
if (element is! ExecutableElement) {
return fatalStatus;
}
if (element.isSynthetic) {
return fatalStatus;
}
_methodElement = element;
var declaration = await sessionHelper.getElementDeclaration(element);
var methodNode = declaration!.node;
_methodNode = methodNode;
var resolvedUnit = declaration.resolvedUnit!;
_methodUnit = resolvedUnit.unit;
_methodUtils = CorrectionUtils(resolvedUnit);
if (methodNode is MethodDeclaration) {
_methodParameters = methodNode.parameters;
_methodBody = methodNode.body;
} else if (methodNode is FunctionDeclaration) {
_methodParameters = methodNode.functionExpression.parameters;
_methodBody = methodNode.functionExpression.body;
} else {
return fatalStatus;
}
isDeclaration = resolveResult.uri == element.source.uri &&
identifier.offset == element.nameOffset;
deleteSource = isDeclaration;
inlineAll = deleteSource;
return RefactoringStatus();
}
/// Analyze [_methodBody] to fill [_methodExpressionPart] and
/// [_methodStatementsPart].
RefactoringStatus _prepareMethodParts() {
var result = RefactoringStatus();
if (_methodBody is ExpressionFunctionBody) {
var body = _methodBody as ExpressionFunctionBody;
_methodExpression = body.expression;
var methodExpressionRange = range.node(_methodExpression!);
_methodExpressionPart = _createSourcePart(methodExpressionRange);
} else if (_methodBody is BlockFunctionBody) {
var body = (_methodBody as BlockFunctionBody).block;
List<Statement> statements = body.statements;
if (statements.isNotEmpty) {
var lastStatement = statements[statements.length - 1];
// "return" statement requires special handling
if (lastStatement is ReturnStatement) {
_methodExpression = lastStatement.expression;
if (_methodExpression != null) {
var methodExpressionRange = range.node(_methodExpression!);
_methodExpressionPart = _createSourcePart(methodExpressionRange);
}
// exclude "return" statement from statements
statements = statements.sublist(0, statements.length - 1);
}
// if there are statements, process them
if (statements.isNotEmpty) {
var statementsRange =
_methodUtils.getLinesRangeStatements(statements);
_methodStatementsPart = _createSourcePart(statementsRange);
}
}
// check if more than one return
body.accept(_ReturnsValidatorVisitor(result));
} else {
return RefactoringStatus.fatal('Cannot inline method without body.');
}
return result;
}
}
class _ParameterOccurrence {
final Precedence parentPrecedence;
final SourceRange range;
_ParameterOccurrence(this.parentPrecedence, this.range);
}
/// Processor for single [SearchMatch] reference to [methodElement].
class _ReferenceProcessor {
final InlineMethodRefactoringImpl ref;
final SearchMatch reference;
late Element refElement;
late CorrectionUtils _refUtils;
late SimpleIdentifier _node;
SourceRange? _refLineRange;
late String _refPrefix;
_ReferenceProcessor(this.ref, this.reference);
Future<void> init() async {
refElement = reference.element;
// prepare CorrectionUtils
var result = await ref.sessionHelper.getResolvedUnitByElement(refElement);
_refUtils = CorrectionUtils(result!);
// prepare node and environment
_node =
_refUtils.findNode(reference.sourceRange.offset) as SimpleIdentifier;
var refStatement = _node.thisOrAncestorOfType<Statement>();
if (refStatement != null) {
_refLineRange = _refUtils.getLinesRangeStatements([refStatement]);
_refPrefix = _refUtils.getNodePrefix(refStatement);
} else {
_refLineRange = null;
_refPrefix = _refUtils.getLinePrefix(_node.offset);
}
}
void _addRefEdit(SourceEdit edit) {
doSourceChange_addElementEdit(ref.change, refElement, edit);
}
bool _canInlineBody(AstNode usage) {
// no statements, usually just expression
if (ref._methodStatementsPart == null) {
// empty method, inline as closure
if (ref._methodExpressionPart == null) {
return false;
}
// OK, just expression
return true;
}
// analyze point of invocation
var parent = usage.parent;
var parent2 = parent?.parent;
// OK, if statement in block
if (parent is Statement) {
return parent2 is Block;
}
// maybe assignment, in block
if (parent is AssignmentExpression) {
var assignment = parent;
// inlining setter
if (assignment.leftHandSide == usage) {
return parent2 is Statement && parent2.parent is Block;
}
// inlining initializer
return ref._methodExpressionPart != null;
}
// maybe value for variable initializer, in block
if (ref._methodExpressionPart != null) {
if (parent is VariableDeclaration) {
if (parent2 is VariableDeclarationList) {
var parent3 = parent2.parent;
return parent3 is VariableDeclarationStatement &&
parent3.parent is Block;
}
}
}
// not in block, cannot inline body
return false;
}
void _inlineMethodInvocation(RefactoringStatus status, Expression usage,
bool cascaded, Expression? target, List<Expression> arguments) {
// we don't support cascade
if (cascaded) {
status.addError(
'Cannot inline cascade invocation.', newLocation_fromNode(usage));
}
// can we inline method body into "methodUsage" block?
if (_canInlineBody(usage)) {
// insert non-return statements
if (ref._methodStatementsPart != null) {
// prepare statements source for invocation
var source = _getMethodSourceForInvocation(status,
ref._methodStatementsPart!, _refUtils, usage, target, arguments);
source = _refUtils.replaceSourceIndent(
source, ref._methodStatementsPart!._prefix, _refPrefix);
// do insert
var edit =
newSourceEdit_range(SourceRange(_refLineRange!.offset, 0), source);
_addRefEdit(edit);
}
// replace invocation with return expression
if (ref._methodExpressionPart != null) {
// prepare expression source for invocation
var source = _getMethodSourceForInvocation(status,
ref._methodExpressionPart!, _refUtils, usage, target, arguments);
if (getExpressionPrecedence(ref._methodExpression!) <
getExpressionParentPrecedence(usage)) {
source = '($source)';
}
// do replace
var methodUsageRange = range.node(usage);
var edit = newSourceEdit_range(methodUsageRange, source);
_addRefEdit(edit);
} else {
var edit = newSourceEdit_range(_refLineRange!, '');
_addRefEdit(edit);
}
return;
}
// inline as closure invocation
String source;
{
source = ref._methodUtils.getRangeText(range.startEnd(
ref._methodParameters!.leftParenthesis, ref._methodNode));
var methodPrefix = ref._methodUtils.getLinePrefix(ref._methodNode.offset);
source = _refUtils.replaceSourceIndent(source, methodPrefix, _refPrefix);
source = source.trim();
}
// do insert
var edit = newSourceEdit_range(range.node(_node), source);
_addRefEdit(edit);
}
void _process(RefactoringStatus status) {
var nodeParent = _node.parent;
// may be only single place should be inlined
if (!_shouldProcess()) {
return;
}
// If the element being inlined is async, ensure that the function
// body that encloses the method is also async.
if (ref._methodElement!.isAsynchronous) {
var body = _node.thisOrAncestorOfType<FunctionBody>();
if (body != null) {
if (body.isSynchronous) {
if (body.isGenerator) {
status.addFatalError(
'Cannot inline async into sync*.', newLocation_fromNode(_node));
return;
}
if (refElement is ExecutableElement) {
var executable = refElement as ExecutableElement;
if (!executable.returnType.isDartAsyncFuture) {
status.addFatalError(
'Cannot inline async into a function that does not return a Future.',
newLocation_fromNode(_node));
return;
}
}
if (ref._alreadyMadeAsync.add(refElement)) {
var bodyStart = range.startLength(body, 0);
_addRefEdit(newSourceEdit_range(bodyStart, 'async '));
}
}
}
}
// may be invocation of inline method
if (nodeParent is MethodInvocation) {
var invocation = nodeParent;
var target = invocation.target;
List<Expression> arguments = invocation.argumentList.arguments;
_inlineMethodInvocation(
status, invocation, invocation.isCascaded, target, arguments);
} else {
// cannot inline reference to method: var v = new A().method;
if (ref._methodElement is MethodElement) {
status.addFatalError('Cannot inline class method reference.',
newLocation_fromNode(_node));
return;
}
// PropertyAccessorElement
if (ref._methodElement is PropertyAccessorElement) {
Expression usage = _node;
Expression? target;
var cascade = false;
if (nodeParent is PrefixedIdentifier) {
var propertyAccess = nodeParent;
usage = propertyAccess;
target = propertyAccess.prefix;
cascade = false;
}
if (nodeParent is PropertyAccess) {
var propertyAccess = nodeParent;
usage = propertyAccess;
target = propertyAccess.realTarget;
cascade = propertyAccess.isCascaded;
}
// prepare arguments
var arguments = <Expression>[];
if (_node.inSetterContext()) {
var assignment = _node.thisOrAncestorOfType<AssignmentExpression>()!;
arguments.add(assignment.rightHandSide);
}
// inline body
_inlineMethodInvocation(status, usage, cascade, target, arguments);
return;
}
// not invocation, just reference to function
String source;
{
source = ref._methodUtils.getRangeText(range.startEnd(
ref._methodParameters!.leftParenthesis, ref._methodNode));
var methodPrefix =
ref._methodUtils.getLinePrefix(ref._methodNode.offset);
source =
_refUtils.replaceSourceIndent(source, methodPrefix, _refPrefix);
source = source.trim();
source = removeEnd(source, ';')!;
}
// do insert
var edit = newSourceEdit_range(range.node(_node), source);
_addRefEdit(edit);
}
}
bool _shouldProcess() {
if (!ref.inlineAll) {
var parentRange = range.node(_node);
return parentRange.contains(ref.offset);
}
return true;
}
}
class _ReturnsValidatorVisitor extends RecursiveAstVisitor<void> {
final RefactoringStatus result;
int _numReturns = 0;
_ReturnsValidatorVisitor(this.result);
@override
void visitReturnStatement(ReturnStatement node) {
_numReturns++;
if (_numReturns == 2) {
result.addError('Ambiguous return value.', newLocation_fromNode(node));
}
}
}
/// Information about the source of a method being inlined.
class _SourcePart {
/// The base for all [SourceRange]s.
final int _base;
/// The source of the method.
final String _source;
/// The original prefix of the method.
final String _prefix;
/// The occurrences of the method parameters.
final Map<ParameterElement, List<_ParameterOccurrence>> _parameters = {};
/// The occurrences of the method local variables.
final Map<VariableElement, List<SourceRange>> _variables = {};
/// The offsets of explicit `this` expression references.
final List<int> _explicitThisOffsets = [];
/// The offsets of implicit `this` expression references.
final List<int> _implicitThisOffsets = [];
/// The offsets of the implicit class references in static member references.
final Map<String, List<int>> _implicitClassNameOffsets = {};
_SourcePart(this._base, this._source, this._prefix);
void addExplicitThisOffset(int offset) {
_explicitThisOffsets.add(offset - _base);
}
void addImplicitClassNameOffset(String className, int offset) {
var offsets = _implicitClassNameOffsets[className];
if (offsets == null) {
offsets = [];
_implicitClassNameOffsets[className] = offsets;
}
offsets.add(offset - _base);
}
void addImplicitThisOffset(int offset) {
_implicitThisOffsets.add(offset - _base);
}
void addParameterOccurrence(ParameterElement parameter,
SourceRange identifierRange, Precedence precedence) {
var occurrences = _parameters[parameter];
if (occurrences == null) {
occurrences = [];
_parameters[parameter] = occurrences;
}
identifierRange = range.offsetBy(identifierRange, -_base);
occurrences.add(_ParameterOccurrence(precedence, identifierRange));
}
void addVariable(VariableElement element, SourceRange identifierRange) {
var ranges = _variables[element];
if (ranges == null) {
ranges = [];
_variables[element] = ranges;
}
identifierRange = range.offsetBy(identifierRange, -_base);
ranges.add(identifierRange);
}
}
/// A visitor that fills [_SourcePart] with fields, parameters and variables.
class _VariablesVisitor extends GeneralizingAstVisitor<void> {
/// The [ExecutableElement] being inlined.
final ExecutableElement methodElement;
/// The [SourceRange] of the element body.
final SourceRange bodyRange;
/// The [_SourcePart] to record reference into.
final _SourcePart result;
_VariablesVisitor(this.methodElement, this.bodyRange, this.result);
@override
void visitNode(AstNode node) {
var nodeRange = range.node(node);
if (!bodyRange.intersects(nodeRange)) {
return null;
}
super.visitNode(node);
}
@override
void visitSimpleIdentifier(SimpleIdentifier node) {
var nodeRange = range.node(node);
if (bodyRange.covers(nodeRange)) {
_addMemberQualifier(node);
_addParameter(node);
_addVariable(node);
}
}
@override
void visitThisExpression(ThisExpression node) {
var offset = node.offset;
if (bodyRange.contains(offset)) {
result.addExplicitThisOffset(offset);
}
}
void _addMemberQualifier(SimpleIdentifier node) {
// should be unqualified
var qualifier = getNodeQualifier(node);
if (qualifier != null) {
return;
}
// should be a method or field reference
var element = node.writeOrReadElement;
if (element is ExecutableElement) {
if (element is MethodElement || element is PropertyAccessorElement) {
// OK
} else {
return;
}
} else {
return;
}
if (element.enclosingElement is! ClassElement) {
return;
}
// record the implicit static or instance reference
var offset = node.offset;
if (element.isStatic) {
var className = element.enclosingElement.displayName;
result.addImplicitClassNameOffset(className, offset);
} else {
result.addImplicitThisOffset(offset);
}
}
void _addParameter(SimpleIdentifier node) {
var parameterElement = getParameterElement(node);
// not a parameter
if (parameterElement == null) {
return;
}
// not a parameter of the function being inlined
if (!methodElement.parameters.contains(parameterElement)) {
return;
}
// OK, add occurrence
var nodeRange = range.node(node);
var parentPrecedence = getExpressionParentPrecedence(node);
result.addParameterOccurrence(
parameterElement, nodeRange, parentPrecedence);
}
void _addVariable(SimpleIdentifier node) {
var variableElement = getLocalVariableElement(node);
if (variableElement != null) {
var nodeRange = range.node(node);
result.addVariable(variableElement, nodeRange);
}
}
}