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 engine.test_support; |
| 6 |
| 7 import 'dart:collection'; |
| 8 |
| 9 import 'package:analyzer/src/generated/ast.dart' show AstNode, NodeLocator; |
| 10 import 'package:analyzer/src/generated/element.dart'; |
| 11 import 'package:analyzer/src/generated/engine.dart'; |
| 12 import 'package:analyzer/src/generated/error.dart'; |
| 13 import 'package:analyzer/src/generated/java_core.dart'; |
| 14 import 'package:analyzer/src/generated/java_engine.dart'; |
| 15 import 'package:analyzer/src/generated/source.dart'; |
| 16 import 'package:unittest/unittest.dart'; |
| 17 |
| 18 import 'resolver_test.dart'; |
| 19 |
| 20 /** |
| 21 * The class `EngineTestCase` defines utility methods for making assertions. |
| 22 */ |
| 23 class EngineTestCase { |
| 24 /** |
| 25 * Assert that the given collection has the same number of elements as the num
ber of specified |
| 26 * names, and that for each specified name, a corresponding element can be fou
nd in the given |
| 27 * collection with that name. |
| 28 * |
| 29 * @param elements the elements |
| 30 * @param names the names |
| 31 */ |
| 32 void assertNamedElements(List<Element> elements, List<String> names) { |
| 33 for (String elemName in names) { |
| 34 bool found = false; |
| 35 for (Element elem in elements) { |
| 36 if (elem.name == elemName) { |
| 37 found = true; |
| 38 break; |
| 39 } |
| 40 } |
| 41 if (!found) { |
| 42 StringBuffer buffer = new StringBuffer(); |
| 43 buffer.write("Expected element named: "); |
| 44 buffer.write(elemName); |
| 45 buffer.write("\n but found: "); |
| 46 for (Element elem in elements) { |
| 47 buffer.write(elem.name); |
| 48 buffer.write(", "); |
| 49 } |
| 50 fail(buffer.toString()); |
| 51 } |
| 52 } |
| 53 expect(elements, hasLength(names.length)); |
| 54 } |
| 55 |
| 56 AnalysisContext createAnalysisContext() { |
| 57 return AnalysisContextFactory.contextWithCore(); |
| 58 } |
| 59 |
| 60 /** |
| 61 * Return the getter in the given type with the given name. Inherited getters
are ignored. |
| 62 * |
| 63 * @param type the type in which the getter is declared |
| 64 * @param getterName the name of the getter to be returned |
| 65 * @return the property accessor element representing the getter with the give
n name |
| 66 */ |
| 67 PropertyAccessorElement getGetter(InterfaceType type, String getterName) { |
| 68 for (PropertyAccessorElement accessor in type.element.accessors) { |
| 69 if (accessor.isGetter && accessor.name == getterName) { |
| 70 return accessor; |
| 71 } |
| 72 } |
| 73 fail("Could not find getter named $getterName in ${type.displayName}"); |
| 74 return null; |
| 75 } |
| 76 |
| 77 /** |
| 78 * Return the method in the given type with the given name. Inherited methods
are ignored. |
| 79 * |
| 80 * @param type the type in which the method is declared |
| 81 * @param methodName the name of the method to be returned |
| 82 * @return the method element representing the method with the given name |
| 83 */ |
| 84 MethodElement getMethod(InterfaceType type, String methodName) { |
| 85 for (MethodElement method in type.element.methods) { |
| 86 if (method.name == methodName) { |
| 87 return method; |
| 88 } |
| 89 } |
| 90 fail("Could not find method named $methodName in ${type.displayName}"); |
| 91 return null; |
| 92 } |
| 93 |
| 94 void setUp() {} |
| 95 |
| 96 void tearDown() {} |
| 97 |
| 98 /** |
| 99 * Assert that the given object is an instance of the expected class. |
| 100 * |
| 101 * @param expectedClass the class that the object is expected to be an instanc
e of |
| 102 * @param object the object being tested |
| 103 * @return the object that was being tested |
| 104 * @throws Exception if the object is not an instance of the expected class |
| 105 */ |
| 106 static Object assertInstanceOf( |
| 107 Predicate<Object> predicate, Type expectedClass, Object object) { |
| 108 if (!predicate(object)) { |
| 109 fail( |
| 110 "Expected instance of $expectedClass, found ${object == null ? "null"
: object.runtimeType}"); |
| 111 } |
| 112 return object; |
| 113 } |
| 114 |
| 115 /** |
| 116 * @return the [AstNode] with requested type at offset of the "prefix". |
| 117 */ |
| 118 static AstNode findNode( |
| 119 AstNode root, String code, String prefix, Predicate<AstNode> predicate) { |
| 120 int offset = code.indexOf(prefix); |
| 121 if (offset == -1) { |
| 122 throw new IllegalArgumentException("Not found '$prefix'."); |
| 123 } |
| 124 AstNode node = new NodeLocator(offset).searchWithin(root); |
| 125 return node.getAncestor(predicate); |
| 126 } |
| 127 } |
| 128 |
| 129 /** |
| 130 * Instances of the class `GatheringErrorListener` implement an error listener t
hat collects |
| 131 * all of the errors passed to it for later examination. |
| 132 */ |
| 133 class GatheringErrorListener implements AnalysisErrorListener { |
| 134 /** |
| 135 * An empty array of errors used when no errors are expected. |
| 136 */ |
| 137 static List<AnalysisError> _NO_ERRORS = new List<AnalysisError>(0); |
| 138 |
| 139 /** |
| 140 * A list containing the errors that were collected. |
| 141 */ |
| 142 List<AnalysisError> _errors = new List<AnalysisError>(); |
| 143 |
| 144 /** |
| 145 * A table mapping sources to the line information for the source. |
| 146 */ |
| 147 HashMap<Source, LineInfo> _lineInfoMap = new HashMap<Source, LineInfo>(); |
| 148 |
| 149 /** |
| 150 * Initialize a newly created error listener to collect errors. |
| 151 */ |
| 152 GatheringErrorListener(); |
| 153 |
| 154 /** |
| 155 * Return the errors that were collected. |
| 156 * |
| 157 * @return the errors that were collected |
| 158 */ |
| 159 List<AnalysisError> get errors => _errors; |
| 160 |
| 161 /** |
| 162 * Return `true` if at least one error has been gathered. |
| 163 * |
| 164 * @return `true` if at least one error has been gathered |
| 165 */ |
| 166 bool get hasErrors => _errors.length > 0; |
| 167 |
| 168 /** |
| 169 * Add all of the given errors to this listener. |
| 170 * |
| 171 * @param the errors to be added |
| 172 */ |
| 173 void addAll(List<AnalysisError> errors) { |
| 174 for (AnalysisError error in errors) { |
| 175 onError(error); |
| 176 } |
| 177 } |
| 178 |
| 179 /** |
| 180 * Add all of the errors recorded by the given listener to this listener. |
| 181 * |
| 182 * @param listener the listener that has recorded the errors to be added |
| 183 */ |
| 184 void addAll2(RecordingErrorListener listener) { |
| 185 addAll(listener.errors); |
| 186 } |
| 187 |
| 188 /** |
| 189 * Assert that the number of errors that have been gathered matches the number
of errors that are |
| 190 * given and that they have the expected error codes and locations. The order
in which the errors |
| 191 * were gathered is ignored. |
| 192 * |
| 193 * @param errorCodes the errors that should have been gathered |
| 194 * @throws AssertionFailedError if a different number of errors have been gath
ered than were |
| 195 * expected or if they do not have the same codes and locations |
| 196 */ |
| 197 void assertErrors(List<AnalysisError> expectedErrors) { |
| 198 if (_errors.length != expectedErrors.length) { |
| 199 _fail(expectedErrors); |
| 200 } |
| 201 List<AnalysisError> remainingErrors = new List<AnalysisError>(); |
| 202 for (AnalysisError error in expectedErrors) { |
| 203 remainingErrors.add(error); |
| 204 } |
| 205 for (AnalysisError error in _errors) { |
| 206 if (!_foundAndRemoved(remainingErrors, error)) { |
| 207 _fail(expectedErrors); |
| 208 } |
| 209 } |
| 210 } |
| 211 |
| 212 /** |
| 213 * Assert that the number of errors that have been gathered matches the number
of errors that are |
| 214 * given and that they have the expected error codes. The order in which the e
rrors were gathered |
| 215 * is ignored. |
| 216 * |
| 217 * @param expectedErrorCodes the error codes of the errors that should have be
en gathered |
| 218 * @throws AssertionFailedError if a different number of errors have been gath
ered than were |
| 219 * expected |
| 220 */ |
| 221 void assertErrorsWithCodes( |
| 222 [List<ErrorCode> expectedErrorCodes = ErrorCode.EMPTY_LIST]) { |
| 223 StringBuffer buffer = new StringBuffer(); |
| 224 // |
| 225 // Verify that the expected error codes have a non-empty message. |
| 226 // |
| 227 for (ErrorCode errorCode in expectedErrorCodes) { |
| 228 expect(errorCode.message.isEmpty, isFalse, |
| 229 reason: "Empty error code message"); |
| 230 } |
| 231 // |
| 232 // Compute the expected number of each type of error. |
| 233 // |
| 234 HashMap<ErrorCode, int> expectedCounts = new HashMap<ErrorCode, int>(); |
| 235 for (ErrorCode code in expectedErrorCodes) { |
| 236 int count = expectedCounts[code]; |
| 237 if (count == null) { |
| 238 count = 1; |
| 239 } else { |
| 240 count = count + 1; |
| 241 } |
| 242 expectedCounts[code] = count; |
| 243 } |
| 244 // |
| 245 // Compute the actual number of each type of error. |
| 246 // |
| 247 HashMap<ErrorCode, List<AnalysisError>> errorsByCode = |
| 248 new HashMap<ErrorCode, List<AnalysisError>>(); |
| 249 for (AnalysisError error in _errors) { |
| 250 ErrorCode code = error.errorCode; |
| 251 List<AnalysisError> list = errorsByCode[code]; |
| 252 if (list == null) { |
| 253 list = new List<AnalysisError>(); |
| 254 errorsByCode[code] = list; |
| 255 } |
| 256 list.add(error); |
| 257 } |
| 258 // |
| 259 // Compare the expected and actual number of each type of error. |
| 260 // |
| 261 expectedCounts.forEach((ErrorCode code, int expectedCount) { |
| 262 int actualCount; |
| 263 List<AnalysisError> list = errorsByCode.remove(code); |
| 264 if (list == null) { |
| 265 actualCount = 0; |
| 266 } else { |
| 267 actualCount = list.length; |
| 268 } |
| 269 if (actualCount != expectedCount) { |
| 270 if (buffer.length == 0) { |
| 271 buffer.write("Expected "); |
| 272 } else { |
| 273 buffer.write("; "); |
| 274 } |
| 275 buffer.write(expectedCount); |
| 276 buffer.write(" errors of type "); |
| 277 buffer.write(code.uniqueName); |
| 278 buffer.write(", found "); |
| 279 buffer.write(actualCount); |
| 280 } |
| 281 }); |
| 282 // |
| 283 // Check that there are no more errors in the actual-errors map, |
| 284 // otherwise record message. |
| 285 // |
| 286 errorsByCode.forEach((ErrorCode code, List<AnalysisError> actualErrors) { |
| 287 int actualCount = actualErrors.length; |
| 288 if (buffer.length == 0) { |
| 289 buffer.write("Expected "); |
| 290 } else { |
| 291 buffer.write("; "); |
| 292 } |
| 293 buffer.write("0 errors of type "); |
| 294 buffer.write(code.uniqueName); |
| 295 buffer.write(", found "); |
| 296 buffer.write(actualCount); |
| 297 buffer.write(" ("); |
| 298 for (int i = 0; i < actualErrors.length; i++) { |
| 299 AnalysisError error = actualErrors[i]; |
| 300 if (i > 0) { |
| 301 buffer.write(", "); |
| 302 } |
| 303 buffer.write(error.offset); |
| 304 } |
| 305 buffer.write(")"); |
| 306 }); |
| 307 if (buffer.length > 0) { |
| 308 fail(buffer.toString()); |
| 309 } |
| 310 } |
| 311 |
| 312 /** |
| 313 * Assert that the number of errors that have been gathered matches the number
of severities that |
| 314 * are given and that there are the same number of errors and warnings as spec
ified by the |
| 315 * argument. The order in which the errors were gathered is ignored. |
| 316 * |
| 317 * @param expectedSeverities the severities of the errors that should have bee
n gathered |
| 318 * @throws AssertionFailedError if a different number of errors have been gath
ered than were |
| 319 * expected |
| 320 */ |
| 321 void assertErrorsWithSeverities(List<ErrorSeverity> expectedSeverities) { |
| 322 int expectedErrorCount = 0; |
| 323 int expectedWarningCount = 0; |
| 324 for (ErrorSeverity severity in expectedSeverities) { |
| 325 if (severity == ErrorSeverity.ERROR) { |
| 326 expectedErrorCount++; |
| 327 } else { |
| 328 expectedWarningCount++; |
| 329 } |
| 330 } |
| 331 int actualErrorCount = 0; |
| 332 int actualWarningCount = 0; |
| 333 for (AnalysisError error in _errors) { |
| 334 if (error.errorCode.errorSeverity == ErrorSeverity.ERROR) { |
| 335 actualErrorCount++; |
| 336 } else { |
| 337 actualWarningCount++; |
| 338 } |
| 339 } |
| 340 if (expectedErrorCount != actualErrorCount || |
| 341 expectedWarningCount != actualWarningCount) { |
| 342 fail( |
| 343 "Expected $expectedErrorCount errors and $expectedWarningCount warning
s, found $actualErrorCount errors and $actualWarningCount warnings"); |
| 344 } |
| 345 } |
| 346 |
| 347 /** |
| 348 * Assert that no errors have been gathered. |
| 349 * |
| 350 * @throws AssertionFailedError if any errors have been gathered |
| 351 */ |
| 352 void assertNoErrors() { |
| 353 assertErrors(_NO_ERRORS); |
| 354 } |
| 355 |
| 356 /** |
| 357 * Return the line information associated with the given source, or `null` if
no line |
| 358 * information has been associated with the source. |
| 359 * |
| 360 * @param source the source with which the line information is associated |
| 361 * @return the line information associated with the source |
| 362 */ |
| 363 LineInfo getLineInfo(Source source) => _lineInfoMap[source]; |
| 364 |
| 365 /** |
| 366 * Return `true` if an error with the given error code has been gathered. |
| 367 * |
| 368 * @param errorCode the error code being searched for |
| 369 * @return `true` if an error with the given error code has been gathered |
| 370 */ |
| 371 bool hasError(ErrorCode errorCode) { |
| 372 for (AnalysisError error in _errors) { |
| 373 if (identical(error.errorCode, errorCode)) { |
| 374 return true; |
| 375 } |
| 376 } |
| 377 return false; |
| 378 } |
| 379 |
| 380 @override |
| 381 void onError(AnalysisError error) { |
| 382 _errors.add(error); |
| 383 } |
| 384 |
| 385 /** |
| 386 * Set the line information associated with the given source to the given info
rmation. |
| 387 * |
| 388 * @param source the source with which the line information is associated |
| 389 * @param lineStarts the line start information to be associated with the sour
ce |
| 390 */ |
| 391 void setLineInfo(Source source, List<int> lineStarts) { |
| 392 _lineInfoMap[source] = new LineInfo(lineStarts); |
| 393 } |
| 394 |
| 395 /** |
| 396 * Return `true` if the two errors are equivalent. |
| 397 * |
| 398 * @param firstError the first error being compared |
| 399 * @param secondError the second error being compared |
| 400 * @return `true` if the two errors are equivalent |
| 401 */ |
| 402 bool _equalErrors(AnalysisError firstError, AnalysisError secondError) => |
| 403 identical(firstError.errorCode, secondError.errorCode) && |
| 404 firstError.offset == secondError.offset && |
| 405 firstError.length == secondError.length && |
| 406 _equalSources(firstError.source, secondError.source); |
| 407 |
| 408 /** |
| 409 * Return `true` if the two sources are equivalent. |
| 410 * |
| 411 * @param firstSource the first source being compared |
| 412 * @param secondSource the second source being compared |
| 413 * @return `true` if the two sources are equivalent |
| 414 */ |
| 415 bool _equalSources(Source firstSource, Source secondSource) { |
| 416 if (firstSource == null) { |
| 417 return secondSource == null; |
| 418 } else if (secondSource == null) { |
| 419 return false; |
| 420 } |
| 421 return firstSource == secondSource; |
| 422 } |
| 423 |
| 424 /** |
| 425 * Assert that the number of errors that have been gathered matches the number
of errors that are |
| 426 * given and that they have the expected error codes. The order in which the e
rrors were gathered |
| 427 * is ignored. |
| 428 * |
| 429 * @param errorCodes the errors that should have been gathered |
| 430 * @throws AssertionFailedError with |
| 431 */ |
| 432 void _fail(List<AnalysisError> expectedErrors) { |
| 433 StringBuffer buffer = new StringBuffer(); |
| 434 buffer.write("Expected "); |
| 435 buffer.write(expectedErrors.length); |
| 436 buffer.write(" errors:"); |
| 437 for (AnalysisError error in expectedErrors) { |
| 438 Source source = error.source; |
| 439 LineInfo lineInfo = _lineInfoMap[source]; |
| 440 buffer.writeln(); |
| 441 if (lineInfo == null) { |
| 442 int offset = error.offset; |
| 443 StringUtils.printf(buffer, " %s %s (%d..%d)", [ |
| 444 source == null ? "" : source.shortName, |
| 445 error.errorCode, |
| 446 offset, |
| 447 offset + error.length |
| 448 ]); |
| 449 } else { |
| 450 LineInfo_Location location = lineInfo.getLocation(error.offset); |
| 451 StringUtils.printf(buffer, " %s %s (%d, %d/%d)", [ |
| 452 source == null ? "" : source.shortName, |
| 453 error.errorCode, |
| 454 location.lineNumber, |
| 455 location.columnNumber, |
| 456 error.length |
| 457 ]); |
| 458 } |
| 459 } |
| 460 buffer.writeln(); |
| 461 buffer.write("found "); |
| 462 buffer.write(_errors.length); |
| 463 buffer.write(" errors:"); |
| 464 for (AnalysisError error in _errors) { |
| 465 Source source = error.source; |
| 466 LineInfo lineInfo = _lineInfoMap[source]; |
| 467 buffer.writeln(); |
| 468 if (lineInfo == null) { |
| 469 int offset = error.offset; |
| 470 StringUtils.printf(buffer, " %s %s (%d..%d): %s", [ |
| 471 source == null ? "" : source.shortName, |
| 472 error.errorCode, |
| 473 offset, |
| 474 offset + error.length, |
| 475 error.message |
| 476 ]); |
| 477 } else { |
| 478 LineInfo_Location location = lineInfo.getLocation(error.offset); |
| 479 StringUtils.printf(buffer, " %s %s (%d, %d/%d): %s", [ |
| 480 source == null ? "" : source.shortName, |
| 481 error.errorCode, |
| 482 location.lineNumber, |
| 483 location.columnNumber, |
| 484 error.length, |
| 485 error.message |
| 486 ]); |
| 487 } |
| 488 } |
| 489 fail(buffer.toString()); |
| 490 } |
| 491 |
| 492 /** |
| 493 * Search through the given list of errors for an error that is equal to the t
arget error. If one |
| 494 * is found, remove it from the list and return `true`, otherwise return `fals
e` |
| 495 * without modifying the list. |
| 496 * |
| 497 * @param errors the errors through which we are searching |
| 498 * @param targetError the error being searched for |
| 499 * @return `true` if the error is found and removed from the list |
| 500 */ |
| 501 bool _foundAndRemoved(List<AnalysisError> errors, AnalysisError targetError) { |
| 502 for (AnalysisError error in errors) { |
| 503 if (_equalErrors(error, targetError)) { |
| 504 errors.remove(error); |
| 505 return true; |
| 506 } |
| 507 } |
| 508 return false; |
| 509 } |
| 510 } |
| 511 |
| 512 /** |
| 513 * Instances of the class [TestLogger] implement a logger that can be used by |
| 514 * tests. |
| 515 */ |
| 516 class TestLogger implements Logger { |
| 517 /** |
| 518 * The number of error messages that were logged. |
| 519 */ |
| 520 int errorCount = 0; |
| 521 |
| 522 /** |
| 523 * The number of informational messages that were logged. |
| 524 */ |
| 525 int infoCount = 0; |
| 526 |
| 527 @override |
| 528 void logError(String message, [CaughtException exception]) { |
| 529 errorCount++; |
| 530 } |
| 531 |
| 532 @override |
| 533 void logError2(String message, Object exception) { |
| 534 errorCount++; |
| 535 } |
| 536 |
| 537 @override |
| 538 void logInformation(String message, [CaughtException exception]) { |
| 539 infoCount++; |
| 540 } |
| 541 |
| 542 @override |
| 543 void logInformation2(String message, Object exception) { |
| 544 infoCount++; |
| 545 } |
| 546 } |
| 547 |
| 548 class TestSource extends Source { |
| 549 String _name; |
| 550 String _contents; |
| 551 int _modificationStamp = 0; |
| 552 bool exists2 = true; |
| 553 |
| 554 /** |
| 555 * A flag indicating whether an exception should be generated when an attempt |
| 556 * is made to access the contents of this source. |
| 557 */ |
| 558 bool generateExceptionOnRead = false; |
| 559 |
| 560 @override |
| 561 int get modificationStamp => |
| 562 generateExceptionOnRead ? -1 : _modificationStamp; |
| 563 |
| 564 /** |
| 565 * The number of times that the contents of this source have been requested. |
| 566 */ |
| 567 int readCount = 0; |
| 568 |
| 569 TestSource([this._name = '/test.dart', this._contents]); |
| 570 |
| 571 TimestampedData<String> get contents { |
| 572 readCount++; |
| 573 if (generateExceptionOnRead) { |
| 574 String msg = "I/O Exception while getting the contents of " + _name; |
| 575 throw new Exception(msg); |
| 576 } |
| 577 return new TimestampedData<String>(0, _contents); |
| 578 } |
| 579 |
| 580 String get encoding { |
| 581 throw new UnsupportedOperationException(); |
| 582 } |
| 583 |
| 584 String get fullName { |
| 585 return _name; |
| 586 } |
| 587 |
| 588 int get hashCode => 0; |
| 589 bool get isInSystemLibrary { |
| 590 return false; |
| 591 } |
| 592 |
| 593 String get shortName { |
| 594 return _name; |
| 595 } |
| 596 |
| 597 Uri get uri { |
| 598 throw new UnsupportedOperationException(); |
| 599 } |
| 600 |
| 601 UriKind get uriKind { |
| 602 throw new UnsupportedOperationException(); |
| 603 } |
| 604 |
| 605 bool operator ==(Object other) { |
| 606 if (other is TestSource) { |
| 607 return other._name == _name; |
| 608 } |
| 609 return false; |
| 610 } |
| 611 |
| 612 bool exists() => exists2; |
| 613 void getContentsToReceiver(Source_ContentReceiver receiver) { |
| 614 throw new UnsupportedOperationException(); |
| 615 } |
| 616 |
| 617 Source resolve(String uri) { |
| 618 throw new UnsupportedOperationException(); |
| 619 } |
| 620 |
| 621 Uri resolveRelativeUri(Uri uri) { |
| 622 return new Uri(scheme: 'file', path: _name).resolveUri(uri); |
| 623 } |
| 624 |
| 625 void setContents(String value) { |
| 626 generateExceptionOnRead = false; |
| 627 _modificationStamp = new DateTime.now().millisecondsSinceEpoch; |
| 628 _contents = value; |
| 629 } |
| 630 |
| 631 @override |
| 632 String toString() => '$_name'; |
| 633 } |
| 634 |
| 635 class TestSourceWithUri extends TestSource { |
| 636 final Uri uri; |
| 637 |
| 638 TestSourceWithUri(String path, this.uri, [String content]) |
| 639 : super(path, content); |
| 640 |
| 641 UriKind get uriKind { |
| 642 if (uri == null) { |
| 643 return UriKind.FILE_URI; |
| 644 } else if (uri.scheme == 'dart') { |
| 645 return UriKind.DART_URI; |
| 646 } else if (uri.scheme == 'package') { |
| 647 return UriKind.PACKAGE_URI; |
| 648 } |
| 649 return UriKind.FILE_URI; |
| 650 } |
| 651 |
| 652 bool operator ==(Object other) { |
| 653 if (other is TestSource) { |
| 654 return other.uri == uri; |
| 655 } |
| 656 return false; |
| 657 } |
| 658 |
| 659 Uri resolveRelativeUri(Uri uri) { |
| 660 return this.uri.resolveUri(uri); |
| 661 } |
| 662 } |
OLD | NEW |