Chromium Code Reviews| 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("\"") || | |
|
Brian Wilkerson
2014/08/20 22:11:16
Why not use single quotes instead of quoting the d
scheglov
2014/08/20 22:19:21
Done.
| |
| 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 rawSelectionSoruce = utils.getRangeText(selectionRange); | |
| 335 List<Token> selectionTokens = TokenUtils.getTokens(rawSelectionSoruce); | |
| 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('Can not extract name part of a property access.'); | |
|
Brian Wilkerson
2014/08/20 22:11:16
"Can not" --> "Cannot" (here and below)
scheglov
2014/08/20 22:19:21
Done.
| |
| 398 } | |
| 399 if (parent is PropertyAccess && identical(parent.propertyName, node)) { | |
| 400 invalidSelection('Can not 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 |