| 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 // This code was auto-generated, is not intended to be edited, and is subject to |
| 6 // significant change. Please see the README file for more information. |
| 7 |
| 8 library services.util; |
| 9 |
| 10 import 'dart:collection'; |
| 11 import 'package:analyzer/src/generated/java_core.dart' hide StringUtils; |
| 12 import 'package:analyzer/src/generated/ast.dart'; |
| 13 import 'package:analyzer/src/generated/element.dart'; |
| 14 import 'package:analyzer/src/generated/engine.dart'; |
| 15 import 'package:analyzer/src/generated/error.dart'; |
| 16 import 'package:analyzer/src/generated/resolver.dart'; |
| 17 import 'package:analyzer/src/generated/source.dart'; |
| 18 import 'package:analyzer/src/generated/scanner.dart'; |
| 19 import 'change.dart'; |
| 20 import 'proposal.dart'; |
| 21 import 'status.dart'; |
| 22 import 'stubs.dart'; |
| 23 |
| 24 /** |
| 25 * Context for which assistance should be provided. |
| 26 */ |
| 27 class AssistContext { |
| 28 final SearchEngine searchEngine; |
| 29 |
| 30 final AnalysisContext analysisContext; |
| 31 |
| 32 final String analysisContextId; |
| 33 |
| 34 final Source source; |
| 35 |
| 36 final CompilationUnit compilationUnit; |
| 37 |
| 38 final int selectionOffset; |
| 39 |
| 40 final int selectionLength; |
| 41 |
| 42 AstNode _coveredNode; |
| 43 |
| 44 AstNode _coveringNode; |
| 45 |
| 46 Element _coveredElement; |
| 47 |
| 48 bool _coveredElementFound = false; |
| 49 |
| 50 AssistContext.con1(this.searchEngine, this.analysisContext, this.analysisConte
xtId, this.source, this.compilationUnit, this.selectionOffset, this.selectionLen
gth); |
| 51 |
| 52 AssistContext.con2(SearchEngine searchEngine, AnalysisContext analysisContext,
String analysisContextId, Source source, CompilationUnit compilationUnit, Sourc
eRange selectionRange) : this.con1(searchEngine, analysisContext, analysisContex
tId, source, compilationUnit, selectionRange.offset, selectionRange.length); |
| 53 |
| 54 /** |
| 55 * @return the resolved [CompilationUnitElement] of the [Source]. |
| 56 */ |
| 57 CompilationUnitElement get compilationUnitElement => compilationUnit.element; |
| 58 |
| 59 /** |
| 60 * @return the [Element] of the [coveredNode], may be <code>null</code>. |
| 61 */ |
| 62 Element get coveredElement { |
| 63 if (!_coveredElementFound) { |
| 64 _coveredElementFound = true; |
| 65 AstNode coveredNode = this.coveredNode; |
| 66 if (coveredNode == null) { |
| 67 return null; |
| 68 } |
| 69 _coveredElement = ElementLocator.locateWithOffset(coveredNode, selectionOf
fset); |
| 70 } |
| 71 return _coveredElement; |
| 72 } |
| 73 |
| 74 /** |
| 75 * @return the [AstNode] that is covered by the selection. |
| 76 */ |
| 77 AstNode get coveredNode { |
| 78 if (_coveredNode == null) { |
| 79 NodeLocator locator = new NodeLocator.con2(selectionOffset, selectionOffse
t); |
| 80 _coveredNode = locator.searchWithin(compilationUnit); |
| 81 } |
| 82 return _coveredNode; |
| 83 } |
| 84 |
| 85 /** |
| 86 * @return the ASTNode that covers the selection. |
| 87 */ |
| 88 AstNode get coveringNode { |
| 89 if (_coveringNode == null) { |
| 90 NodeLocator locator = new NodeLocator.con2(selectionOffset, selectionOffse
t + selectionLength); |
| 91 _coveringNode = locator.searchWithin(compilationUnit); |
| 92 } |
| 93 return _coveringNode; |
| 94 } |
| 95 |
| 96 /** |
| 97 * @return the errors associated with the [Source]. |
| 98 */ |
| 99 List<AnalysisError> get errors { |
| 100 Source source = this.source; |
| 101 if (analysisContext == null || source == null) { |
| 102 return AnalysisError.NO_ERRORS; |
| 103 } |
| 104 return analysisContext.getErrors(source).errors; |
| 105 } |
| 106 |
| 107 /** |
| 108 * @return the [SourceRange] of the selection. |
| 109 */ |
| 110 SourceRange get selectionRange => new SourceRange(selectionOffset, selectionLe
ngth); |
| 111 } |
| 112 |
| 113 /** |
| 114 * Utilities for analyzing [CompilationUnit], its parts and source. |
| 115 */ |
| 116 class CorrectionUtils { |
| 117 /** |
| 118 * If `true` then [addEdit] validates that |
| 119 * [Edit] replaces correct part of the [Source]. |
| 120 */ |
| 121 static bool _DEBUG_VALIDATE_EDITS = true; |
| 122 |
| 123 static List<String> _KNOWN_METHOD_NAME_PREFIXES = ["get", "is", "to"]; |
| 124 |
| 125 /** |
| 126 * Validates that the [Edit] replaces the expected part of the [Source] and ad
ds this |
| 127 * [Edit] to the [SourceChange]. |
| 128 */ |
| 129 static void addEdit(AnalysisContext context, SourceChange change, String descr
iption, String expected, Edit edit) { |
| 130 if (_DEBUG_VALIDATE_EDITS) { |
| 131 Source source = change.source; |
| 132 String sourceContent = getSourceContent(context, source); |
| 133 // prepare range |
| 134 int beginIndex = edit.offset; |
| 135 int endIndex = beginIndex + edit.length; |
| 136 int sourceLength = sourceContent.length; |
| 137 if (beginIndex >= sourceLength || endIndex >= sourceLength) { |
| 138 throw new IllegalStateException("${source} has ${sourceLength} character
s but ${beginIndex} to ${endIndex} requested.\n\nTry to use Tools | Reanalyze So
urces."); |
| 139 } |
| 140 // check that range has expected content |
| 141 String rangeContent = sourceContent.substring(beginIndex, endIndex); |
| 142 if (rangeContent != expected) { |
| 143 throw new IllegalStateException("${source} expected |${expected}| at ${b
eginIndex} to ${endIndex} but |${rangeContent}| found.\n\nTry to use Tools | Rea
nalyze Sources."); |
| 144 } |
| 145 } |
| 146 // do add the Edit |
| 147 change.addEdit(edit, description); |
| 148 } |
| 149 |
| 150 /** |
| 151 * @return <code>true</code> if given [List]s are equals at given position. |
| 152 */ |
| 153 static bool allListsEqual(List<List> lists, int position) { |
| 154 Object element = lists[0][position]; |
| 155 for (List list in lists) { |
| 156 if (!identical(list[position], element)) { |
| 157 return false; |
| 158 } |
| 159 } |
| 160 return true; |
| 161 } |
| 162 |
| 163 /** |
| 164 * @return the updated [String] with applied [Edit]s. |
| 165 */ |
| 166 static String applyReplaceEdits(String s, List<Edit> edits) { |
| 167 // sort edits |
| 168 edits = []; |
| 169 edits.sort((Edit o1, Edit o2) => o1.offset - o2.offset); |
| 170 // apply edits |
| 171 int delta = 0; |
| 172 for (Edit edit in edits) { |
| 173 int editOffset = edit.offset + delta; |
| 174 String beforeEdit = s.substring(0, editOffset); |
| 175 String afterEdit = s.substring(editOffset + edit.length); |
| 176 s = "${beforeEdit}${edit.replacement}${afterEdit}"; |
| 177 delta += getDeltaOffset(edit); |
| 178 } |
| 179 // done |
| 180 return s; |
| 181 } |
| 182 |
| 183 /** |
| 184 * @return <code>true</code> if given [SourceRange] covers given [AstNode]. |
| 185 */ |
| 186 static bool covers(SourceRange r, AstNode node) { |
| 187 SourceRange nodeRange = SourceRangeFactory.rangeNode(node); |
| 188 return r.covers(nodeRange); |
| 189 } |
| 190 |
| 191 /** |
| 192 * @return all direct children of the given [Element]. |
| 193 */ |
| 194 static List<Element> getChildren(Element parent) => getChildren2(parent, null)
; |
| 195 |
| 196 /** |
| 197 * @param name the required name of children; may be <code>null</code> to get
children with any |
| 198 * name. |
| 199 * @return all direct children of the given [Element], with given name. |
| 200 */ |
| 201 static List<Element> getChildren2(Element parent, String name) { |
| 202 List<Element> children = []; |
| 203 parent.accept(new GeneralizingElementVisitor_CorrectionUtils_getChildren(par
ent, name, children)); |
| 204 return children; |
| 205 } |
| 206 |
| 207 static String getDefaultValueCode(DartType type) { |
| 208 if (type != null) { |
| 209 String typeName = type.displayName; |
| 210 if (typeName == "bool") { |
| 211 return "false"; |
| 212 } |
| 213 if (typeName == "int") { |
| 214 return "0"; |
| 215 } |
| 216 if (typeName == "double") { |
| 217 return "0.0"; |
| 218 } |
| 219 if (typeName == "String") { |
| 220 return "''"; |
| 221 } |
| 222 } |
| 223 // no better guess |
| 224 return "null"; |
| 225 } |
| 226 |
| 227 /** |
| 228 * @return the number of characters this [Edit] will move offsets after its ra
nge. |
| 229 */ |
| 230 static int getDeltaOffset(Edit edit) => edit.replacement.length - edit.length; |
| 231 |
| 232 /** |
| 233 * @return the name of the [Element] kind. |
| 234 */ |
| 235 static String getElementKindName(Element element) { |
| 236 ElementKind kind = element.kind; |
| 237 return getElementKindName2(kind); |
| 238 } |
| 239 |
| 240 /** |
| 241 * @return the display name of the [ElementKind]. |
| 242 */ |
| 243 static String getElementKindName2(ElementKind kind) => kind.displayName; |
| 244 |
| 245 /** |
| 246 * @return the human name of the [Element]. |
| 247 */ |
| 248 static String getElementQualifiedName(Element element) { |
| 249 ElementKind kind = element.kind; |
| 250 while (true) { |
| 251 if (kind == ElementKind.FIELD || kind == ElementKind.METHOD) { |
| 252 return "${element.enclosingElement.displayName}.${element.displayName}"; |
| 253 } else { |
| 254 return element.displayName; |
| 255 } |
| 256 break; |
| 257 } |
| 258 } |
| 259 |
| 260 /** |
| 261 * @return the [ExecutableElement] of the enclosing executable [AstNode]. |
| 262 */ |
| 263 static ExecutableElement getEnclosingExecutableElement(AstNode node) { |
| 264 while (node != null) { |
| 265 if (node is FunctionDeclaration) { |
| 266 return node.element; |
| 267 } |
| 268 if (node is ConstructorDeclaration) { |
| 269 return node.element; |
| 270 } |
| 271 if (node is MethodDeclaration) { |
| 272 return node.element; |
| 273 } |
| 274 node = node.parent; |
| 275 } |
| 276 return null; |
| 277 } |
| 278 |
| 279 /** |
| 280 * @return the enclosing executable [AstNode]. |
| 281 */ |
| 282 static AstNode getEnclosingExecutableNode(AstNode node) { |
| 283 while (node != null) { |
| 284 if (node is FunctionDeclaration) { |
| 285 return node; |
| 286 } |
| 287 if (node is ConstructorDeclaration) { |
| 288 return node; |
| 289 } |
| 290 if (node is MethodDeclaration) { |
| 291 return node; |
| 292 } |
| 293 node = node.parent; |
| 294 } |
| 295 return null; |
| 296 } |
| 297 |
| 298 /** |
| 299 * @return [Element] exported from the given [LibraryElement]. |
| 300 */ |
| 301 static Element getExportedElement(LibraryElement library, String name) { |
| 302 if (library == null) { |
| 303 return null; |
| 304 } |
| 305 return getExportNamespace2(library)[name]; |
| 306 } |
| 307 |
| 308 /** |
| 309 * TODO(scheglov) may be replace with some API for this |
| 310 * |
| 311 * @return the namespace of the given [ExportElement]. |
| 312 */ |
| 313 static Map<String, Element> getExportNamespace(ExportElement exp) { |
| 314 Namespace namespace = new NamespaceBuilder().createExportNamespaceForDirecti
ve(exp); |
| 315 return namespace.definedNames; |
| 316 } |
| 317 |
| 318 /** |
| 319 * TODO(scheglov) may be replace with some API for this |
| 320 * |
| 321 * @return the export namespace of the given [LibraryElement]. |
| 322 */ |
| 323 static Map<String, Element> getExportNamespace2(LibraryElement library) { |
| 324 Namespace namespace = new NamespaceBuilder().createExportNamespaceForLibrary
(library); |
| 325 return namespace.definedNames; |
| 326 } |
| 327 |
| 328 /** |
| 329 * @return [getExpressionPrecedence] for parent node, or `0` if parent node |
| 330 * is [ParenthesizedExpression]. The reason is that `(expr)` is always |
| 331 * executed after `expr`. |
| 332 */ |
| 333 static int getExpressionParentPrecedence(AstNode node) { |
| 334 AstNode parent = node.parent; |
| 335 if (parent is ParenthesizedExpression) { |
| 336 return 0; |
| 337 } |
| 338 return getExpressionPrecedence(parent); |
| 339 } |
| 340 |
| 341 /** |
| 342 * @return the precedence of the given node - result of [Expression#getPrecede
nce] if an |
| 343 * [Expression], negative otherwise. |
| 344 */ |
| 345 static int getExpressionPrecedence(AstNode node) { |
| 346 if (node is Expression) { |
| 347 return node.precedence; |
| 348 } |
| 349 return -1000; |
| 350 } |
| 351 |
| 352 /** |
| 353 * TODO(scheglov) may be replace with some API for this |
| 354 * |
| 355 * @return the namespace of the given [ImportElement]. |
| 356 */ |
| 357 static Map<String, Element> getImportNamespace(ImportElement imp) { |
| 358 Namespace namespace = new NamespaceBuilder().createImportNamespaceForDirecti
ve(imp); |
| 359 return namespace.definedNames; |
| 360 } |
| 361 |
| 362 /** |
| 363 * @return all [CompilationUnitElement] the given [LibraryElement] consists of
. |
| 364 */ |
| 365 static List<CompilationUnitElement> getLibraryUnits(LibraryElement library) { |
| 366 List<CompilationUnitElement> units = []; |
| 367 units.add(library.definingCompilationUnit); |
| 368 units.addAll(library.parts); |
| 369 return units; |
| 370 } |
| 371 |
| 372 /** |
| 373 * @return the line prefix from the given source, i.e. basically just whitespa
ce prefix of the |
| 374 * given [String]. |
| 375 */ |
| 376 static String getLinesPrefix(String lines) { |
| 377 int index = 0; |
| 378 while (index < lines.length) { |
| 379 int c = lines.codeUnitAt(index); |
| 380 if (!Character.isWhitespace(c)) { |
| 381 break; |
| 382 } |
| 383 index++; |
| 384 } |
| 385 return lines.substring(0, index); |
| 386 } |
| 387 |
| 388 /** |
| 389 * @return the [LocalVariableElement] or [ParameterElement] if given |
| 390 * [SimpleIdentifier] is the reference to local variable or parameter,
or |
| 391 * <code>null</code> in the other case. |
| 392 */ |
| 393 static VariableElement getLocalOrParameterVariableElement(SimpleIdentifier nod
e) { |
| 394 Element element = node.staticElement; |
| 395 if (element is LocalVariableElement) { |
| 396 return element; |
| 397 } |
| 398 if (element is ParameterElement) { |
| 399 return element; |
| 400 } |
| 401 return null; |
| 402 } |
| 403 |
| 404 /** |
| 405 * @return the [LocalVariableElement] if given [SimpleIdentifier] is the refer
ence to |
| 406 * local variable, or <code>null</code> in the other case. |
| 407 */ |
| 408 static LocalVariableElement getLocalVariableElement(SimpleIdentifier node) { |
| 409 Element element = node.staticElement; |
| 410 if (element is LocalVariableElement) { |
| 411 return element; |
| 412 } |
| 413 return null; |
| 414 } |
| 415 |
| 416 /** |
| 417 * @return the nearest common ancestor [AstNode] of the given [AstNode]s. |
| 418 */ |
| 419 static AstNode getNearestCommonAncestor(List<AstNode> nodes) { |
| 420 // may be no nodes |
| 421 if (nodes.isEmpty) { |
| 422 return null; |
| 423 } |
| 424 // prepare parents |
| 425 List<List<AstNode>> parents = []; |
| 426 for (AstNode node in nodes) { |
| 427 parents.add(getParents(node)); |
| 428 } |
| 429 // find min length |
| 430 int minLength = 2147483647; |
| 431 for (List<AstNode> parentList in parents) { |
| 432 minLength = Math.min(minLength, parentList.length); |
| 433 } |
| 434 // find deepest parent |
| 435 int i = 0; |
| 436 for (; i < minLength; i++) { |
| 437 if (!allListsEqual(parents, i)) { |
| 438 break; |
| 439 } |
| 440 } |
| 441 return parents[0][i - 1]; |
| 442 } |
| 443 |
| 444 /** |
| 445 * @return the [Expression] qualified if given node is name part of a [Propert
yAccess] |
| 446 * or [PrefixedIdentifier]. May be <code>null</code>. |
| 447 */ |
| 448 static Expression getNodeQualifier(SimpleIdentifier node) { |
| 449 AstNode parent = node.parent; |
| 450 if (parent is PropertyAccess) { |
| 451 PropertyAccess propertyAccess = parent; |
| 452 if (identical(propertyAccess.propertyName, node)) { |
| 453 return propertyAccess.target; |
| 454 } |
| 455 } |
| 456 if (parent is PrefixedIdentifier) { |
| 457 PrefixedIdentifier prefixed = parent; |
| 458 if (identical(prefixed.identifier, node)) { |
| 459 return prefixed.prefix; |
| 460 } |
| 461 } |
| 462 return null; |
| 463 } |
| 464 |
| 465 /** |
| 466 * @return the [ParameterElement] if given [SimpleIdentifier] is the reference
to |
| 467 * parameter, or <code>null</code> in the other case. |
| 468 */ |
| 469 static ParameterElement getParameterElement(SimpleIdentifier node) { |
| 470 Element element = node.staticElement; |
| 471 if (element is ParameterElement) { |
| 472 return element; |
| 473 } |
| 474 return null; |
| 475 } |
| 476 |
| 477 /** |
| 478 * @return the precedence of the given [Expression] parent. May be `-1` no ope
rator. |
| 479 * @see #getPrecedence(Expression) |
| 480 */ |
| 481 static int getParentPrecedence(Expression expression) { |
| 482 AstNode parent = expression.parent; |
| 483 if (parent is Expression) { |
| 484 return getPrecedence(parent); |
| 485 } |
| 486 return -1; |
| 487 } |
| 488 |
| 489 /** |
| 490 * @return parent [AstNode]s from [CompilationUnit] (at index "0") to the give
n one. |
| 491 */ |
| 492 static List<AstNode> getParents(AstNode node) { |
| 493 // prepare number of parents |
| 494 int numParents = 0; |
| 495 { |
| 496 AstNode current = node.parent; |
| 497 while (current != null) { |
| 498 numParents++; |
| 499 current = current.parent; |
| 500 } |
| 501 } |
| 502 // fill array of parents |
| 503 List<AstNode> parents = new List<AstNode>(numParents); |
| 504 AstNode current = node.parent; |
| 505 int index = numParents; |
| 506 while (current != null) { |
| 507 parents[--index] = current; |
| 508 current = current.parent; |
| 509 } |
| 510 return JavaArrays.asList(parents); |
| 511 } |
| 512 |
| 513 /** |
| 514 * @return the precedence of the given [Expression] operator. May be |
| 515 * `Integer#MAX_VALUE` if not an operator. |
| 516 */ |
| 517 static int getPrecedence(Expression expression) { |
| 518 if (expression is BinaryExpression) { |
| 519 BinaryExpression binaryExpression = expression; |
| 520 return binaryExpression.operator.type.precedence; |
| 521 } |
| 522 if (expression is PrefixExpression) { |
| 523 PrefixExpression prefixExpression = expression; |
| 524 return prefixExpression.operator.type.precedence; |
| 525 } |
| 526 if (expression is PostfixExpression) { |
| 527 PostfixExpression postfixExpression = expression; |
| 528 return postfixExpression.operator.type.precedence; |
| 529 } |
| 530 return 2147483647; |
| 531 } |
| 532 |
| 533 /** |
| 534 * @return the [PropertyAccessorElement] if given [SimpleIdentifier] is the re
ference |
| 535 * to property, or <code>null</code> in the other case. |
| 536 */ |
| 537 static PropertyAccessorElement getPropertyAccessorElement(SimpleIdentifier nod
e) { |
| 538 Element element = node.staticElement; |
| 539 if (element is PropertyAccessorElement) { |
| 540 return element; |
| 541 } |
| 542 return null; |
| 543 } |
| 544 |
| 545 /** |
| 546 * If given [AstNode] is name of qualified property extraction, returns target
from which |
| 547 * this property is extracted. Otherwise `null`. |
| 548 */ |
| 549 static Expression getQualifiedPropertyTarget(AstNode node) { |
| 550 AstNode parent = node.parent; |
| 551 if (parent is PrefixedIdentifier) { |
| 552 PrefixedIdentifier prefixed = parent; |
| 553 if (identical(prefixed.identifier, node)) { |
| 554 return parent.prefix; |
| 555 } |
| 556 } |
| 557 if (parent is PropertyAccess) { |
| 558 PropertyAccess access = parent; |
| 559 if (identical(access.propertyName, node)) { |
| 560 return access.realTarget; |
| 561 } |
| 562 } |
| 563 return null; |
| 564 } |
| 565 |
| 566 /** |
| 567 * Returns the name of the file which corresponds to the name of the class acc
ording to the style |
| 568 * guide. However class does not have to be in this file. |
| 569 */ |
| 570 static String getRecommentedFileNameForClass(String className) { |
| 571 int len = className.length; |
| 572 JavaStringBuilder sb = new JavaStringBuilder(); |
| 573 bool prevWasUpper = false; |
| 574 for (int i = 0; i < len; i++) { |
| 575 int c = className.codeUnitAt(i); |
| 576 if (Character.isUpperCase(c)) { |
| 577 bool nextIsUpper = i < len - 1 && Character.isUpperCase(className.codeUn
itAt(i + 1)); |
| 578 if (i == 0) { |
| 579 } else if (prevWasUpper) { |
| 580 // HTTPServer |
| 581 // ^ |
| 582 if (!nextIsUpper) { |
| 583 sb.appendChar(0x5F); |
| 584 } |
| 585 } else { |
| 586 // HttpServer |
| 587 // ^ |
| 588 sb.appendChar(0x5F); |
| 589 } |
| 590 prevWasUpper = true; |
| 591 c = Character.toLowerCase(c); |
| 592 } else { |
| 593 prevWasUpper = false; |
| 594 } |
| 595 sb.appendChar(c); |
| 596 } |
| 597 sb.append(".dart"); |
| 598 String fileName = sb.toString(); |
| 599 return fileName; |
| 600 } |
| 601 |
| 602 /** |
| 603 * @return given [Statement] if not [Block], first child [Statement] if |
| 604 * [Block], or <code>null</code> if more than one child. |
| 605 */ |
| 606 static Statement getSingleStatement(Statement statement) { |
| 607 if (statement is Block) { |
| 608 List<Statement> blockStatements = statement.statements; |
| 609 if (blockStatements.length != 1) { |
| 610 return null; |
| 611 } |
| 612 return blockStatements[0]; |
| 613 } |
| 614 return statement; |
| 615 } |
| 616 |
| 617 /** |
| 618 * @return the [String] content of the given [Source]. |
| 619 */ |
| 620 static String getSourceContent(AnalysisContext context, Source source) => cont
ext.getContents(source).data.toString(); |
| 621 |
| 622 /** |
| 623 * @return given [Statement] if not [Block], all children [Statement]s if |
| 624 * [Block]. |
| 625 */ |
| 626 static List<Statement> getStatements(Statement statement) { |
| 627 if (statement is Block) { |
| 628 return statement.statements; |
| 629 } |
| 630 return []; |
| 631 } |
| 632 |
| 633 /** |
| 634 * @return all top-level elements declared in the given [LibraryElement]. |
| 635 */ |
| 636 static List<Element> getTopLevelElements(LibraryElement library) { |
| 637 List<Element> elements = []; |
| 638 List<CompilationUnitElement> units = getLibraryUnits(library); |
| 639 for (CompilationUnitElement unit in units) { |
| 640 elements.addAll(unit.functions); |
| 641 elements.addAll(unit.functionTypeAliases); |
| 642 elements.addAll(unit.types); |
| 643 elements.addAll(unit.topLevelVariables); |
| 644 } |
| 645 return elements; |
| 646 } |
| 647 |
| 648 /** |
| 649 * @return the possible names for variable with initializer of the given [Stri
ngLiteral]. |
| 650 */ |
| 651 static List<String> getVariableNameSuggestions(String text, Set<String> exclud
ed) { |
| 652 // filter out everything except of letters and white spaces |
| 653 { |
| 654 JavaStringBuilder sb = new JavaStringBuilder(); |
| 655 for (int i = 0; i < text.length; i++) { |
| 656 int c = text.codeUnitAt(i); |
| 657 if (Character.isLetter(c) || Character.isWhitespace(c)) { |
| 658 sb.appendChar(c); |
| 659 } |
| 660 } |
| 661 text = sb.toString(); |
| 662 } |
| 663 // make single camel-case text |
| 664 { |
| 665 List<String> words = StringUtils.split(text); |
| 666 JavaStringBuilder sb = new JavaStringBuilder(); |
| 667 for (int i = 0; i < words.length; i++) { |
| 668 String word = words[i]; |
| 669 if (i > 0) { |
| 670 word = StringUtils.capitalize(word); |
| 671 } |
| 672 sb.append(word); |
| 673 } |
| 674 text = sb.toString(); |
| 675 } |
| 676 // split camel-case into separate suggested names |
| 677 Set<String> res = new LinkedHashSet(); |
| 678 _addAll(excluded, res, _getVariableNameSuggestions(text)); |
| 679 return new List.from(res); |
| 680 } |
| 681 |
| 682 /** |
| 683 * @return the possible names for variable with given expected type and expres
sion. |
| 684 */ |
| 685 static List<String> getVariableNameSuggestions2(DartType expectedType, Express
ion assignedExpression, Set<String> excluded) { |
| 686 Set<String> res = new LinkedHashSet(); |
| 687 // use expression |
| 688 if (assignedExpression != null) { |
| 689 String nameFromExpression = _getBaseNameFromExpression(assignedExpression)
; |
| 690 if (nameFromExpression != null) { |
| 691 nameFromExpression = StringUtils.removeStart(nameFromExpression, "_"); |
| 692 _addAll(excluded, res, _getVariableNameSuggestions(nameFromExpression)); |
| 693 } |
| 694 String nameFromParent = _getBaseNameFromLocationInParent(assignedExpressio
n); |
| 695 if (nameFromParent != null) { |
| 696 _addAll(excluded, res, _getVariableNameSuggestions(nameFromParent)); |
| 697 } |
| 698 } |
| 699 // use type |
| 700 if (expectedType != null && !expectedType.isDynamic) { |
| 701 String typeName = expectedType.name; |
| 702 if ("int" == typeName) { |
| 703 _addSingleCharacterName(excluded, res, 0x69); |
| 704 } else if ("double" == typeName) { |
| 705 _addSingleCharacterName(excluded, res, 0x64); |
| 706 } else if ("String" == typeName) { |
| 707 _addSingleCharacterName(excluded, res, 0x73); |
| 708 } else { |
| 709 _addAll(excluded, res, _getVariableNameSuggestions(typeName)); |
| 710 } |
| 711 res.remove(typeName); |
| 712 } |
| 713 // done |
| 714 return new List.from(res); |
| 715 } |
| 716 |
| 717 /** |
| 718 * @return `true` if the given [Element#getDisplayName] equals to the given na
me. |
| 719 */ |
| 720 static bool hasDisplayName(Element element, String name) { |
| 721 if (element == null) { |
| 722 return false; |
| 723 } |
| 724 String elementDisplayName = element.displayName; |
| 725 return StringUtils.equals(elementDisplayName, name); |
| 726 } |
| 727 |
| 728 /** |
| 729 * @return `true` if the given [Element#getName] equals to the given name. |
| 730 */ |
| 731 static bool hasName(Element element, String name) { |
| 732 if (element == null) { |
| 733 return false; |
| 734 } |
| 735 String elementName = element.name; |
| 736 return StringUtils.equals(elementName, name); |
| 737 } |
| 738 |
| 739 /** |
| 740 * @return `true` if the given [SimpleIdentifier] is the name of the |
| 741 * [NamedExpression]. |
| 742 */ |
| 743 static bool isNamedExpressionName(SimpleIdentifier node) { |
| 744 AstNode parent = node.parent; |
| 745 if (parent is Label) { |
| 746 Label label = parent; |
| 747 if (identical(label.label, node)) { |
| 748 AstNode parent2 = label.parent; |
| 749 if (parent2 is NamedExpression) { |
| 750 return identical(parent2.name, label); |
| 751 } |
| 752 } |
| 753 } |
| 754 return false; |
| 755 } |
| 756 |
| 757 /** |
| 758 * Adds "toAdd" items which are not excluded. |
| 759 */ |
| 760 static void _addAll(Set<String> excluded, Set<String> result, Iterable<String>
toAdd) { |
| 761 for (String item in toAdd) { |
| 762 // add name based on "item", but not "excluded" |
| 763 for (int suffix = 1;; suffix++) { |
| 764 // prepare name, just "item" or "item2", "item3", etc |
| 765 String name = item; |
| 766 if (suffix > 1) { |
| 767 name += suffix.toString(); |
| 768 } |
| 769 // add once found not excluded |
| 770 if (!excluded.contains(name)) { |
| 771 result.add(name); |
| 772 break; |
| 773 } |
| 774 } |
| 775 } |
| 776 } |
| 777 |
| 778 /** |
| 779 * Add to "result" then given "c" or the first ASCII character after it. |
| 780 */ |
| 781 static void _addSingleCharacterName(Set<String> excluded, Set<String> result,
int c) { |
| 782 while (c < 0x7A) { |
| 783 String name = new String.fromCharCode(c); |
| 784 // may be done |
| 785 if (!excluded.contains(name)) { |
| 786 result.add(name); |
| 787 break; |
| 788 } |
| 789 // next character |
| 790 c = (c + 1); |
| 791 } |
| 792 } |
| 793 |
| 794 static String _getBaseNameFromExpression(Expression expression) { |
| 795 String name = null; |
| 796 // e as Type |
| 797 if (expression is AsExpression) { |
| 798 AsExpression asExpression = expression as AsExpression; |
| 799 expression = asExpression.expression; |
| 800 } |
| 801 // analyze expressions |
| 802 if (expression is SimpleIdentifier) { |
| 803 SimpleIdentifier node = expression; |
| 804 return node.name; |
| 805 } else if (expression is PrefixedIdentifier) { |
| 806 PrefixedIdentifier node = expression; |
| 807 return node.identifier.name; |
| 808 } else if (expression is MethodInvocation) { |
| 809 name = expression.methodName.name; |
| 810 } else if (expression is InstanceCreationExpression) { |
| 811 InstanceCreationExpression creation = expression; |
| 812 ConstructorName constructorName = creation.constructorName; |
| 813 TypeName typeName = constructorName.type; |
| 814 if (typeName != null) { |
| 815 Identifier typeNameIdentifier = typeName.name; |
| 816 // new ClassName() |
| 817 if (typeNameIdentifier is SimpleIdentifier) { |
| 818 return typeNameIdentifier.name; |
| 819 } |
| 820 // new prefix.name(); |
| 821 if (typeNameIdentifier is PrefixedIdentifier) { |
| 822 PrefixedIdentifier prefixed = typeNameIdentifier; |
| 823 // new prefix.ClassName() |
| 824 if (prefixed.prefix.staticElement is PrefixElement) { |
| 825 return prefixed.identifier.name; |
| 826 } |
| 827 // new ClassName.constructorName() |
| 828 return prefixed.prefix.name; |
| 829 } |
| 830 } |
| 831 } |
| 832 // strip known prefixes |
| 833 if (name != null) { |
| 834 for (int i = 0; i < _KNOWN_METHOD_NAME_PREFIXES.length; i++) { |
| 835 String curr = _KNOWN_METHOD_NAME_PREFIXES[i]; |
| 836 if (name.startsWith(curr)) { |
| 837 if (name == curr) { |
| 838 return null; |
| 839 } else if (Character.isUpperCase(name.codeUnitAt(curr.length))) { |
| 840 return name.substring(curr.length); |
| 841 } |
| 842 } |
| 843 } |
| 844 } |
| 845 // done |
| 846 return name; |
| 847 } |
| 848 |
| 849 static String _getBaseNameFromLocationInParent(Expression expression) { |
| 850 // value in named expression |
| 851 if (expression.parent is NamedExpression) { |
| 852 NamedExpression namedExpression = expression.parent as NamedExpression; |
| 853 if (identical(namedExpression.expression, expression)) { |
| 854 return namedExpression.name.label.name; |
| 855 } |
| 856 } |
| 857 // positional argument |
| 858 { |
| 859 ParameterElement parameter = expression.propagatedParameterElement; |
| 860 if (parameter == null) { |
| 861 parameter = expression.staticParameterElement; |
| 862 } |
| 863 if (parameter != null) { |
| 864 return parameter.displayName; |
| 865 } |
| 866 } |
| 867 // unknown |
| 868 return null; |
| 869 } |
| 870 |
| 871 /** |
| 872 * @return [Expression]s from <code>operands</code> which are completely cover
ed by given |
| 873 * [SourceRange]. Range should start and end between given [Expression
]s. |
| 874 */ |
| 875 static List<Expression> _getOperandsForSourceRange(List<Expression> operands,
SourceRange range) { |
| 876 assert(!operands.isEmpty); |
| 877 List<Expression> subOperands = []; |
| 878 // track range enter/exit |
| 879 bool entered = false; |
| 880 bool exited = false; |
| 881 // may be range starts before or on first operand |
| 882 if (range.offset <= operands[0].offset) { |
| 883 entered = true; |
| 884 } |
| 885 // iterate over gaps between operands |
| 886 for (int i = 0; i < operands.length - 1; i++) { |
| 887 Expression operand = operands[i]; |
| 888 Expression nextOperand = operands[i + 1]; |
| 889 SourceRange inclusiveGap = SourceRangeFactory.rangeEndStart(operand, nextO
perand).getMoveEnd(1); |
| 890 // add operand, if already entered range |
| 891 if (entered) { |
| 892 subOperands.add(operand); |
| 893 // may be last operand in range |
| 894 if (range.endsIn(inclusiveGap)) { |
| 895 exited = true; |
| 896 } |
| 897 } else { |
| 898 // may be first operand in range |
| 899 if (range.startsIn(inclusiveGap)) { |
| 900 entered = true; |
| 901 } |
| 902 } |
| 903 } |
| 904 // check if last operand is in range |
| 905 Expression lastGroupMember = operands[operands.length - 1]; |
| 906 if (range.end == lastGroupMember.end) { |
| 907 subOperands.add(lastGroupMember); |
| 908 exited = true; |
| 909 } |
| 910 // we expect that range covers only given operands |
| 911 if (!exited) { |
| 912 return []; |
| 913 } |
| 914 // done |
| 915 return subOperands; |
| 916 } |
| 917 |
| 918 /** |
| 919 * @return all operands of the given [BinaryExpression] and its children with
the same |
| 920 * operator. |
| 921 */ |
| 922 static List<Expression> _getOperandsInOrderFor(BinaryExpression groupRoot) { |
| 923 List<Expression> operands = []; |
| 924 TokenType groupOperatorType = groupRoot.operator.type; |
| 925 groupRoot.accept(new GeneralizingAstVisitor_CorrectionUtils_getOperandsInOrd
erFor(groupOperatorType, operands)); |
| 926 return operands; |
| 927 } |
| 928 |
| 929 /** |
| 930 * @return all variants of names by removing leading words by one. |
| 931 */ |
| 932 static List<String> _getVariableNameSuggestions(String name) { |
| 933 List<String> result = []; |
| 934 List<String> parts = name.split("(?<!(^|[A-Z]))(?=[A-Z])|(?<!^)(?=[A-Z][a-z]
)"); |
| 935 for (int i = 0; i < parts.length; i++) { |
| 936 String suggestion = "${parts[i].toLowerCase()}${StringUtils.join(parts, ""
, i + 1, parts.length)}"; |
| 937 result.add(suggestion); |
| 938 } |
| 939 return result; |
| 940 } |
| 941 |
| 942 /** |
| 943 * Adds enclosing parenthesis if the precedence of the [InvertedCondition] if
less than the |
| 944 * precedence of the expression we are going it to use in. |
| 945 */ |
| 946 static String _parenthesizeIfRequired(CorrectionUtils_InvertedCondition expr,
int newOperatorPrecedence) { |
| 947 if (expr._precedence < newOperatorPrecedence) { |
| 948 return "(${expr._source})"; |
| 949 } |
| 950 return expr._source; |
| 951 } |
| 952 |
| 953 final CompilationUnit unit; |
| 954 |
| 955 LibraryElement _library; |
| 956 |
| 957 String _buffer; |
| 958 |
| 959 String _endOfLine; |
| 960 |
| 961 CorrectionUtils(this.unit) { |
| 962 CompilationUnitElement element = unit.element; |
| 963 this._library = element.library; |
| 964 this._buffer = getSourceContent(element.context, element.source); |
| 965 } |
| 966 |
| 967 /** |
| 968 * @return the source of the given [SourceRange] with indentation changed from
"oldIndent" |
| 969 * to "newIndent", keeping indentation of the lines relative to each o
ther. |
| 970 */ |
| 971 Edit createIndentEdit(SourceRange range, String oldIndent, String newIndent) { |
| 972 String newSource = getIndentSource(range, oldIndent, newIndent); |
| 973 return new Edit(range.offset, range.length, newSource); |
| 974 } |
| 975 |
| 976 /** |
| 977 * @return the [AstNode] that encloses the given offset. |
| 978 */ |
| 979 AstNode findNode(int offset) => new NodeLocator.con1(offset).searchWithin(unit
); |
| 980 |
| 981 /** |
| 982 * TODO(scheglov) replace with nodes once there will be [CompilationUnit#getCo
mments]. |
| 983 * |
| 984 * @return the [SourceRange]s of all comments in [CompilationUnit]. |
| 985 */ |
| 986 List<SourceRange> get commentRanges { |
| 987 List<SourceRange> ranges = []; |
| 988 Token token = unit.beginToken; |
| 989 while (token != null && token.type != TokenType.EOF) { |
| 990 Token commentToken = token.precedingComments; |
| 991 while (commentToken != null) { |
| 992 ranges.add(SourceRangeFactory.rangeToken(commentToken)); |
| 993 commentToken = commentToken.next; |
| 994 } |
| 995 token = token.next; |
| 996 } |
| 997 return ranges; |
| 998 } |
| 999 |
| 1000 /** |
| 1001 * @return the EOL to use for this [CompilationUnit]. |
| 1002 */ |
| 1003 String get endOfLine { |
| 1004 if (_endOfLine == null) { |
| 1005 if (_buffer.contains("\r\n")) { |
| 1006 _endOfLine = "\r\n"; |
| 1007 } else { |
| 1008 _endOfLine = "\n"; |
| 1009 } |
| 1010 } |
| 1011 return _endOfLine; |
| 1012 } |
| 1013 |
| 1014 /** |
| 1015 * @return the default indentation with given level. |
| 1016 */ |
| 1017 String getIndent(int level) => StringUtils.repeat(" ", level); |
| 1018 |
| 1019 /** |
| 1020 * @return the source of the given [SourceRange] with indentation changed from
"oldIndent" |
| 1021 * to "newIndent", keeping indentation of the lines relative to each o
ther. |
| 1022 */ |
| 1023 String getIndentSource(SourceRange range, String oldIndent, String newIndent)
{ |
| 1024 String oldSource = getText3(range); |
| 1025 return getIndentSource3(oldSource, oldIndent, newIndent); |
| 1026 } |
| 1027 |
| 1028 /** |
| 1029 * Indents given source left or right. |
| 1030 * |
| 1031 * @return the source with changed indentation. |
| 1032 */ |
| 1033 String getIndentSource2(String source, bool right) { |
| 1034 JavaStringBuilder sb = new JavaStringBuilder(); |
| 1035 String indent = getIndent(1); |
| 1036 String eol = endOfLine; |
| 1037 List<String> lines = StringUtils.splitByWholeSeparatorPreserveAllTokens(sour
ce, eol); |
| 1038 for (int i = 0; i < lines.length; i++) { |
| 1039 String line = lines[i]; |
| 1040 // last line, stop if empty |
| 1041 if (i == lines.length - 1 && StringUtils.isEmpty(line)) { |
| 1042 break; |
| 1043 } |
| 1044 // update line |
| 1045 if (right) { |
| 1046 line = "${indent}${line}"; |
| 1047 } else { |
| 1048 line = StringUtils.removeStart(line, indent); |
| 1049 } |
| 1050 // append line |
| 1051 sb.append(line); |
| 1052 sb.append(eol); |
| 1053 } |
| 1054 return sb.toString(); |
| 1055 } |
| 1056 |
| 1057 /** |
| 1058 * @return the source with indentation changed from "oldIndent" to "newIndent"
, keeping |
| 1059 * indentation of the lines relative to each other. |
| 1060 */ |
| 1061 String getIndentSource3(String source, String oldIndent, String newIndent) { |
| 1062 // prepare STRING token ranges |
| 1063 List<SourceRange> lineRanges = []; |
| 1064 for (Token token in TokenUtils.getTokens(source)) { |
| 1065 if (token.type == TokenType.STRING) { |
| 1066 lineRanges.add(SourceRangeFactory.rangeToken(token)); |
| 1067 } |
| 1068 } |
| 1069 // re-indent lines |
| 1070 JavaStringBuilder sb = new JavaStringBuilder(); |
| 1071 String eol = endOfLine; |
| 1072 List<String> lines = StringUtils.splitByWholeSeparatorPreserveAllTokens(sour
ce, eol); |
| 1073 int lineOffset = 0; |
| 1074 for (int i = 0; i < lines.length; i++) { |
| 1075 String line = lines[i]; |
| 1076 // last line, stop if empty |
| 1077 if (i == lines.length - 1 && StringUtils.isEmpty(line)) { |
| 1078 break; |
| 1079 } |
| 1080 // check if "offset" is in one of the String ranges |
| 1081 bool inString = false; |
| 1082 for (SourceRange lineRange in lineRanges) { |
| 1083 inString = javaBooleanOr(inString, lineOffset > lineRange.offset && line
Offset < lineRange.end); |
| 1084 if (lineOffset > lineRange.end) { |
| 1085 break; |
| 1086 } |
| 1087 } |
| 1088 lineOffset += line.length + eol.length; |
| 1089 // update line indent |
| 1090 if (!inString) { |
| 1091 line = "${newIndent}${StringUtils.removeStart(line, oldIndent)}"; |
| 1092 } |
| 1093 // append line |
| 1094 sb.append(line); |
| 1095 sb.append(eol); |
| 1096 } |
| 1097 return sb.toString(); |
| 1098 } |
| 1099 |
| 1100 /** |
| 1101 * @return [InsertDesc], description where to insert new library-related direc
tive. |
| 1102 */ |
| 1103 CorrectionUtils_InsertDesc get insertDescImport { |
| 1104 // analyze directives |
| 1105 Directive prevDirective = null; |
| 1106 for (Directive directive in unit.directives) { |
| 1107 if (directive is LibraryDirective || directive is ImportDirective || direc
tive is ExportDirective) { |
| 1108 prevDirective = directive; |
| 1109 } |
| 1110 } |
| 1111 // insert after last library-related directive |
| 1112 if (prevDirective != null) { |
| 1113 CorrectionUtils_InsertDesc result = new CorrectionUtils_InsertDesc(); |
| 1114 result.offset = prevDirective.end; |
| 1115 String eol = endOfLine; |
| 1116 if (prevDirective is LibraryDirective) { |
| 1117 result.prefix = "${eol}${eol}"; |
| 1118 } else { |
| 1119 result.prefix = eol; |
| 1120 } |
| 1121 return result; |
| 1122 } |
| 1123 // no directives, use "top" location |
| 1124 return insertDescTop; |
| 1125 } |
| 1126 |
| 1127 /** |
| 1128 * @return [InsertDesc], description where to insert new 'part 'directive. |
| 1129 */ |
| 1130 CorrectionUtils_InsertDesc get insertDescPart { |
| 1131 // analyze directives |
| 1132 Directive prevDirective = null; |
| 1133 for (Directive directive in unit.directives) { |
| 1134 prevDirective = directive; |
| 1135 } |
| 1136 // insert after last directive |
| 1137 if (prevDirective != null) { |
| 1138 CorrectionUtils_InsertDesc result = new CorrectionUtils_InsertDesc(); |
| 1139 result.offset = prevDirective.end; |
| 1140 String eol = endOfLine; |
| 1141 if (prevDirective is PartDirective) { |
| 1142 result.prefix = eol; |
| 1143 } else { |
| 1144 result.prefix = "${eol}${eol}"; |
| 1145 } |
| 1146 return result; |
| 1147 } |
| 1148 // no directives, use "top" location |
| 1149 return insertDescTop; |
| 1150 } |
| 1151 |
| 1152 /** |
| 1153 * @return [InsertDesc], description where to insert new directive or top-leve
l declaration |
| 1154 * at the top of file. |
| 1155 */ |
| 1156 CorrectionUtils_InsertDesc get insertDescTop { |
| 1157 // skip leading line comments |
| 1158 int offset = 0; |
| 1159 bool insertEmptyLineBefore = false; |
| 1160 bool insertEmptyLineAfter = false; |
| 1161 String source = text; |
| 1162 // skip hash-bang |
| 1163 if (offset < source.length - 2) { |
| 1164 String linePrefix = getText2(offset, 2); |
| 1165 if (linePrefix == "#!") { |
| 1166 insertEmptyLineBefore = true; |
| 1167 offset = getLineNext(offset); |
| 1168 // skip empty lines to first line comment |
| 1169 int emptyOffset = offset; |
| 1170 while (emptyOffset < source.length - 2) { |
| 1171 int nextLineOffset = getLineNext(emptyOffset); |
| 1172 String line = source.substring(emptyOffset, nextLineOffset); |
| 1173 if (line.trim().isEmpty) { |
| 1174 emptyOffset = nextLineOffset; |
| 1175 continue; |
| 1176 } else if (line.startsWith("//")) { |
| 1177 offset = emptyOffset; |
| 1178 break; |
| 1179 } else { |
| 1180 break; |
| 1181 } |
| 1182 } |
| 1183 } |
| 1184 } |
| 1185 // skip line comments |
| 1186 while (offset < source.length - 2) { |
| 1187 String linePrefix = getText2(offset, 2); |
| 1188 if (linePrefix == "//") { |
| 1189 insertEmptyLineBefore = true; |
| 1190 offset = getLineNext(offset); |
| 1191 } else { |
| 1192 break; |
| 1193 } |
| 1194 } |
| 1195 // determine if empty line is required after |
| 1196 int nextLineOffset = getLineNext(offset); |
| 1197 String insertLine = source.substring(offset, nextLineOffset); |
| 1198 if (!insertLine.trim().isEmpty) { |
| 1199 insertEmptyLineAfter = true; |
| 1200 } |
| 1201 // fill InsertDesc |
| 1202 CorrectionUtils_InsertDesc desc = new CorrectionUtils_InsertDesc(); |
| 1203 desc.offset = offset; |
| 1204 if (insertEmptyLineBefore) { |
| 1205 desc.prefix = endOfLine; |
| 1206 } |
| 1207 if (insertEmptyLineAfter) { |
| 1208 desc.suffix = endOfLine; |
| 1209 } |
| 1210 return desc; |
| 1211 } |
| 1212 |
| 1213 /** |
| 1214 * Skips whitespace characters and single EOL on the right from the given posi
tion. If from |
| 1215 * statement or method end, then this is in the most cases start of the next l
ine. |
| 1216 */ |
| 1217 int getLineContentEnd(int index) { |
| 1218 int length = _buffer.length; |
| 1219 // skip whitespace characters |
| 1220 while (index < length) { |
| 1221 int c = _buffer.codeUnitAt(index); |
| 1222 if (!Character.isWhitespace(c) || c == 0xD || c == 0xA) { |
| 1223 break; |
| 1224 } |
| 1225 index++; |
| 1226 } |
| 1227 // skip single \r |
| 1228 if (index < length && _buffer.codeUnitAt(index) == 0xD) { |
| 1229 index++; |
| 1230 } |
| 1231 // skip single \n |
| 1232 if (index < length && _buffer.codeUnitAt(index) == 0xA) { |
| 1233 index++; |
| 1234 } |
| 1235 // done |
| 1236 return index; |
| 1237 } |
| 1238 |
| 1239 /** |
| 1240 * @return the index of the last space or tab on the left from the given one,
if from statement or |
| 1241 * method start, then this is in most cases start of the line. |
| 1242 */ |
| 1243 int getLineContentStart(int index) { |
| 1244 while (index > 0) { |
| 1245 int c = _buffer.codeUnitAt(index - 1); |
| 1246 if (c != 0x20 && c != 0x9) { |
| 1247 break; |
| 1248 } |
| 1249 index--; |
| 1250 } |
| 1251 return index; |
| 1252 } |
| 1253 |
| 1254 /** |
| 1255 * @return the start index of the next line after the line which contains give
n index. |
| 1256 */ |
| 1257 int getLineNext(int index) { |
| 1258 int length = _buffer.length; |
| 1259 // skip whitespace characters |
| 1260 while (index < length) { |
| 1261 int c = _buffer.codeUnitAt(index); |
| 1262 if (c == 0xD || c == 0xA) { |
| 1263 break; |
| 1264 } |
| 1265 index++; |
| 1266 } |
| 1267 // skip single \r |
| 1268 if (index < length && _buffer.codeUnitAt(index) == 0xD) { |
| 1269 index++; |
| 1270 } |
| 1271 // skip single \n |
| 1272 if (index < length && _buffer.codeUnitAt(index) == 0xA) { |
| 1273 index++; |
| 1274 } |
| 1275 // done |
| 1276 return index; |
| 1277 } |
| 1278 |
| 1279 /** |
| 1280 * @return the whitespace prefix of the line which contains given offset. |
| 1281 */ |
| 1282 String getLinePrefix(int index) { |
| 1283 int lineStart = getLineThis(index); |
| 1284 int length = _buffer.length; |
| 1285 int lineNonWhitespace = lineStart; |
| 1286 while (lineNonWhitespace < length) { |
| 1287 int c = _buffer.codeUnitAt(lineNonWhitespace); |
| 1288 if (c == 0xD || c == 0xA) { |
| 1289 break; |
| 1290 } |
| 1291 if (!Character.isWhitespace(c)) { |
| 1292 break; |
| 1293 } |
| 1294 lineNonWhitespace++; |
| 1295 } |
| 1296 return getText2(lineStart, lineNonWhitespace - lineStart); |
| 1297 } |
| 1298 |
| 1299 /** |
| 1300 * @return the [getLinesRange] for given [Statement]s. |
| 1301 */ |
| 1302 SourceRange getLinesRange(List<Statement> statements) { |
| 1303 SourceRange range = SourceRangeFactory.rangeNodes(statements); |
| 1304 return getLinesRange2(range); |
| 1305 } |
| 1306 |
| 1307 /** |
| 1308 * @return the [SourceRange] which starts at the start of the line of "offset"
and ends at |
| 1309 * the start of the next line after "end" of the given [SourceRange],
i.e. basically |
| 1310 * complete lines of the source for given [SourceRange]. |
| 1311 */ |
| 1312 SourceRange getLinesRange2(SourceRange range) { |
| 1313 // start |
| 1314 int startOffset = range.offset; |
| 1315 int startLineOffset = getLineContentStart(startOffset); |
| 1316 // end |
| 1317 int endOffset = range.end; |
| 1318 int afterEndLineOffset = getLineContentEnd(endOffset); |
| 1319 // range |
| 1320 return SourceRangeFactory.rangeStartEnd(startLineOffset, afterEndLineOffset)
; |
| 1321 } |
| 1322 |
| 1323 /** |
| 1324 * @return the [getLinesRange] for given [Statement]s. |
| 1325 */ |
| 1326 SourceRange getLinesRange3(List<Statement> statements) => getLinesRange([]); |
| 1327 |
| 1328 /** |
| 1329 * @return the start index of the line which contains given index. |
| 1330 */ |
| 1331 int getLineThis(int index) { |
| 1332 while (index > 0) { |
| 1333 int c = _buffer.codeUnitAt(index - 1); |
| 1334 if (c == 0xD || c == 0xA) { |
| 1335 break; |
| 1336 } |
| 1337 index--; |
| 1338 } |
| 1339 return index; |
| 1340 } |
| 1341 |
| 1342 /** |
| 1343 * @return the line prefix consisting of spaces and tabs on the left from the
given |
| 1344 * [AstNode]. |
| 1345 */ |
| 1346 String getNodePrefix(AstNode node) { |
| 1347 int offset = node.offset; |
| 1348 // function literal is special, it uses offset of enclosing line |
| 1349 if (node is FunctionExpression) { |
| 1350 return getLinePrefix(offset); |
| 1351 } |
| 1352 // use just prefix directly before node |
| 1353 return getPrefix(offset); |
| 1354 } |
| 1355 |
| 1356 /** |
| 1357 * @return the index of the first non-whitespace character after given index. |
| 1358 */ |
| 1359 int getNonWhitespaceForward(int index) { |
| 1360 int length = _buffer.length; |
| 1361 // skip whitespace characters |
| 1362 while (index < length) { |
| 1363 int c = _buffer.codeUnitAt(index); |
| 1364 if (!Character.isWhitespace(c)) { |
| 1365 break; |
| 1366 } |
| 1367 index++; |
| 1368 } |
| 1369 // done |
| 1370 return index; |
| 1371 } |
| 1372 |
| 1373 /** |
| 1374 * @return the source for the parameter with the given type and name. |
| 1375 */ |
| 1376 String getParameterSource(DartType type, String name) { |
| 1377 // no type |
| 1378 if (type == null || type.isDynamic) { |
| 1379 return name; |
| 1380 } |
| 1381 // function type |
| 1382 if (type is FunctionType) { |
| 1383 FunctionType functionType = type; |
| 1384 JavaStringBuilder sb = new JavaStringBuilder(); |
| 1385 // return type |
| 1386 DartType returnType = functionType.returnType; |
| 1387 if (returnType != null && !returnType.isDynamic) { |
| 1388 sb.append(getTypeSource2(returnType)); |
| 1389 sb.appendChar(0x20); |
| 1390 } |
| 1391 // parameter name |
| 1392 sb.append(name); |
| 1393 // parameters |
| 1394 sb.appendChar(0x28); |
| 1395 List<ParameterElement> fParameters = functionType.parameters; |
| 1396 for (int i = 0; i < fParameters.length; i++) { |
| 1397 ParameterElement fParameter = fParameters[i]; |
| 1398 if (i != 0) { |
| 1399 sb.append(", "); |
| 1400 } |
| 1401 sb.append(getParameterSource(fParameter.type, fParameter.name)); |
| 1402 } |
| 1403 sb.appendChar(0x29); |
| 1404 // done |
| 1405 return sb.toString(); |
| 1406 } |
| 1407 // simple type |
| 1408 return "${getTypeSource2(type)} ${name}"; |
| 1409 } |
| 1410 |
| 1411 /** |
| 1412 * @return the line prefix consisting of spaces and tabs on the left from the
given offset. |
| 1413 */ |
| 1414 String getPrefix(int endIndex) { |
| 1415 int startIndex = getLineContentStart(endIndex); |
| 1416 return _buffer.substring(startIndex, endIndex); |
| 1417 } |
| 1418 |
| 1419 /** |
| 1420 * @return the full text of unit. |
| 1421 */ |
| 1422 String get text => _buffer; |
| 1423 |
| 1424 /** |
| 1425 * @return the given range of text from unit. |
| 1426 */ |
| 1427 String getText(AstNode node) => getText2(node.offset, node.length); |
| 1428 |
| 1429 /** |
| 1430 * @return the given range of text from unit. |
| 1431 */ |
| 1432 String getText2(int offset, int length) => _buffer.substring(offset, offset +
length); |
| 1433 |
| 1434 /** |
| 1435 * @return the given range of text from unit. |
| 1436 */ |
| 1437 String getText3(SourceRange range) => getText2(range.offset, range.length); |
| 1438 |
| 1439 /** |
| 1440 * @return the actual type source of the given [Expression], may be `null` if
can not |
| 1441 * be resolved, should be treated as <code>Dynamic</code>. |
| 1442 */ |
| 1443 String getTypeSource(Expression expression) { |
| 1444 if (expression == null) { |
| 1445 return null; |
| 1446 } |
| 1447 DartType type = expression.staticType; |
| 1448 String typeSource = getTypeSource2(type); |
| 1449 if ("dynamic" == typeSource) { |
| 1450 return null; |
| 1451 } |
| 1452 return typeSource; |
| 1453 } |
| 1454 |
| 1455 /** |
| 1456 * @return the source to reference the given [Type] in this [CompilationUnit]. |
| 1457 */ |
| 1458 String getTypeSource2(DartType type) { |
| 1459 JavaStringBuilder sb = new JavaStringBuilder(); |
| 1460 // prepare element |
| 1461 Element element = type.element; |
| 1462 if (element == null) { |
| 1463 String source = type.toString(); |
| 1464 source = StringUtils.remove(source, "<dynamic>"); |
| 1465 source = StringUtils.remove(source, "<dynamic, dynamic>"); |
| 1466 return source; |
| 1467 } |
| 1468 // append prefix |
| 1469 { |
| 1470 ImportElement imp = _getImportElement(element); |
| 1471 if (imp != null && imp.prefix != null) { |
| 1472 sb.append(imp.prefix.displayName); |
| 1473 sb.append("."); |
| 1474 } |
| 1475 } |
| 1476 // append simple name |
| 1477 String name = element.displayName; |
| 1478 sb.append(name); |
| 1479 // may be type arguments |
| 1480 if (type is InterfaceType) { |
| 1481 InterfaceType interfaceType = type; |
| 1482 List<DartType> arguments = interfaceType.typeArguments; |
| 1483 // check if has arguments |
| 1484 bool hasArguments = false; |
| 1485 for (DartType argument in arguments) { |
| 1486 if (!argument.isDynamic) { |
| 1487 hasArguments = true; |
| 1488 break; |
| 1489 } |
| 1490 } |
| 1491 // append type arguments |
| 1492 if (hasArguments) { |
| 1493 sb.append("<"); |
| 1494 for (int i = 0; i < arguments.length; i++) { |
| 1495 DartType argument = arguments[i]; |
| 1496 if (i != 0) { |
| 1497 sb.append(", "); |
| 1498 } |
| 1499 sb.append(getTypeSource2(argument)); |
| 1500 } |
| 1501 sb.append(">"); |
| 1502 } |
| 1503 } |
| 1504 // done |
| 1505 return sb.toString(); |
| 1506 } |
| 1507 |
| 1508 /** |
| 1509 * @return the source of the inverted condition for the given logical expressi
on. |
| 1510 */ |
| 1511 String invertCondition(Expression expression) => _invertCondition0(expression)
._source; |
| 1512 |
| 1513 /** |
| 1514 * @return <code>true</code> if selection range contains only whitespace. |
| 1515 */ |
| 1516 bool isJustWhitespace(SourceRange range) => getText3(range).trim().length == 0
; |
| 1517 |
| 1518 /** |
| 1519 * @return <code>true</code> if selection range contains only whitespace or co
mments |
| 1520 */ |
| 1521 bool isJustWhitespaceOrComment(SourceRange range) { |
| 1522 String trimmedText = getText3(range).trim(); |
| 1523 // may be whitespace |
| 1524 if (trimmedText.isEmpty) { |
| 1525 return true; |
| 1526 } |
| 1527 // may be comment |
| 1528 return TokenUtils.getTokens(trimmedText).isEmpty; |
| 1529 } |
| 1530 |
| 1531 /** |
| 1532 * @return <code>true</code> if "selection" covers "node" and there are any no
n-whitespace tokens |
| 1533 * between "selection" and "node" start/end. |
| 1534 */ |
| 1535 bool selectionIncludesNonWhitespaceOutsideNode(SourceRange selection, AstNode
node) => _selectionIncludesNonWhitespaceOutsideRange(selection, SourceRangeFacto
ry.rangeNode(node)); |
| 1536 |
| 1537 /** |
| 1538 * @return <code>true</code> if given range of [BinaryExpression] can be extra
cted. |
| 1539 */ |
| 1540 bool validateBinaryExpressionRange(BinaryExpression binaryExpression, SourceRa
nge range) { |
| 1541 // only parts of associative expression are safe to extract |
| 1542 if (!binaryExpression.operator.type.isAssociativeOperator) { |
| 1543 return false; |
| 1544 } |
| 1545 // prepare selected operands |
| 1546 List<Expression> operands = _getOperandsInOrderFor(binaryExpression); |
| 1547 List<Expression> subOperands = _getOperandsForSourceRange(operands, range); |
| 1548 // if empty, then something wrong with selection |
| 1549 if (subOperands.isEmpty) { |
| 1550 return false; |
| 1551 } |
| 1552 // may be some punctuation included into selection - operators, braces, etc |
| 1553 if (_selectionIncludesNonWhitespaceOutsideOperands(range, subOperands)) { |
| 1554 return false; |
| 1555 } |
| 1556 // OK |
| 1557 return true; |
| 1558 } |
| 1559 |
| 1560 /** |
| 1561 * @return the [ImportElement] used to import given [Element] into [library]. |
| 1562 * May be `null` if was not imported, i.e. declared in the same librar
y. |
| 1563 */ |
| 1564 ImportElement _getImportElement(Element element) { |
| 1565 for (ImportElement imp in _library.imports) { |
| 1566 Map<String, Element> definedNames = getImportNamespace(imp); |
| 1567 if (definedNames.containsValue(element)) { |
| 1568 return imp; |
| 1569 } |
| 1570 } |
| 1571 return null; |
| 1572 } |
| 1573 |
| 1574 /** |
| 1575 * @return the [InvertedCondition] for the given logical expression. |
| 1576 */ |
| 1577 CorrectionUtils_InvertedCondition _invertCondition0(Expression expression) { |
| 1578 if (expression is BooleanLiteral) { |
| 1579 BooleanLiteral literal = expression; |
| 1580 if (literal.value) { |
| 1581 return CorrectionUtils_InvertedCondition._simple("false"); |
| 1582 } else { |
| 1583 return CorrectionUtils_InvertedCondition._simple("true"); |
| 1584 } |
| 1585 } |
| 1586 if (expression is BinaryExpression) { |
| 1587 BinaryExpression binary = expression; |
| 1588 TokenType operator = binary.operator.type; |
| 1589 Expression le = binary.leftOperand; |
| 1590 Expression re = binary.rightOperand; |
| 1591 CorrectionUtils_InvertedCondition ls = _invertCondition0(le); |
| 1592 CorrectionUtils_InvertedCondition rs = _invertCondition0(re); |
| 1593 if (operator == TokenType.LT) { |
| 1594 return CorrectionUtils_InvertedCondition._binary2(ls, " >= ", rs); |
| 1595 } |
| 1596 if (operator == TokenType.GT) { |
| 1597 return CorrectionUtils_InvertedCondition._binary2(ls, " <= ", rs); |
| 1598 } |
| 1599 if (operator == TokenType.LT_EQ) { |
| 1600 return CorrectionUtils_InvertedCondition._binary2(ls, " > ", rs); |
| 1601 } |
| 1602 if (operator == TokenType.GT_EQ) { |
| 1603 return CorrectionUtils_InvertedCondition._binary2(ls, " < ", rs); |
| 1604 } |
| 1605 if (operator == TokenType.EQ_EQ) { |
| 1606 return CorrectionUtils_InvertedCondition._binary2(ls, " != ", rs); |
| 1607 } |
| 1608 if (operator == TokenType.BANG_EQ) { |
| 1609 return CorrectionUtils_InvertedCondition._binary2(ls, " == ", rs); |
| 1610 } |
| 1611 if (operator == TokenType.AMPERSAND_AMPERSAND) { |
| 1612 int newPrecedence = TokenType.BAR_BAR.precedence; |
| 1613 return CorrectionUtils_InvertedCondition._binary(newPrecedence, ls, " ||
", rs); |
| 1614 } |
| 1615 if (operator == TokenType.BAR_BAR) { |
| 1616 int newPrecedence = TokenType.AMPERSAND_AMPERSAND.precedence; |
| 1617 return CorrectionUtils_InvertedCondition._binary(newPrecedence, ls, " &&
", rs); |
| 1618 } |
| 1619 } |
| 1620 if (expression is IsExpression) { |
| 1621 IsExpression isExpression = expression; |
| 1622 String expressionSource = getText(isExpression.expression); |
| 1623 String typeSource = getText(isExpression.type); |
| 1624 if (isExpression.notOperator == null) { |
| 1625 return CorrectionUtils_InvertedCondition._simple("${expressionSource} is
! ${typeSource}"); |
| 1626 } else { |
| 1627 return CorrectionUtils_InvertedCondition._simple("${expressionSource} is
${typeSource}"); |
| 1628 } |
| 1629 } |
| 1630 if (expression is PrefixExpression) { |
| 1631 PrefixExpression prefixExpression = expression; |
| 1632 TokenType operator = prefixExpression.operator.type; |
| 1633 if (operator == TokenType.BANG) { |
| 1634 Expression operand = prefixExpression.operand; |
| 1635 while (operand is ParenthesizedExpression) { |
| 1636 ParenthesizedExpression pe = operand as ParenthesizedExpression; |
| 1637 operand = pe.expression; |
| 1638 } |
| 1639 return CorrectionUtils_InvertedCondition._simple(getText(operand)); |
| 1640 } |
| 1641 } |
| 1642 if (expression is ParenthesizedExpression) { |
| 1643 ParenthesizedExpression pe = expression; |
| 1644 Expression innerExpresion = pe.expression; |
| 1645 while (innerExpresion is ParenthesizedExpression) { |
| 1646 innerExpresion = (innerExpresion as ParenthesizedExpression).expression; |
| 1647 } |
| 1648 return _invertCondition0(innerExpresion); |
| 1649 } |
| 1650 DartType type = expression.bestType; |
| 1651 if (type.displayName == "bool") { |
| 1652 return CorrectionUtils_InvertedCondition._simple("!${getText(expression)}"
); |
| 1653 } |
| 1654 return CorrectionUtils_InvertedCondition._simple(getText(expression)); |
| 1655 } |
| 1656 |
| 1657 bool _selectionIncludesNonWhitespaceOutsideOperands(SourceRange selection, Lis
t<Expression> operands) => _selectionIncludesNonWhitespaceOutsideRange(selection
, SourceRangeFactory.rangeNodes(operands)); |
| 1658 |
| 1659 /** |
| 1660 * @return <code>true</code> if "selection" covers "range" and there are any n
on-whitespace tokens |
| 1661 * between "selection" and "range" start/end. |
| 1662 */ |
| 1663 bool _selectionIncludesNonWhitespaceOutsideRange(SourceRange selection, Source
Range range) { |
| 1664 // selection should cover range |
| 1665 if (!selection.covers(range)) { |
| 1666 return false; |
| 1667 } |
| 1668 // non-whitespace between selection start and range start |
| 1669 if (!isJustWhitespaceOrComment(SourceRangeFactory.rangeStartStart(selection,
range))) { |
| 1670 return true; |
| 1671 } |
| 1672 // non-whitespace after range |
| 1673 if (!isJustWhitespaceOrComment(SourceRangeFactory.rangeEndEnd(range, selecti
on))) { |
| 1674 return true; |
| 1675 } |
| 1676 // only whitespace in selection around range |
| 1677 return false; |
| 1678 } |
| 1679 } |
| 1680 |
| 1681 /** |
| 1682 * Describes where to insert new directive or top-level declaration. |
| 1683 */ |
| 1684 class CorrectionUtils_InsertDesc { |
| 1685 int offset = 0; |
| 1686 |
| 1687 String prefix = ""; |
| 1688 |
| 1689 String suffix = ""; |
| 1690 } |
| 1691 |
| 1692 /** |
| 1693 * This class is used to hold the source and also its precedence during invertin
g logical |
| 1694 * expressions. |
| 1695 */ |
| 1696 class CorrectionUtils_InvertedCondition { |
| 1697 static CorrectionUtils_InvertedCondition _binary(int precedence, CorrectionUti
ls_InvertedCondition left, String operation, CorrectionUtils_InvertedCondition r
ight) => new CorrectionUtils_InvertedCondition(precedence, "${CorrectionUtils._p
arenthesizeIfRequired(left, precedence)}${operation}${CorrectionUtils._parenthes
izeIfRequired(right, precedence)}"); |
| 1698 |
| 1699 static CorrectionUtils_InvertedCondition _binary2(CorrectionUtils_InvertedCond
ition left, String operation, CorrectionUtils_InvertedCondition right) => new Co
rrectionUtils_InvertedCondition(2147483647, "${left._source}${operation}${right.
_source}"); |
| 1700 |
| 1701 static CorrectionUtils_InvertedCondition _simple(String source) => new Correct
ionUtils_InvertedCondition(2147483647, source); |
| 1702 |
| 1703 final int _precedence; |
| 1704 |
| 1705 final String _source; |
| 1706 |
| 1707 CorrectionUtils_InvertedCondition(this._precedence, this._source); |
| 1708 } |
| 1709 |
| 1710 class GeneralizingAstVisitor_CorrectionUtils_getOperandsInOrderFor extends Gener
alizingAstVisitor<Object> { |
| 1711 TokenType groupOperatorType; |
| 1712 |
| 1713 List<Expression> operands; |
| 1714 |
| 1715 GeneralizingAstVisitor_CorrectionUtils_getOperandsInOrderFor(this.groupOperato
rType, this.operands) : super(); |
| 1716 |
| 1717 @override |
| 1718 Object visitExpression(Expression node) { |
| 1719 if (node is BinaryExpression && node.operator.type == groupOperatorType) { |
| 1720 return super.visitNode(node); |
| 1721 } |
| 1722 operands.add(node); |
| 1723 return null; |
| 1724 } |
| 1725 } |
| 1726 |
| 1727 class GeneralizingElementVisitor_CorrectionUtils_getChildren extends Generalizin
gElementVisitor<Object> { |
| 1728 Element parent; |
| 1729 |
| 1730 String name; |
| 1731 |
| 1732 List<Element> children; |
| 1733 |
| 1734 GeneralizingElementVisitor_CorrectionUtils_getChildren(this.parent, this.name,
this.children) : super(); |
| 1735 |
| 1736 @override |
| 1737 Object visitElement(Element element) { |
| 1738 if (identical(element, parent)) { |
| 1739 super.visitElement(element); |
| 1740 } else if (name == null || CorrectionUtils.hasDisplayName(element, name)) { |
| 1741 children.add(element); |
| 1742 } |
| 1743 return null; |
| 1744 } |
| 1745 } |
| 1746 |
| 1747 class GeneralizingElementVisitor_HierarchyUtils_getDirectMembers extends General
izingElementVisitor<Object> { |
| 1748 ClassElement clazz; |
| 1749 |
| 1750 bool includeSynthetic = false; |
| 1751 |
| 1752 List<Element> members; |
| 1753 |
| 1754 GeneralizingElementVisitor_HierarchyUtils_getDirectMembers(this.clazz, this.in
cludeSynthetic, this.members) : super(); |
| 1755 |
| 1756 @override |
| 1757 Object visitElement(Element element) { |
| 1758 if (identical(element, clazz)) { |
| 1759 return super.visitElement(element); |
| 1760 } |
| 1761 if (!includeSynthetic && element.isSynthetic) { |
| 1762 return null; |
| 1763 } |
| 1764 if (element is ConstructorElement) { |
| 1765 return null; |
| 1766 } |
| 1767 if (element is ExecutableElement) { |
| 1768 members.add(element); |
| 1769 } |
| 1770 if (element is FieldElement) { |
| 1771 members.add(element); |
| 1772 } |
| 1773 return null; |
| 1774 } |
| 1775 } |
| 1776 |
| 1777 class NameOccurrencesFinder extends RecursiveAstVisitor<Object> { |
| 1778 static Iterable<AstNode> findIn(SimpleIdentifier ident, AstNode root) { |
| 1779 if (ident == null || ident.bestElement == null) { |
| 1780 return new Set<AstNode>(); |
| 1781 } |
| 1782 NameOccurrencesFinder finder = new NameOccurrencesFinder(ident.bestElement); |
| 1783 root.accept(finder); |
| 1784 return finder.matches; |
| 1785 } |
| 1786 |
| 1787 Element _target; |
| 1788 |
| 1789 Element _target2; |
| 1790 |
| 1791 Element _target3; |
| 1792 |
| 1793 Element _target4; |
| 1794 |
| 1795 Set<AstNode> _matches; |
| 1796 |
| 1797 NameOccurrencesFinder(Element source) { |
| 1798 this._target = source; |
| 1799 while (true) { |
| 1800 if (source.kind == ElementKind.GETTER || source.kind == ElementKind.SETTER
) { |
| 1801 PropertyAccessorElement accessorElem = source as PropertyAccessorElement
; |
| 1802 this._target2 = accessorElem.variable; |
| 1803 if (source is Member) { |
| 1804 Member member = source; |
| 1805 this._target4 = member.baseElement; |
| 1806 } |
| 1807 if (this._target2 is Member) { |
| 1808 Member member = source as Member; |
| 1809 this._target3 = member.baseElement; |
| 1810 } |
| 1811 } else if (source.kind == ElementKind.FIELD || source.kind == ElementKind.
TOP_LEVEL_VARIABLE) { |
| 1812 PropertyInducingElement propertyElem = source as PropertyInducingElement
; |
| 1813 this._target2 = propertyElem.getter; |
| 1814 this._target3 = propertyElem.setter; |
| 1815 } else if (source.kind == ElementKind.METHOD) { |
| 1816 if (source is Member) { |
| 1817 Member member = source; |
| 1818 this._target4 = member.baseElement; |
| 1819 } |
| 1820 } else if (source.kind == ElementKind.PARAMETER) { |
| 1821 ParameterElement param = source as ParameterElement; |
| 1822 if (param.isInitializingFormal) { |
| 1823 FieldFormalParameterElement fieldInit = param as FieldFormalParameterE
lement; |
| 1824 this._target2 = fieldInit.field; |
| 1825 } |
| 1826 } else { |
| 1827 } |
| 1828 break; |
| 1829 } |
| 1830 if (_target2 == null) { |
| 1831 _target2 = _target; |
| 1832 } |
| 1833 if (_target3 == null) { |
| 1834 _target3 = _target; |
| 1835 } |
| 1836 if (_target4 == null) { |
| 1837 _target4 = _target; |
| 1838 } |
| 1839 this._matches = new Set<AstNode>(); |
| 1840 } |
| 1841 |
| 1842 Iterable<AstNode> get matches => _matches; |
| 1843 |
| 1844 @override |
| 1845 Object visitSimpleIdentifier(SimpleIdentifier node) { |
| 1846 Element element = node.bestElement; |
| 1847 if (element == null) { |
| 1848 return null; |
| 1849 } |
| 1850 _match(element, node); |
| 1851 if (element is Member) { |
| 1852 Member member = element; |
| 1853 _match(member.baseElement, node); |
| 1854 } |
| 1855 while (true) { |
| 1856 if (element.kind == ElementKind.GETTER || element.kind == ElementKind.SETT
ER) { |
| 1857 PropertyAccessorElement accessorElem = element as PropertyAccessorElemen
t; |
| 1858 _match(accessorElem.variable, node); |
| 1859 } else if (element.kind == ElementKind.FIELD || element.kind == ElementKin
d.TOP_LEVEL_VARIABLE) { |
| 1860 PropertyInducingElement propertyElem = element as PropertyInducingElemen
t; |
| 1861 _match(propertyElem.getter, node); |
| 1862 _match(propertyElem.setter, node); |
| 1863 } else if (element.kind == ElementKind.PARAMETER) { |
| 1864 ParameterElement param = element as ParameterElement; |
| 1865 if (param.isInitializingFormal) { |
| 1866 FieldFormalParameterElement fieldInit = param as FieldFormalParameterE
lement; |
| 1867 _match(fieldInit.field, node); |
| 1868 } |
| 1869 } else { |
| 1870 } |
| 1871 break; |
| 1872 } |
| 1873 return null; |
| 1874 } |
| 1875 |
| 1876 void _match(Element element, AstNode node) { |
| 1877 if (identical(_target, element) || identical(_target2, element) || identical
(_target3, element) || identical(_target4, element)) { |
| 1878 _matches.add(node); |
| 1879 } |
| 1880 } |
| 1881 } |
| 1882 |
| 1883 /** |
| 1884 * Abstract visitor for visiting [AstNode]s covered by the selection [SourceRang
e]. |
| 1885 */ |
| 1886 class SelectionAnalyzer extends GeneralizingAstVisitor<Object> { |
| 1887 SourceRange selection; |
| 1888 |
| 1889 AstNode _coveringNode; |
| 1890 |
| 1891 List<AstNode> _selectedNodes; |
| 1892 |
| 1893 SelectionAnalyzer(SourceRange selection) { |
| 1894 assert(selection != null); |
| 1895 this.selection = selection; |
| 1896 } |
| 1897 |
| 1898 /** |
| 1899 * @return the [AstNode] with the shortest length which completely covers the
specified |
| 1900 * selection. |
| 1901 */ |
| 1902 AstNode get coveringNode => _coveringNode; |
| 1903 |
| 1904 /** |
| 1905 * @return the first selected [AstNode], may be <code>null</code>. |
| 1906 */ |
| 1907 AstNode get firstSelectedNode { |
| 1908 if (_selectedNodes == null || _selectedNodes.isEmpty) { |
| 1909 return null; |
| 1910 } |
| 1911 return _selectedNodes[0]; |
| 1912 } |
| 1913 |
| 1914 /** |
| 1915 * @return the last selected [AstNode], may be <code>null</code>. |
| 1916 */ |
| 1917 AstNode get lastSelectedNode { |
| 1918 if (_selectedNodes == null || _selectedNodes.isEmpty) { |
| 1919 return null; |
| 1920 } |
| 1921 return _selectedNodes[_selectedNodes.length - 1]; |
| 1922 } |
| 1923 |
| 1924 /** |
| 1925 * @return the [SourceRange] which covers selected [AstNode]s, may be |
| 1926 * <code>null</code> if no [AstNode]s under selection. |
| 1927 */ |
| 1928 SourceRange get selectedNodeRange { |
| 1929 if (_selectedNodes == null || _selectedNodes.isEmpty) { |
| 1930 return null; |
| 1931 } |
| 1932 AstNode firstNode = _selectedNodes[0]; |
| 1933 AstNode lastNode = _selectedNodes[_selectedNodes.length - 1]; |
| 1934 return SourceRangeFactory.rangeStartEnd(firstNode, lastNode); |
| 1935 } |
| 1936 |
| 1937 /** |
| 1938 * @return the [AstNode]s fully covered by the selection [SourceRange]. |
| 1939 */ |
| 1940 List<AstNode> get selectedNodes { |
| 1941 if (_selectedNodes == null || _selectedNodes.isEmpty) { |
| 1942 return []; |
| 1943 } |
| 1944 return _selectedNodes; |
| 1945 } |
| 1946 |
| 1947 /** |
| 1948 * @return <code>true</code> if there are [AstNode] fully covered by the selec
tion |
| 1949 * [SourceRange]. |
| 1950 */ |
| 1951 bool get hasSelectedNodes => _selectedNodes != null && !_selectedNodes.isEmpty
; |
| 1952 |
| 1953 @override |
| 1954 Object visitNode(AstNode node) { |
| 1955 SourceRange nodeRange = SourceRangeFactory.rangeNode(node); |
| 1956 if (selection.covers(nodeRange)) { |
| 1957 if (isFirstNode) { |
| 1958 handleFirstSelectedNode(node); |
| 1959 } else { |
| 1960 handleNextSelectedNode(node); |
| 1961 } |
| 1962 return null; |
| 1963 } else if (selection.coveredBy(nodeRange)) { |
| 1964 _coveringNode = node; |
| 1965 node.visitChildren(this); |
| 1966 return null; |
| 1967 } else if (selection.startsIn(nodeRange)) { |
| 1968 handleSelectionStartsIn(node); |
| 1969 node.visitChildren(this); |
| 1970 return null; |
| 1971 } else if (selection.endsIn(nodeRange)) { |
| 1972 handleSelectionEndsIn(node); |
| 1973 node.visitChildren(this); |
| 1974 return null; |
| 1975 } |
| 1976 // no intersection |
| 1977 return null; |
| 1978 } |
| 1979 |
| 1980 /** |
| 1981 * Adds first selected [AstNode]. |
| 1982 */ |
| 1983 void handleFirstSelectedNode(AstNode node) { |
| 1984 _selectedNodes = []; |
| 1985 _selectedNodes.add(node); |
| 1986 } |
| 1987 |
| 1988 /** |
| 1989 * Adds second or more selected [AstNode]. |
| 1990 */ |
| 1991 void handleNextSelectedNode(AstNode node) { |
| 1992 if (identical(firstSelectedNode.parent, node.parent)) { |
| 1993 _selectedNodes.add(node); |
| 1994 } |
| 1995 } |
| 1996 |
| 1997 /** |
| 1998 * Notifies that selection ends in given [AstNode]. |
| 1999 */ |
| 2000 void handleSelectionEndsIn(AstNode node) { |
| 2001 } |
| 2002 |
| 2003 /** |
| 2004 * Notifies that selection starts in given [AstNode]. |
| 2005 */ |
| 2006 void handleSelectionStartsIn(AstNode node) { |
| 2007 } |
| 2008 |
| 2009 /** |
| 2010 * Resets selected nodes. |
| 2011 */ |
| 2012 void reset() { |
| 2013 _selectedNodes = null; |
| 2014 } |
| 2015 |
| 2016 /** |
| 2017 * @return <code>true</code> if there was no selected nodes yet. |
| 2018 */ |
| 2019 bool get isFirstNode => _selectedNodes == null; |
| 2020 } |
| 2021 |
| 2022 /** |
| 2023 * Helper for building Dart source with tracked positions. |
| 2024 */ |
| 2025 class SourceBuilder { |
| 2026 final int offset; |
| 2027 |
| 2028 JavaStringBuilder _buffer = new JavaStringBuilder(); |
| 2029 |
| 2030 Map<String, List<SourceRange>> _linkedPositions = {}; |
| 2031 |
| 2032 final Map<String, List<LinkedPositionProposal>> linkedProposals = {}; |
| 2033 |
| 2034 String _currentPositionGroupId; |
| 2035 |
| 2036 int _currentPositionStart = 0; |
| 2037 |
| 2038 int _endPosition = -1; |
| 2039 |
| 2040 SourceBuilder.con1(this.offset); |
| 2041 |
| 2042 SourceBuilder.con2(SourceRange offsetRange) : this.con1(offsetRange.offset); |
| 2043 |
| 2044 /** |
| 2045 * Adds proposal for the current position, may be called after [startPosition]
. |
| 2046 */ |
| 2047 void addProposal(CorrectionImage icon, String text) { |
| 2048 List<LinkedPositionProposal> proposals = linkedProposals[_currentPositionGro
upId]; |
| 2049 if (proposals == null) { |
| 2050 proposals = []; |
| 2051 linkedProposals[_currentPositionGroupId] = proposals; |
| 2052 } |
| 2053 proposals.add(new LinkedPositionProposal(icon, text)); |
| 2054 } |
| 2055 |
| 2056 /** |
| 2057 * Appends source to the buffer. |
| 2058 */ |
| 2059 SourceBuilder append(String s) { |
| 2060 _buffer.append(s); |
| 2061 return this; |
| 2062 } |
| 2063 |
| 2064 /** |
| 2065 * Ends position started using [startPosition]. |
| 2066 */ |
| 2067 void endPosition() { |
| 2068 assert(_currentPositionGroupId != null); |
| 2069 _addPosition(); |
| 2070 _currentPositionGroupId = null; |
| 2071 } |
| 2072 |
| 2073 /** |
| 2074 * @return the "end position" for the [CorrectionProposal], may be <code>-1</c
ode> if not |
| 2075 * set in this [SourceBuilder]. |
| 2076 */ |
| 2077 int get endPosition2 { |
| 2078 if (_endPosition == -1) { |
| 2079 return -1; |
| 2080 } |
| 2081 return offset + _endPosition; |
| 2082 } |
| 2083 |
| 2084 /** |
| 2085 * @return the [Map] or position IDs to their locations. |
| 2086 */ |
| 2087 Map<String, List<SourceRange>> get linkedPositions => _linkedPositions; |
| 2088 |
| 2089 /** |
| 2090 * @return the length of the built source. |
| 2091 */ |
| 2092 int length() => _buffer.length; |
| 2093 |
| 2094 /** |
| 2095 * Marks current position as "end position" of the [CorrectionProposal]. |
| 2096 */ |
| 2097 void setEndPosition() { |
| 2098 _endPosition = _buffer.length; |
| 2099 } |
| 2100 |
| 2101 /** |
| 2102 * Sets text-only proposals for the current position. |
| 2103 */ |
| 2104 void set proposals(List<String> proposals) { |
| 2105 List<LinkedPositionProposal> proposalList = []; |
| 2106 for (String proposalText in proposals) { |
| 2107 proposalList.add(new LinkedPositionProposal(null, proposalText)); |
| 2108 } |
| 2109 linkedProposals[_currentPositionGroupId] = proposalList; |
| 2110 } |
| 2111 |
| 2112 /** |
| 2113 * Starts linked position with given ID. |
| 2114 */ |
| 2115 void startPosition(String groupId) { |
| 2116 assert(_currentPositionGroupId == null); |
| 2117 _currentPositionGroupId = groupId; |
| 2118 _currentPositionStart = _buffer.length; |
| 2119 } |
| 2120 |
| 2121 @override |
| 2122 String toString() => _buffer.toString(); |
| 2123 |
| 2124 /** |
| 2125 * Adds position location [SourceRange] using current fields. |
| 2126 */ |
| 2127 void _addPosition() { |
| 2128 List<SourceRange> locations = _linkedPositions[_currentPositionGroupId]; |
| 2129 if (locations == null) { |
| 2130 locations = []; |
| 2131 _linkedPositions[_currentPositionGroupId] = locations; |
| 2132 } |
| 2133 int start = offset + _currentPositionStart; |
| 2134 int end = offset + _buffer.length; |
| 2135 locations.add(SourceRangeFactory.rangeStartEnd(start, end)); |
| 2136 } |
| 2137 } |
| 2138 |
| 2139 /** |
| 2140 * Analyzer to check if a selection covers a valid set of statements of AST. |
| 2141 */ |
| 2142 class StatementAnalyzer extends SelectionAnalyzer { |
| 2143 /** |
| 2144 * @return <code>true</code> if "nodes" contains "node". |
| 2145 */ |
| 2146 static bool _contains(List<AstNode> nodes, AstNode node) => nodes.contains(nod
e); |
| 2147 |
| 2148 /** |
| 2149 * @return <code>true</code> if "nodes" contains one of the "otherNodes". |
| 2150 */ |
| 2151 static bool _contains2(List<AstNode> nodes, List<AstNode> otherNodes) { |
| 2152 for (AstNode otherNode in otherNodes) { |
| 2153 if (nodes.contains(otherNode)) { |
| 2154 return true; |
| 2155 } |
| 2156 } |
| 2157 return false; |
| 2158 } |
| 2159 |
| 2160 CorrectionUtils utils; |
| 2161 |
| 2162 RefactoringStatus _status = new RefactoringStatus(); |
| 2163 |
| 2164 StatementAnalyzer.con1(CompilationUnit cunit, SourceRange selection) : this.co
n2(new CorrectionUtils(cunit), selection); |
| 2165 |
| 2166 StatementAnalyzer.con2(CorrectionUtils utils, SourceRange selection) : super(s
election) { |
| 2167 this.utils = utils; |
| 2168 } |
| 2169 |
| 2170 /** |
| 2171 * @return the [RefactoringStatus] result of checking selection. |
| 2172 */ |
| 2173 RefactoringStatus get status => _status; |
| 2174 |
| 2175 @override |
| 2176 Object visitCompilationUnit(CompilationUnit node) { |
| 2177 super.visitCompilationUnit(node); |
| 2178 if (!hasSelectedNodes) { |
| 2179 return null; |
| 2180 } |
| 2181 // check that selection does not begin/end in comment |
| 2182 { |
| 2183 int selectionStart = selection.offset; |
| 2184 int selectionEnd = selection.end; |
| 2185 List<SourceRange> commentRanges = utils.commentRanges; |
| 2186 for (SourceRange commentRange in commentRanges) { |
| 2187 if (commentRange.contains(selectionStart)) { |
| 2188 invalidSelection("Selection begins inside a comment."); |
| 2189 } |
| 2190 if (commentRange.containsExclusive(selectionEnd)) { |
| 2191 invalidSelection("Selection ends inside a comment."); |
| 2192 } |
| 2193 } |
| 2194 } |
| 2195 // more checks |
| 2196 if (!_status.hasFatalError) { |
| 2197 _checkSelectedNodes(node); |
| 2198 } |
| 2199 return null; |
| 2200 } |
| 2201 |
| 2202 @override |
| 2203 Object visitDoStatement(DoStatement node) { |
| 2204 super.visitDoStatement(node); |
| 2205 List<AstNode> selectedNodes = this.selectedNodes; |
| 2206 if (_contains(selectedNodes, node.body)) { |
| 2207 invalidSelection("Operation not applicable to a 'do' statement's body and
expression."); |
| 2208 } |
| 2209 return null; |
| 2210 } |
| 2211 |
| 2212 @override |
| 2213 Object visitForStatement(ForStatement node) { |
| 2214 super.visitForStatement(node); |
| 2215 List<AstNode> selectedNodes = this.selectedNodes; |
| 2216 bool containsInit = _contains(selectedNodes, node.initialization) || _contai
ns(selectedNodes, node.variables); |
| 2217 bool containsCondition = _contains(selectedNodes, node.condition); |
| 2218 bool containsUpdaters = _contains2(selectedNodes, node.updaters); |
| 2219 bool containsBody = _contains(selectedNodes, node.body); |
| 2220 if (containsInit && containsCondition) { |
| 2221 invalidSelection("Operation not applicable to a 'for' statement's initiali
zer and condition."); |
| 2222 } else if (containsCondition && containsUpdaters) { |
| 2223 invalidSelection("Operation not applicable to a 'for' statement's conditio
n and updaters."); |
| 2224 } else if (containsUpdaters && containsBody) { |
| 2225 invalidSelection("Operation not applicable to a 'for' statement's updaters
and body."); |
| 2226 } |
| 2227 return null; |
| 2228 } |
| 2229 |
| 2230 @override |
| 2231 Object visitSwitchStatement(SwitchStatement node) { |
| 2232 super.visitSwitchStatement(node); |
| 2233 List<AstNode> selectedNodes = this.selectedNodes; |
| 2234 List<SwitchMember> switchMembers = node.members; |
| 2235 for (AstNode selectedNode in selectedNodes) { |
| 2236 if (switchMembers.contains(selectedNode)) { |
| 2237 invalidSelection("Selection must either cover whole switch statement or
parts of a single case block."); |
| 2238 break; |
| 2239 } |
| 2240 } |
| 2241 return null; |
| 2242 } |
| 2243 |
| 2244 @override |
| 2245 Object visitTryStatement(TryStatement node) { |
| 2246 super.visitTryStatement(node); |
| 2247 AstNode firstSelectedNode = this.firstSelectedNode; |
| 2248 if (firstSelectedNode != null) { |
| 2249 if (identical(firstSelectedNode, node.body) || identical(firstSelectedNode
, node.finallyBlock)) { |
| 2250 invalidSelection("Selection must either cover whole try statement or par
ts of try, catch, or finally block."); |
| 2251 } else { |
| 2252 List<CatchClause> catchClauses = node.catchClauses; |
| 2253 for (CatchClause catchClause in catchClauses) { |
| 2254 if (identical(firstSelectedNode, catchClause) || identical(firstSelect
edNode, catchClause.body) || identical(firstSelectedNode, catchClause.exceptionP
arameter)) { |
| 2255 invalidSelection("Selection must either cover whole try statement or
parts of try, catch, or finally block."); |
| 2256 } |
| 2257 } |
| 2258 } |
| 2259 } |
| 2260 return null; |
| 2261 } |
| 2262 |
| 2263 @override |
| 2264 Object visitWhileStatement(WhileStatement node) { |
| 2265 super.visitWhileStatement(node); |
| 2266 List<AstNode> selectedNodes = this.selectedNodes; |
| 2267 if (_contains(selectedNodes, node.condition) && _contains(selectedNodes, nod
e.body)) { |
| 2268 invalidSelection("Operation not applicable to a while statement's expressi
on and body."); |
| 2269 } |
| 2270 return null; |
| 2271 } |
| 2272 |
| 2273 /** |
| 2274 * Records fatal error with given message. |
| 2275 */ |
| 2276 void invalidSelection(String message) { |
| 2277 invalidSelection2(message, null); |
| 2278 } |
| 2279 |
| 2280 /** |
| 2281 * Records fatal error with given message and [RefactoringStatusContext]. |
| 2282 */ |
| 2283 void invalidSelection2(String message, RefactoringStatusContext context) { |
| 2284 _status.addFatalError(message, context); |
| 2285 reset(); |
| 2286 } |
| 2287 |
| 2288 /** |
| 2289 * Checks final selected [AstNode]s after processing [CompilationUnit]. |
| 2290 */ |
| 2291 void _checkSelectedNodes(CompilationUnit unit) { |
| 2292 List<AstNode> nodes = selectedNodes; |
| 2293 // some tokens before first selected node |
| 2294 { |
| 2295 AstNode firstNode = nodes[0]; |
| 2296 SourceRange rangeBeforeFirstNode = SourceRangeFactory.rangeStartStart(sele
ction, firstNode); |
| 2297 if (_hasTokens(rangeBeforeFirstNode)) { |
| 2298 invalidSelection2("The beginning of the selection contains characters th
at do not belong to a statement.", new RefactoringStatusContext.forUnit(unit, ra
ngeBeforeFirstNode)); |
| 2299 } |
| 2300 } |
| 2301 // some tokens after last selected node |
| 2302 { |
| 2303 AstNode lastNode = nodes.last; |
| 2304 SourceRange rangeAfterLastNode = SourceRangeFactory.rangeEndEnd(lastNode,
selection); |
| 2305 if (_hasTokens(rangeAfterLastNode)) { |
| 2306 invalidSelection2("The end of the selection contains characters that do
not belong to a statement.", new RefactoringStatusContext.forUnit(unit, rangeAft
erLastNode)); |
| 2307 } |
| 2308 } |
| 2309 } |
| 2310 |
| 2311 /** |
| 2312 * @return the [Token]s in given [SourceRange]. |
| 2313 */ |
| 2314 List<Token> _getTokens(SourceRange range) { |
| 2315 try { |
| 2316 String text = utils.getText3(range); |
| 2317 return TokenUtils.getTokens(text); |
| 2318 } catch (e) { |
| 2319 return []; |
| 2320 } |
| 2321 } |
| 2322 |
| 2323 /** |
| 2324 * @return <code>true</code> if there are [Token]s in the given [SourceRange]. |
| 2325 */ |
| 2326 bool _hasTokens(SourceRange range) => !_getTokens(range).isEmpty; |
| 2327 } |
| 2328 |
| 2329 /** |
| 2330 * Utilities to work with [Token]s. |
| 2331 */ |
| 2332 class TokenUtils { |
| 2333 /** |
| 2334 * @return the first [KeywordToken] with given [Keyword], may be <code>null</c
ode> if |
| 2335 * not found. |
| 2336 */ |
| 2337 static KeywordToken findKeywordToken(List<Token> tokens, Keyword keyword) { |
| 2338 for (Token token in tokens) { |
| 2339 if (token is KeywordToken) { |
| 2340 KeywordToken keywordToken = token; |
| 2341 if (keywordToken.keyword == keyword) { |
| 2342 return keywordToken; |
| 2343 } |
| 2344 } |
| 2345 } |
| 2346 return null; |
| 2347 } |
| 2348 |
| 2349 /** |
| 2350 * @return the first [Token] with given [TokenType], may be <code>null</code>
if not |
| 2351 * found. |
| 2352 */ |
| 2353 static Token findToken(List<Token> tokens, TokenType type) { |
| 2354 for (Token token in tokens) { |
| 2355 if (token.type == type) { |
| 2356 return token; |
| 2357 } |
| 2358 } |
| 2359 return null; |
| 2360 } |
| 2361 |
| 2362 /** |
| 2363 * @return [Token]s of the given Dart source, not <code>null</code>, may be em
pty if no |
| 2364 * tokens or some exception happens. |
| 2365 */ |
| 2366 static List<Token> getTokens(String s) { |
| 2367 try { |
| 2368 List<Token> tokens = []; |
| 2369 Scanner scanner = new Scanner(null, new CharSequenceReader(s), null); |
| 2370 Token token = scanner.tokenize(); |
| 2371 while (token.type != TokenType.EOF) { |
| 2372 tokens.add(token); |
| 2373 token = token.next; |
| 2374 } |
| 2375 return tokens; |
| 2376 } catch (e) { |
| 2377 return []; |
| 2378 } |
| 2379 } |
| 2380 |
| 2381 /** |
| 2382 * @return <code>true</code> if given [Token]s contain only single [Token] wit
h given |
| 2383 * [TokenType]. |
| 2384 */ |
| 2385 static bool hasOnly(List<Token> tokens, TokenType type) => tokens.length == 1
&& tokens[0].type == type; |
| 2386 } |
| 2387 |
| 2388 class URIUtils { |
| 2389 /** |
| 2390 * Computes relative relative path to reference "target" from "base". Uses "..
" if needed, in |
| 2391 * contrast to [URI#relativize]. |
| 2392 */ |
| 2393 static String computeRelativePath(String base, String target) { |
| 2394 // convert to URI separator |
| 2395 base = base.replaceAll("\\\\", "/"); |
| 2396 target = target.replaceAll("\\\\", "/"); |
| 2397 if (base.startsWith("/") && target.startsWith("/")) { |
| 2398 base = base.substring(1); |
| 2399 target = target.substring(1); |
| 2400 } |
| 2401 // equal paths - no relative |
| 2402 if (base == target) { |
| 2403 return null; |
| 2404 } |
| 2405 // split paths |
| 2406 List<String> baseParts = base.split("/"); |
| 2407 List<String> targetParts = target.split("/"); |
| 2408 // prepare maximum possible common root length |
| 2409 int length = baseParts.length < targetParts.length ? baseParts.length : targ
etParts.length; |
| 2410 // find common root |
| 2411 int lastCommonRoot = -1; |
| 2412 for (int i = 0; i < length; i++) { |
| 2413 if (baseParts[i] == targetParts[i]) { |
| 2414 lastCommonRoot = i; |
| 2415 } else { |
| 2416 break; |
| 2417 } |
| 2418 } |
| 2419 // append .. |
| 2420 JavaStringBuilder relativePath = new JavaStringBuilder(); |
| 2421 for (int i = lastCommonRoot + 1; i < baseParts.length; i++) { |
| 2422 if (baseParts[i].length > 0) { |
| 2423 relativePath.append("../"); |
| 2424 } |
| 2425 } |
| 2426 // append target folder names |
| 2427 for (int i = lastCommonRoot + 1; i < targetParts.length - 1; i++) { |
| 2428 String p = targetParts[i]; |
| 2429 relativePath.append(p); |
| 2430 relativePath.append("/"); |
| 2431 } |
| 2432 // append target file name |
| 2433 relativePath.append(targetParts[targetParts.length - 1]); |
| 2434 // done |
| 2435 return relativePath.toString(); |
| 2436 } |
| 2437 } |
| OLD | NEW |