| OLD | NEW |
| (Empty) | |
| 1 // Copyright (c) 2014, the Dart project authors. Please see the AUTHORS file |
| 2 // for details. All rights reserved. Use of this source code is governed by a |
| 3 // BSD-style license that can be found in the LICENSE file. |
| 4 |
| 5 library services.src.refactoring.extract_local; |
| 6 |
| 7 import 'dart:async'; |
| 8 |
| 9 import 'package:analysis_server/src/protocol2.dart' show SourceEdit; |
| 10 import 'package:analysis_server/src/services/correction/change.dart'; |
| 11 import 'package:analysis_server/src/services/correction/selection_analyzer.dart'
; |
| 12 import 'package:analysis_server/src/services/correction/source_range.dart'; |
| 13 import 'package:analysis_server/src/services/correction/status.dart'; |
| 14 import 'package:analysis_server/src/services/correction/strings.dart'; |
| 15 import 'package:analysis_server/src/services/correction/util.dart'; |
| 16 import 'package:analysis_server/src/services/refactoring/naming_conventions.dart
'; |
| 17 import 'package:analysis_server/src/services/refactoring/refactoring.dart'; |
| 18 import 'package:analysis_server/src/services/refactoring/refactoring_internal.da
rt'; |
| 19 import 'package:analysis_server/src/services/search/element_visitors.dart'; |
| 20 import 'package:analyzer/src/generated/ast.dart'; |
| 21 import 'package:analyzer/src/generated/element.dart'; |
| 22 import 'package:analyzer/src/generated/java_core.dart'; |
| 23 import 'package:analyzer/src/generated/scanner.dart'; |
| 24 import 'package:analyzer/src/generated/source.dart'; |
| 25 |
| 26 |
| 27 const String _TOKEN_SEPARATOR = "\uFFFF"; |
| 28 |
| 29 |
| 30 /** |
| 31 * [ExtractLocalRefactoring] implementation. |
| 32 */ |
| 33 class ExtractLocalRefactoringImpl extends RefactoringImpl implements |
| 34 ExtractLocalRefactoring { |
| 35 final CompilationUnit unit; |
| 36 final int selectionOffset; |
| 37 final int selectionLength; |
| 38 String file; |
| 39 SourceRange selectionRange; |
| 40 CorrectionUtils utils; |
| 41 |
| 42 String name; |
| 43 bool extractAll = true; |
| 44 final List<String> names = <String>[]; |
| 45 final List<int> offsets = <int>[]; |
| 46 final List<int> lengths = <int>[]; |
| 47 |
| 48 Expression rootExpression; |
| 49 Expression singleExpression; |
| 50 bool wholeStatementExpression = false; |
| 51 String stringLiteralPart; |
| 52 final List<SourceRange> occurrences = <SourceRange>[]; |
| 53 final Set<String> excludedVariableNames = new Set<String>(); |
| 54 |
| 55 ExtractLocalRefactoringImpl(this.unit, this.selectionOffset, |
| 56 this.selectionLength) { |
| 57 file = unit.element.source.fullName; |
| 58 selectionRange = new SourceRange(selectionOffset, selectionLength); |
| 59 utils = new CorrectionUtils(unit); |
| 60 } |
| 61 |
| 62 String get declarationKeyword { |
| 63 if (_isPartOfConstantExpression(rootExpression)) { |
| 64 return "const"; |
| 65 } else { |
| 66 return "var"; |
| 67 } |
| 68 } |
| 69 |
| 70 @override |
| 71 String get refactoringName => 'Extract Local Variable'; |
| 72 |
| 73 @override |
| 74 Future<RefactoringStatus> checkFinalConditions() { |
| 75 RefactoringStatus result = new RefactoringStatus(); |
| 76 if (excludedVariableNames.contains(name)) { |
| 77 result.addWarning( |
| 78 format( |
| 79 "A variable with name '{0}' is already defined in the visible scop
e.", |
| 80 name)); |
| 81 } |
| 82 return new Future.value(result); |
| 83 } |
| 84 |
| 85 @override |
| 86 Future<RefactoringStatus> checkInitialConditions() { |
| 87 RefactoringStatus result = new RefactoringStatus(); |
| 88 // selection |
| 89 result.addStatus(_checkSelection()); |
| 90 // occurrences |
| 91 if (!result.hasFatalError) { |
| 92 _prepareOccurrences(); |
| 93 _prepareExcludedNames(); |
| 94 } |
| 95 // suggested names |
| 96 _prepareNames(); |
| 97 // done |
| 98 return new Future.value(result); |
| 99 } |
| 100 |
| 101 @override |
| 102 RefactoringStatus checkName() { |
| 103 return validateVariableName(name); |
| 104 } |
| 105 |
| 106 @override |
| 107 Future<Change> createChange() { |
| 108 Change change = new Change(refactoringName); |
| 109 // prepare occurrences |
| 110 List<SourceRange> occurrences; |
| 111 if (extractAll) { |
| 112 occurrences = this.occurrences; |
| 113 } else { |
| 114 occurrences = [selectionRange]; |
| 115 } |
| 116 // If the whole expression of a statement is selected, like '1 + 2', |
| 117 // then convert it into a variable declaration statement. |
| 118 if (wholeStatementExpression && occurrences.length == 1) { |
| 119 String keyword = declarationKeyword; |
| 120 String declarationSource = '$keyword $name = '; |
| 121 SourceEdit edit = |
| 122 new SourceEdit(singleExpression.offset, 0, declarationSource); |
| 123 change.addEdit(file, edit); |
| 124 return new Future.value(change); |
| 125 } |
| 126 // add variable declaration |
| 127 { |
| 128 String declarationSource; |
| 129 if (stringLiteralPart != null) { |
| 130 declarationSource = "var ${name} = '${stringLiteralPart}';"; |
| 131 } else { |
| 132 String keyword = declarationKeyword; |
| 133 String initializerSource = utils.getRangeText(selectionRange); |
| 134 declarationSource = "${keyword} ${name} = ${initializerSource};"; |
| 135 } |
| 136 // prepare location for declaration |
| 137 Statement targetStatement = _findTargetStatement(occurrences); |
| 138 String prefix = utils.getNodePrefix(targetStatement); |
| 139 // insert variable declaration |
| 140 String eol = utils.endOfLine; |
| 141 SourceEdit edit = new SourceEdit( |
| 142 targetStatement.offset, |
| 143 0, |
| 144 '${declarationSource}${eol}${prefix}'); |
| 145 change.addEdit(file, edit); |
| 146 } |
| 147 // prepare replacement |
| 148 String occurrenceReplacement = name; |
| 149 if (stringLiteralPart != null) { |
| 150 occurrenceReplacement = "\${$name}"; |
| 151 } |
| 152 // replace occurrences with variable reference |
| 153 for (SourceRange range in occurrences) { |
| 154 SourceEdit edit = editFromRange(range, occurrenceReplacement); |
| 155 change.addEdit(file, edit); |
| 156 } |
| 157 // done |
| 158 return new Future.value(change); |
| 159 } |
| 160 |
| 161 @override |
| 162 bool requiresPreview() => false; |
| 163 |
| 164 /** |
| 165 * Checks if [selectionRange] selects [Expression] which can be extracted, and |
| 166 * location of this [DartExpression] in AST allows extracting. |
| 167 */ |
| 168 RefactoringStatus _checkSelection() { |
| 169 _ExtractExpressionAnalyzer _selectionAnalyzer = |
| 170 new _ExtractExpressionAnalyzer(selectionRange); |
| 171 unit.accept(_selectionAnalyzer); |
| 172 AstNode coveringNode = _selectionAnalyzer.coveringNode; |
| 173 // may be fatal error |
| 174 { |
| 175 RefactoringStatus status = _selectionAnalyzer.status; |
| 176 if (status.hasFatalError) { |
| 177 return status; |
| 178 } |
| 179 } |
| 180 // we need enclosing block to add variable declaration statement |
| 181 if (coveringNode == null || |
| 182 coveringNode.getAncestor((node) => node is Block) == null) { |
| 183 return new RefactoringStatus.fatal( |
| 184 'Expression inside of function must be selected ' |
| 185 'to activate this refactoring.'); |
| 186 } |
| 187 // part of string literal |
| 188 if (coveringNode is StringLiteral) { |
| 189 stringLiteralPart = utils.getRangeText(selectionRange); |
| 190 if (stringLiteralPart.startsWith("'") || |
| 191 stringLiteralPart.startsWith('"') || |
| 192 stringLiteralPart.endsWith("'") || |
| 193 stringLiteralPart.endsWith('"')) { |
| 194 return new RefactoringStatus.fatal( |
| 195 'Cannot extract only leading or trailing quote of string literal.'); |
| 196 } |
| 197 return new RefactoringStatus(); |
| 198 } |
| 199 // single node selected |
| 200 if (_selectionAnalyzer.selectedNodes.length == 1 && |
| 201 !utils.selectionIncludesNonWhitespaceOutsideNode( |
| 202 selectionRange, |
| 203 _selectionAnalyzer.firstSelectedNode)) { |
| 204 AstNode selectedNode = _selectionAnalyzer.firstSelectedNode; |
| 205 if (selectedNode is Expression) { |
| 206 rootExpression = selectedNode; |
| 207 singleExpression = rootExpression; |
| 208 wholeStatementExpression = |
| 209 singleExpression.parent is ExpressionStatement; |
| 210 return new RefactoringStatus(); |
| 211 } |
| 212 } |
| 213 // fragment of binary expression selected |
| 214 if (coveringNode is BinaryExpression) { |
| 215 BinaryExpression binaryExpression = coveringNode; |
| 216 if (utils.validateBinaryExpressionRange( |
| 217 binaryExpression, |
| 218 selectionRange)) { |
| 219 rootExpression = binaryExpression; |
| 220 singleExpression = null; |
| 221 return new RefactoringStatus(); |
| 222 } |
| 223 } |
| 224 // invalid selection |
| 225 return new RefactoringStatus.fatal( |
| 226 'Expression must be selected to activate this refactoring.'); |
| 227 } |
| 228 |
| 229 /** |
| 230 * Returns [AstNode]s at the offsets of the given [SourceRange]s. |
| 231 */ |
| 232 List<AstNode> _findNodes(List<SourceRange> ranges) { |
| 233 List<AstNode> nodes = <AstNode>[]; |
| 234 for (SourceRange range in ranges) { |
| 235 AstNode node = new NodeLocator.con1(range.offset).searchWithin(unit); |
| 236 nodes.add(node); |
| 237 } |
| 238 return nodes; |
| 239 } |
| 240 |
| 241 /** |
| 242 * @return the [Statement] such that variable declaration added before it will
be visible in |
| 243 * all given occurrences. |
| 244 */ |
| 245 Statement _findTargetStatement(List<SourceRange> occurrences) { |
| 246 List<AstNode> nodes = _findNodes(occurrences); |
| 247 List<AstNode> firstParents = getParents(nodes[0]); |
| 248 AstNode commonParent = getNearestCommonAncestor(nodes); |
| 249 if (commonParent is Block) { |
| 250 int commonIndex = firstParents.indexOf(commonParent); |
| 251 return firstParents[commonIndex + 1] as Statement; |
| 252 } else { |
| 253 return commonParent.getAncestor((node) => node is Statement); |
| 254 } |
| 255 } |
| 256 |
| 257 /** |
| 258 * @return `true` if it is OK to extract the node with the given [SourceRange]
. |
| 259 */ |
| 260 bool _isExtractable(SourceRange range) { |
| 261 _ExtractExpressionAnalyzer analyzer = new _ExtractExpressionAnalyzer(range); |
| 262 utils.unit.accept(analyzer); |
| 263 return analyzer.status.isOK; |
| 264 } |
| 265 |
| 266 bool _isPartOfConstantExpression(AstNode node) { |
| 267 if (node is TypedLiteral) { |
| 268 return node.constKeyword != null; |
| 269 } |
| 270 if (node is InstanceCreationExpression) { |
| 271 InstanceCreationExpression creation = node; |
| 272 return creation.isConst; |
| 273 } |
| 274 if (node is ArgumentList || |
| 275 node is ConditionalExpression || |
| 276 node is BinaryExpression || |
| 277 node is ParenthesizedExpression || |
| 278 node is PrefixExpression || |
| 279 node is Literal || |
| 280 node is MapLiteralEntry) { |
| 281 return _isPartOfConstantExpression(node.parent); |
| 282 } |
| 283 return false; |
| 284 } |
| 285 |
| 286 void _prepareExcludedNames() { |
| 287 excludedVariableNames.clear(); |
| 288 // TODO(scheglov) clean up? |
| 289 AstNode enclosingNode = |
| 290 new NodeLocator.con1(selectionOffset).searchWithin(unit); |
| 291 Block enclosingBlock = enclosingNode.getAncestor((node) => node is Block); |
| 292 if (enclosingBlock != null) { |
| 293 SourceRange newVariableVisibleRange = |
| 294 rangeStartEnd(selectionRange, enclosingBlock.end); |
| 295 ExecutableElement enclosingExecutable = |
| 296 getEnclosingExecutableElement(enclosingNode); |
| 297 if (enclosingExecutable != null) { |
| 298 visitChildren(enclosingExecutable, (Element element) { |
| 299 if (element is LocalElement) { |
| 300 SourceRange elementRange = element.visibleRange; |
| 301 if (elementRange != null && |
| 302 elementRange.intersects(newVariableVisibleRange)) { |
| 303 excludedVariableNames.add(element.displayName); |
| 304 } |
| 305 } |
| 306 return true; |
| 307 }); |
| 308 } |
| 309 } |
| 310 } |
| 311 |
| 312 void _prepareNames() { |
| 313 names.clear(); |
| 314 // TODO(scheglov) implement |
| 315 // Set<String> excluded = excludedVariableNames; |
| 316 // if (_stringLiteralPart != null) { |
| 317 // return getVariableNameSuggestions(_stringLiteralPart, excluded); |
| 318 // } else if (_singleExpression != null) { |
| 319 // _guessedNames = CorrectionUtils.getVariableNameSuggestions2(_singleExpre
ssion.staticType, _singleExpression, excluded); |
| 320 // } else { |
| 321 // _guessedNames = ArrayUtils.EMPTY_STRING_ARRAY; |
| 322 // } |
| 323 } |
| 324 |
| 325 /** |
| 326 * @return all occurrences of the source which matches given selection, sorted
by offset. First |
| 327 * [SourceRange] is same as the given selection. May be empty, but not |
| 328 * <code>null</code>. |
| 329 */ |
| 330 List<SourceRange> _prepareOccurrences() { |
| 331 // prepare selection |
| 332 String selectionSource; |
| 333 { |
| 334 String rawSelectionSource = utils.getRangeText(selectionRange); |
| 335 List<Token> selectionTokens = TokenUtils.getTokens(rawSelectionSource); |
| 336 selectionSource = selectionTokens.join(_TOKEN_SEPARATOR); |
| 337 } |
| 338 // prepare enclosing function |
| 339 AstNode enclosingFunction; |
| 340 { |
| 341 AstNode selectionNode = |
| 342 new NodeLocator.con1(selectionOffset).searchWithin(unit); |
| 343 enclosingFunction = getEnclosingExecutableNode(selectionNode); |
| 344 } |
| 345 // visit function |
| 346 enclosingFunction.accept( |
| 347 new _OccurrencesVisitor(this, occurrences, selectionSource)); |
| 348 // done |
| 349 return occurrences; |
| 350 } |
| 351 } |
| 352 |
| 353 |
| 354 /** |
| 355 * [SelectionAnalyzer] for [ExtractLocalRefactoringImpl]. |
| 356 */ |
| 357 class _ExtractExpressionAnalyzer extends SelectionAnalyzer { |
| 358 final RefactoringStatus status = new RefactoringStatus(); |
| 359 |
| 360 _ExtractExpressionAnalyzer(SourceRange selection) : super(selection); |
| 361 |
| 362 /** |
| 363 * Records fatal error with given message. |
| 364 */ |
| 365 void invalidSelection(String message) { |
| 366 _invalidSelection(message, null); |
| 367 } |
| 368 |
| 369 @override |
| 370 Object visitAssignmentExpression(AssignmentExpression node) { |
| 371 super.visitAssignmentExpression(node); |
| 372 Expression lhs = node.leftHandSide; |
| 373 if (_isFirstSelectedNode(lhs)) { |
| 374 _invalidSelection( |
| 375 'Cannot extract the left-hand side of an assignment.', |
| 376 new RefactoringStatusContext.forNode(lhs)); |
| 377 } |
| 378 return null; |
| 379 } |
| 380 |
| 381 @override |
| 382 Object visitSimpleIdentifier(SimpleIdentifier node) { |
| 383 super.visitSimpleIdentifier(node); |
| 384 if (_isFirstSelectedNode(node)) { |
| 385 // name of declaration |
| 386 if (node.inDeclarationContext()) { |
| 387 invalidSelection('Cannot extract the name part of a declaration.'); |
| 388 } |
| 389 // method name |
| 390 Element element = node.bestElement; |
| 391 if (element is FunctionElement || element is MethodElement) { |
| 392 invalidSelection('Cannot extract a single method name.'); |
| 393 } |
| 394 // name in property access |
| 395 AstNode parent = node.parent; |
| 396 if (parent is PrefixedIdentifier && identical(parent.identifier, node)) { |
| 397 invalidSelection('Cannot extract name part of a property access.'); |
| 398 } |
| 399 if (parent is PropertyAccess && identical(parent.propertyName, node)) { |
| 400 invalidSelection('Cannot extract name part of a property access.'); |
| 401 } |
| 402 } |
| 403 return null; |
| 404 } |
| 405 |
| 406 /** |
| 407 * Records fatal error with given message and [RefactoringStatusContext]. |
| 408 */ |
| 409 void _invalidSelection(String message, RefactoringStatusContext context) { |
| 410 status.addFatalError(message, context); |
| 411 reset(); |
| 412 } |
| 413 |
| 414 bool _isFirstSelectedNode(AstNode node) => identical(firstSelectedNode, node); |
| 415 } |
| 416 |
| 417 |
| 418 class _HasStatementVisitor extends GeneralizingAstVisitor { |
| 419 final List<bool> result; |
| 420 |
| 421 _HasStatementVisitor(this.result); |
| 422 |
| 423 @override |
| 424 visitStatement(Statement node) { |
| 425 result[0] = true; |
| 426 } |
| 427 } |
| 428 |
| 429 |
| 430 class _OccurrencesVisitor extends GeneralizingAstVisitor<Object> { |
| 431 final ExtractLocalRefactoringImpl ref; |
| 432 |
| 433 List<SourceRange> occurrences; |
| 434 |
| 435 String selectionSource; |
| 436 |
| 437 _OccurrencesVisitor(this.ref, this.occurrences, this.selectionSource); |
| 438 |
| 439 @override |
| 440 Object visitBinaryExpression(BinaryExpression node) { |
| 441 if (!_hasStatements(node)) { |
| 442 _tryToFindOccurrenceFragment(node); |
| 443 return null; |
| 444 } |
| 445 return super.visitBinaryExpression(node); |
| 446 } |
| 447 |
| 448 @override |
| 449 Object visitExpression(Expression node) { |
| 450 if (ref._isExtractable(rangeNode(node))) { |
| 451 _tryToFindOccurrence(node); |
| 452 } |
| 453 return super.visitExpression(node); |
| 454 } |
| 455 |
| 456 @override |
| 457 Object visitSimpleStringLiteral(SimpleStringLiteral node) { |
| 458 if (ref.stringLiteralPart != null) { |
| 459 int occuLength = ref.stringLiteralPart.length; |
| 460 String value = node.value; |
| 461 int valueOffset = node.offset + (node.isMultiline ? 3 : 1); |
| 462 int lastIndex = 0; |
| 463 while (true) { |
| 464 int index = value.indexOf(ref.stringLiteralPart, lastIndex); |
| 465 if (index == -1) { |
| 466 break; |
| 467 } |
| 468 lastIndex = index + occuLength; |
| 469 int occuStart = valueOffset + index; |
| 470 SourceRange occuRange = rangeStartLength(occuStart, occuLength); |
| 471 occurrences.add(occuRange); |
| 472 } |
| 473 return null; |
| 474 } |
| 475 return visitExpression(node); |
| 476 } |
| 477 |
| 478 void _addOccurrence(SourceRange range) { |
| 479 if (range.intersects(ref.selectionRange)) { |
| 480 occurrences.add(ref.selectionRange); |
| 481 } else { |
| 482 occurrences.add(range); |
| 483 } |
| 484 } |
| 485 |
| 486 bool _hasStatements(AstNode root) { |
| 487 List<bool> result = [false]; |
| 488 root.accept(new _HasStatementVisitor(result)); |
| 489 return result[0]; |
| 490 } |
| 491 |
| 492 void _tryToFindOccurrence(Expression node) { |
| 493 String nodeSource = ref.utils.getNodeText(node); |
| 494 List<Token> nodeTokens = TokenUtils.getTokens(nodeSource); |
| 495 nodeSource = nodeTokens.join(_TOKEN_SEPARATOR); |
| 496 if (nodeSource == selectionSource) { |
| 497 SourceRange occuRange = rangeNode(node); |
| 498 _addOccurrence(occuRange); |
| 499 } |
| 500 } |
| 501 |
| 502 void _tryToFindOccurrenceFragment(Expression node) { |
| 503 int nodeOffset = node.offset; |
| 504 String nodeSource = ref.utils.getNodeText(node); |
| 505 List<Token> nodeTokens = TokenUtils.getTokens(nodeSource); |
| 506 nodeSource = nodeTokens.join(_TOKEN_SEPARATOR); |
| 507 // find "selection" in "node" tokens |
| 508 int lastIndex = 0; |
| 509 while (true) { |
| 510 // find next occurrence |
| 511 int index = nodeSource.indexOf(selectionSource, lastIndex); |
| 512 if (index == -1) { |
| 513 break; |
| 514 } |
| 515 lastIndex = index + selectionSource.length; |
| 516 // find start/end tokens |
| 517 int startTokenIndex = |
| 518 countMatches(nodeSource.substring(0, index), _TOKEN_SEPARATOR); |
| 519 int endTokenIndex = |
| 520 countMatches(nodeSource.substring(0, lastIndex), _TOKEN_SEPARATOR); |
| 521 Token startToken = nodeTokens[startTokenIndex]; |
| 522 Token endToken = nodeTokens[endTokenIndex]; |
| 523 // add occurrence range |
| 524 int occuStart = nodeOffset + startToken.offset; |
| 525 int occuEnd = nodeOffset + endToken.end; |
| 526 SourceRange occuRange = rangeStartEnd(occuStart, occuEnd); |
| 527 _addOccurrence(occuRange); |
| 528 } |
| 529 } |
| 530 } |
| OLD | NEW |