| 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 code_transformer.src.resolver_impl; | |
| 6 | |
| 7 import 'dart:async'; | |
| 8 import 'package:analyzer/analyzer.dart' show parseDirectives; | |
| 9 import 'package:analyzer/src/generated/ast.dart' hide ConstantEvaluator; | |
| 10 import 'package:analyzer/src/generated/constant.dart' | |
| 11 show ConstantEvaluator, EvaluationResult; | |
| 12 import 'package:analyzer/src/generated/element.dart'; | |
| 13 import 'package:analyzer/src/generated/engine.dart'; | |
| 14 import 'package:analyzer/src/generated/sdk.dart' show DartSdk; | |
| 15 import 'package:analyzer/src/generated/source.dart'; | |
| 16 import 'package:barback/barback.dart'; | |
| 17 import 'package:code_transformers/assets.dart'; | |
| 18 import 'package:path/path.dart' as native_path; | |
| 19 import 'package:source_maps/refactor.dart'; | |
| 20 import 'package:source_span/source_span.dart'; | |
| 21 | |
| 22 import 'resolver.dart'; | |
| 23 import 'dart_sdk.dart' show UriAnnotatedSource; | |
| 24 | |
| 25 // We should always be using url paths here since it's always Dart/pub code. | |
| 26 final path = native_path.url; | |
| 27 | |
| 28 /// Resolves and updates an AST based on Barback-based assets. | |
| 29 /// | |
| 30 /// This also provides a handful of useful APIs for traversing and working | |
| 31 /// with the resolved AST. | |
| 32 class ResolverImpl implements Resolver { | |
| 33 /// Cache of all asset sources currently referenced. | |
| 34 final Map<AssetId, _AssetBasedSource> sources = <AssetId, _AssetBasedSource>{ | |
| 35 }; | |
| 36 | |
| 37 final InternalAnalysisContext _context = | |
| 38 AnalysisEngine.instance.createAnalysisContext(); | |
| 39 | |
| 40 /// Transform for which this is currently updating, or null when not updating. | |
| 41 Transform _currentTransform; | |
| 42 | |
| 43 /// The currently resolved entry libraries, or null if nothing is resolved. | |
| 44 List<LibraryElement> _entryLibraries; | |
| 45 Set<LibraryElement> _libraries; | |
| 46 | |
| 47 /// Future indicating when this resolver is done in the current phase. | |
| 48 Future _lastPhaseComplete = new Future.value(); | |
| 49 | |
| 50 /// Completer for wrapping up the current phase. | |
| 51 Completer _currentPhaseComplete; | |
| 52 | |
| 53 /// Creates a resolver with a given [sdk] implementation for resolving | |
| 54 /// `dart:*` imports. | |
| 55 ResolverImpl(DartSdk sdk, DartUriResolver dartUriResolver, | |
| 56 {AnalysisOptions options}) { | |
| 57 if (options == null) { | |
| 58 options = new AnalysisOptionsImpl() | |
| 59 ..cacheSize = 256 // # of sources to cache ASTs for. | |
| 60 ..preserveComments = false | |
| 61 ..analyzeFunctionBodies = true; | |
| 62 } | |
| 63 _context.analysisOptions = options; | |
| 64 sdk.context.analysisOptions = options; | |
| 65 _context.sourceFactory = | |
| 66 new SourceFactory([dartUriResolver, new _AssetUriResolver(this)]); | |
| 67 } | |
| 68 | |
| 69 LibraryElement getLibrary(AssetId assetId) { | |
| 70 var source = sources[assetId]; | |
| 71 return source == null ? null : _context.computeLibraryElement(source); | |
| 72 } | |
| 73 | |
| 74 Future<Resolver> resolve(Transform transform, [List<AssetId> entryPoints]) { | |
| 75 // Can only have one resolve in progress at a time, so chain the current | |
| 76 // resolution to be after the last one. | |
| 77 var phaseComplete = new Completer(); | |
| 78 var future = _lastPhaseComplete.whenComplete(() { | |
| 79 _currentPhaseComplete = phaseComplete; | |
| 80 return _performResolve(transform, | |
| 81 entryPoints == null ? [transform.primaryInput.id] : entryPoints); | |
| 82 }).then((_) => this); | |
| 83 // Advance the lastPhaseComplete to be done when this phase is all done. | |
| 84 _lastPhaseComplete = phaseComplete.future; | |
| 85 return future; | |
| 86 } | |
| 87 | |
| 88 void release() { | |
| 89 if (_currentPhaseComplete == null) { | |
| 90 throw new StateError('Releasing without current lock.'); | |
| 91 } | |
| 92 _currentPhaseComplete.complete(null); | |
| 93 _currentPhaseComplete = null; | |
| 94 | |
| 95 // Clear out libraries since they should not be referenced after release. | |
| 96 _entryLibraries = null; | |
| 97 _libraries = null; | |
| 98 _currentTransform = null; | |
| 99 } | |
| 100 | |
| 101 Future _performResolve(Transform transform, List<AssetId> entryPoints) { | |
| 102 if (_currentTransform != null) { | |
| 103 throw new StateError('Cannot be accessed by concurrent transforms'); | |
| 104 } | |
| 105 _currentTransform = transform; | |
| 106 | |
| 107 // Basic approach is to start at the first file, update it's contents | |
| 108 // and see if it changed, then walk all files accessed by it. | |
| 109 var visited = new Set<AssetId>(); | |
| 110 var visiting = new FutureGroup(); | |
| 111 var toUpdate = []; | |
| 112 | |
| 113 void processAsset(AssetId assetId) { | |
| 114 visited.add(assetId); | |
| 115 | |
| 116 visiting.add(transform.readInputAsString(assetId).then((contents) { | |
| 117 var source = sources[assetId]; | |
| 118 if (source == null) { | |
| 119 source = new _AssetBasedSource(assetId, this); | |
| 120 sources[assetId] = source; | |
| 121 } | |
| 122 source.updateDependencies(contents); | |
| 123 toUpdate.add(new _PendingUpdate(source, contents)); | |
| 124 source.dependentAssets | |
| 125 .where((id) => !visited.contains(id)) | |
| 126 .forEach(processAsset); | |
| 127 }, onError: (e) { | |
| 128 var source = sources[assetId]; | |
| 129 if (source != null && source.exists()) { | |
| 130 _context.applyChanges(new ChangeSet()..removedSource(source)); | |
| 131 sources[assetId].updateContents(null); | |
| 132 } | |
| 133 })); | |
| 134 } | |
| 135 entryPoints.forEach(processAsset); | |
| 136 | |
| 137 // Once we have all asset sources updated with the new contents then | |
| 138 // resolve everything. | |
| 139 return visiting.future.then((_) { | |
| 140 var changeSet = new ChangeSet(); | |
| 141 toUpdate.forEach((pending) => pending.apply(changeSet)); | |
| 142 var unreachableAssets = | |
| 143 sources.keys.toSet().difference(visited).map((id) => sources[id]); | |
| 144 for (var unreachable in unreachableAssets) { | |
| 145 changeSet.removedSource(unreachable); | |
| 146 unreachable.updateContents(null); | |
| 147 sources.remove(unreachable.assetId); | |
| 148 } | |
| 149 | |
| 150 // Update the analyzer context with the latest sources | |
| 151 _context.applyChanges(changeSet); | |
| 152 // Force resolve each entry point (the getter will ensure the library is | |
| 153 // computed first). | |
| 154 _entryLibraries = entryPoints.map((id) { | |
| 155 var source = sources[id]; | |
| 156 if (source == null) return null; | |
| 157 return _context.computeLibraryElement(source); | |
| 158 }).toList(); | |
| 159 }); | |
| 160 } | |
| 161 | |
| 162 Iterable<LibraryElement> get libraries { | |
| 163 if (_libraries == null) { | |
| 164 // Note: we don't use `lib.visibleLibraries` because that excludes the | |
| 165 // exports seen in the entry libraries. | |
| 166 _libraries = new Set<LibraryElement>(); | |
| 167 _entryLibraries.forEach(_collectLibraries); | |
| 168 } | |
| 169 return _libraries; | |
| 170 } | |
| 171 | |
| 172 void _collectLibraries(LibraryElement lib) { | |
| 173 if (lib == null || _libraries.contains(lib)) return; | |
| 174 _libraries.add(lib); | |
| 175 lib.importedLibraries.forEach(_collectLibraries); | |
| 176 lib.exportedLibraries.forEach(_collectLibraries); | |
| 177 } | |
| 178 | |
| 179 LibraryElement getLibraryByName(String libraryName) => | |
| 180 libraries.firstWhere((l) => l.name == libraryName, orElse: () => null); | |
| 181 | |
| 182 LibraryElement getLibraryByUri(Uri uri) => | |
| 183 libraries.firstWhere((l) => getImportUri(l) == uri, orElse: () => null); | |
| 184 | |
| 185 ClassElement getType(String typeName) { | |
| 186 var dotIndex = typeName.lastIndexOf('.'); | |
| 187 var libraryName = dotIndex == -1 ? '' : typeName.substring(0, dotIndex); | |
| 188 | |
| 189 var className = | |
| 190 dotIndex == -1 ? typeName : typeName.substring(dotIndex + 1); | |
| 191 | |
| 192 for (var lib in libraries.where((l) => l.name == libraryName)) { | |
| 193 var type = lib.getType(className); | |
| 194 if (type != null) return type; | |
| 195 } | |
| 196 return null; | |
| 197 } | |
| 198 | |
| 199 Element getLibraryVariable(String variableName) { | |
| 200 var dotIndex = variableName.lastIndexOf('.'); | |
| 201 var libraryName = dotIndex == -1 ? '' : variableName.substring(0, dotIndex); | |
| 202 | |
| 203 var name = | |
| 204 dotIndex == -1 ? variableName : variableName.substring(dotIndex + 1); | |
| 205 | |
| 206 return libraries | |
| 207 .where((lib) => lib.name == libraryName) | |
| 208 .expand((lib) => lib.units) | |
| 209 .expand((unit) => unit.topLevelVariables) | |
| 210 .firstWhere((variable) => variable.name == name, orElse: () => null); | |
| 211 } | |
| 212 | |
| 213 Element getLibraryFunction(String fnName) { | |
| 214 var dotIndex = fnName.lastIndexOf('.'); | |
| 215 var libraryName = dotIndex == -1 ? '' : fnName.substring(0, dotIndex); | |
| 216 | |
| 217 var name = dotIndex == -1 ? fnName : fnName.substring(dotIndex + 1); | |
| 218 | |
| 219 return libraries | |
| 220 .where((lib) => lib.name == libraryName) | |
| 221 .expand((lib) => lib.units) | |
| 222 .expand((unit) => unit.functions) | |
| 223 .firstWhere((fn) => fn.name == name, orElse: () => null); | |
| 224 } | |
| 225 | |
| 226 EvaluationResult evaluateConstant( | |
| 227 LibraryElement library, Expression expression) { | |
| 228 return new ConstantEvaluator(library.source, _context.typeProvider) | |
| 229 .evaluate(expression); | |
| 230 } | |
| 231 | |
| 232 Uri getImportUri(LibraryElement lib, {AssetId from}) => | |
| 233 _getSourceUri(lib, from: from); | |
| 234 | |
| 235 /// Similar to getImportUri but will get the part URI for parts rather than | |
| 236 /// the library URI. | |
| 237 Uri _getSourceUri(Element element, {AssetId from}) { | |
| 238 var source = element.source; | |
| 239 if (source is _AssetBasedSource) { | |
| 240 var uriString = assetIdToUri(source.assetId, from: from); | |
| 241 return uriString != null ? Uri.parse(uriString) : null; | |
| 242 } else if (source is UriAnnotatedSource) { | |
| 243 return source.uri; | |
| 244 } | |
| 245 // Should not be able to encounter any other source types. | |
| 246 throw new StateError('Unable to resolve URI for ${source.runtimeType}'); | |
| 247 } | |
| 248 | |
| 249 AssetId getSourceAssetId(Element element) { | |
| 250 var source = element.source; | |
| 251 if (source is _AssetBasedSource) return source.assetId; | |
| 252 return null; | |
| 253 } | |
| 254 | |
| 255 SourceSpan getSourceSpan(Element element) { | |
| 256 var sourceFile = getSourceFile(element); | |
| 257 if (sourceFile == null) return null; | |
| 258 return sourceFile.span(element.node.offset, element.node.end); | |
| 259 } | |
| 260 | |
| 261 TextEditTransaction createTextEditTransaction(Element element) { | |
| 262 if (element.source is! _AssetBasedSource) return null; | |
| 263 | |
| 264 // Cannot edit unless there is an active transformer. | |
| 265 if (_currentTransform == null) return null; | |
| 266 | |
| 267 _AssetBasedSource source = element.source; | |
| 268 // Cannot modify assets in other packages. | |
| 269 if (source.assetId.package != _currentTransform.primaryInput.id.package) { | |
| 270 return null; | |
| 271 } | |
| 272 | |
| 273 var sourceFile = getSourceFile(element); | |
| 274 if (sourceFile == null) return null; | |
| 275 | |
| 276 return new TextEditTransaction(source.rawContents, sourceFile); | |
| 277 } | |
| 278 | |
| 279 /// Gets the SourceFile for the source of the element. | |
| 280 SourceFile getSourceFile(Element element) { | |
| 281 var assetId = getSourceAssetId(element); | |
| 282 if (assetId == null) return null; | |
| 283 | |
| 284 var importUri = _getSourceUri(element); | |
| 285 var spanPath = importUri != null ? importUri.toString() : assetId.path; | |
| 286 return new SourceFile(sources[assetId].rawContents, url: spanPath); | |
| 287 } | |
| 288 } | |
| 289 | |
| 290 /// Implementation of Analyzer's Source for Barback based assets. | |
| 291 class _AssetBasedSource extends Source { | |
| 292 | |
| 293 /// Asset ID where this source can be found. | |
| 294 final AssetId assetId; | |
| 295 | |
| 296 /// The resolver this is being used in. | |
| 297 final ResolverImpl _resolver; | |
| 298 | |
| 299 /// Cache of dependent asset IDs, to avoid re-parsing the AST. | |
| 300 Iterable<AssetId> _dependentAssets; | |
| 301 | |
| 302 /// The current revision of the file, incremented only when file changes. | |
| 303 int _revision = 0; | |
| 304 | |
| 305 /// The file contents. | |
| 306 String _contents; | |
| 307 | |
| 308 _AssetBasedSource(this.assetId, this._resolver); | |
| 309 | |
| 310 /// Update the dependencies of this source. This parses [contents] but avoids | |
| 311 /// any analyzer resolution. | |
| 312 void updateDependencies(String contents) { | |
| 313 if (contents == _contents) return; | |
| 314 var unit = parseDirectives(contents, suppressErrors: true); | |
| 315 _dependentAssets = unit.directives | |
| 316 .where((d) => (d is ImportDirective || | |
| 317 d is PartDirective || | |
| 318 d is ExportDirective)) | |
| 319 .map((d) => _resolve( | |
| 320 assetId, d.uri.stringValue, _logger, _getSpan(d, contents))) | |
| 321 .where((id) => id != null) | |
| 322 .toSet(); | |
| 323 } | |
| 324 | |
| 325 /// Update the contents of this file with [contents]. | |
| 326 /// | |
| 327 /// Returns true if the contents of this asset have changed. | |
| 328 bool updateContents(String contents) { | |
| 329 if (contents == _contents) return false; | |
| 330 _contents = contents; | |
| 331 ++_revision; | |
| 332 return true; | |
| 333 } | |
| 334 | |
| 335 /// Contents of the file. | |
| 336 TimestampedData<String> get contents { | |
| 337 if (!exists()) throw new StateError('$assetId does not exist'); | |
| 338 | |
| 339 return new TimestampedData<String>(modificationStamp, _contents); | |
| 340 } | |
| 341 | |
| 342 /// Contents of the file. | |
| 343 String get rawContents => _contents; | |
| 344 | |
| 345 Uri get uri => Uri.parse('asset:${assetId.package}/${assetId.path}'); | |
| 346 | |
| 347 /// Logger for the current transform. | |
| 348 /// | |
| 349 /// Only valid while the resolver is updating assets. | |
| 350 TransformLogger get _logger => _resolver._currentTransform.logger; | |
| 351 | |
| 352 /// Gets all imports/parts/exports which resolve to assets (non-Dart files). | |
| 353 Iterable<AssetId> get dependentAssets => _dependentAssets; | |
| 354 | |
| 355 bool exists() => _contents != null; | |
| 356 | |
| 357 bool operator ==(Object other) => | |
| 358 other is _AssetBasedSource && assetId == other.assetId; | |
| 359 | |
| 360 int get hashCode => assetId.hashCode; | |
| 361 | |
| 362 void getContentsToReceiver(Source_ContentReceiver receiver) { | |
| 363 receiver.accept(rawContents, modificationStamp); | |
| 364 } | |
| 365 | |
| 366 String get encoding => | |
| 367 "${uriKind.encoding}${assetId.package}/${assetId.path}"; | |
| 368 | |
| 369 String get fullName => assetId.toString(); | |
| 370 | |
| 371 int get modificationStamp => _revision; | |
| 372 | |
| 373 String get shortName => path.basename(assetId.path); | |
| 374 | |
| 375 UriKind get uriKind { | |
| 376 if (assetId.path.startsWith('lib/')) return UriKind.PACKAGE_URI; | |
| 377 return UriKind.FILE_URI; | |
| 378 } | |
| 379 | |
| 380 bool get isInSystemLibrary => false; | |
| 381 | |
| 382 Source resolveRelative(Uri relativeUri) { | |
| 383 var id = _resolve(assetId, relativeUri.toString(), _logger, null); | |
| 384 if (id == null) return null; | |
| 385 | |
| 386 // The entire AST should have been parsed and loaded at this point. | |
| 387 var source = _resolver.sources[id]; | |
| 388 if (source == null) { | |
| 389 _logger.error('Could not load asset $id'); | |
| 390 } | |
| 391 return source; | |
| 392 } | |
| 393 | |
| 394 Uri resolveRelativeUri(Uri relativeUri) { | |
| 395 var id = _resolve(assetId, relativeUri.toString(), _logger, null); | |
| 396 if (id == null) return uri.resolveUri(relativeUri); | |
| 397 | |
| 398 // The entire AST should have been parsed and loaded at this point. | |
| 399 var source = _resolver.sources[id]; | |
| 400 if (source == null) { | |
| 401 _logger.error('Could not load asset $id'); | |
| 402 } | |
| 403 return source.uri; | |
| 404 } | |
| 405 | |
| 406 /// For logging errors. | |
| 407 SourceSpan _getSpan(AstNode node, [String contents]) => | |
| 408 _getSourceFile(contents).span(node.offset, node.end); | |
| 409 /// For logging errors. | |
| 410 SourceFile _getSourceFile([String contents]) { | |
| 411 var uri = assetIdToUri(assetId); | |
| 412 var path = uri != null ? uri : assetId.path; | |
| 413 return new SourceFile(contents != null ? contents : rawContents, url: path); | |
| 414 } | |
| 415 } | |
| 416 | |
| 417 /// Implementation of Analyzer's UriResolver for Barback based assets. | |
| 418 class _AssetUriResolver implements UriResolver { | |
| 419 final ResolverImpl _resolver; | |
| 420 _AssetUriResolver(this._resolver); | |
| 421 | |
| 422 Source resolveAbsolute(Uri uri) { | |
| 423 assert(uri.scheme != 'dart'); | |
| 424 var assetId; | |
| 425 if (uri.scheme == 'asset') { | |
| 426 var parts = path.split(uri.path); | |
| 427 assetId = new AssetId(parts[0], path.joinAll(parts.skip(1))); | |
| 428 } else { | |
| 429 assetId = _resolve(null, uri.toString(), logger, null); | |
| 430 if (assetId == null) { | |
| 431 logger.error('Unable to resolve asset ID for "$uri"'); | |
| 432 return null; | |
| 433 } | |
| 434 } | |
| 435 var source = _resolver.sources[assetId]; | |
| 436 // Analyzer expects that sources which are referenced but do not exist yet | |
| 437 // still exist, so just make an empty source. | |
| 438 if (source == null) { | |
| 439 source = new _AssetBasedSource(assetId, _resolver); | |
| 440 _resolver.sources[assetId] = source; | |
| 441 } | |
| 442 return source; | |
| 443 } | |
| 444 | |
| 445 Source fromEncoding(UriKind kind, Uri uri) => | |
| 446 throw new UnsupportedError('fromEncoding is not supported'); | |
| 447 | |
| 448 Uri restoreAbsolute(Source source) => | |
| 449 throw new UnsupportedError('restoreAbsolute is not supported'); | |
| 450 | |
| 451 TransformLogger get logger => _resolver._currentTransform.logger; | |
| 452 } | |
| 453 | |
| 454 /// Get an asset ID for a URL relative to another source asset. | |
| 455 AssetId _resolve( | |
| 456 AssetId source, String url, TransformLogger logger, SourceSpan span) { | |
| 457 if (url == null || url == '') return null; | |
| 458 var uri = Uri.parse(url); | |
| 459 | |
| 460 // Workaround for dartbug.com/17156- pub transforms package: imports from | |
| 461 // files of the transformers package to have absolute /packages/ URIs. | |
| 462 if (uri.scheme == '' && | |
| 463 path.isAbsolute(url) && | |
| 464 uri.pathSegments[0] == 'packages') { | |
| 465 uri = Uri.parse('package:${uri.pathSegments.skip(1).join(path.separator)}'); | |
| 466 } | |
| 467 | |
| 468 if (uri.scheme == 'package') { | |
| 469 var segments = new List.from(uri.pathSegments); | |
| 470 var package = segments[0]; | |
| 471 segments[0] = 'lib'; | |
| 472 return new AssetId(package, segments.join(path.separator)); | |
| 473 } | |
| 474 // Dart SDK libraries do not have assets. | |
| 475 if (uri.scheme == 'dart') return null; | |
| 476 | |
| 477 return uriToAssetId(source, url, logger, span); | |
| 478 } | |
| 479 | |
| 480 /// A completer that waits until all added [Future]s complete. | |
| 481 // TODO(blois): Copied from quiver. Remove from here when it gets | |
| 482 // added to dart:core. (See #6626.) | |
| 483 class FutureGroup<E> { | |
| 484 static const _FINISHED = -1; | |
| 485 | |
| 486 int _pending = 0; | |
| 487 Future _failedTask; | |
| 488 final Completer<List> _completer = new Completer<List>(); | |
| 489 final List results = []; | |
| 490 | |
| 491 /** Gets the task that failed, if any. */ | |
| 492 Future get failedTask => _failedTask; | |
| 493 | |
| 494 /** | |
| 495 * Wait for [task] to complete. | |
| 496 * | |
| 497 * If this group has already been marked as completed, a [StateError] will be | |
| 498 * thrown. | |
| 499 * | |
| 500 * If this group has a [failedTask], new tasks will be ignored, because the | |
| 501 * error has already been signaled. | |
| 502 */ | |
| 503 void add(Future task) { | |
| 504 if (_failedTask != null) return; | |
| 505 if (_pending == _FINISHED) throw new StateError("Future already completed"); | |
| 506 | |
| 507 _pending++; | |
| 508 var i = results.length; | |
| 509 results.add(null); | |
| 510 task.then((res) { | |
| 511 results[i] = res; | |
| 512 if (_failedTask != null) return; | |
| 513 _pending--; | |
| 514 if (_pending == 0) { | |
| 515 _pending = _FINISHED; | |
| 516 _completer.complete(results); | |
| 517 } | |
| 518 }, onError: (e, s) { | |
| 519 if (_failedTask != null) return; | |
| 520 _failedTask = task; | |
| 521 _completer.completeError(e, s); | |
| 522 }); | |
| 523 } | |
| 524 | |
| 525 /** | |
| 526 * A Future that completes with a List of the values from all the added | |
| 527 * tasks, when they have all completed. | |
| 528 * | |
| 529 * If any task fails, this Future will receive the error. Only the first | |
| 530 * error will be sent to the Future. | |
| 531 */ | |
| 532 Future<List<E>> get future => _completer.future; | |
| 533 } | |
| 534 | |
| 535 /// A pending update to notify the resolver that a [Source] has been added or | |
| 536 /// changed. This is used by the `_performResolve` algorithm above to apply all | |
| 537 /// changes after it first discovers the transitive closure of files that are | |
| 538 /// reachable from the sources. | |
| 539 class _PendingUpdate { | |
| 540 _AssetBasedSource source; | |
| 541 String content; | |
| 542 | |
| 543 _PendingUpdate(this.source, this.content); | |
| 544 | |
| 545 void apply(ChangeSet changeSet) { | |
| 546 if (!source.updateContents(content)) return; | |
| 547 if (source._revision == 1 && source._contents != null) { | |
| 548 changeSet.addedSource(source); | |
| 549 } else { | |
| 550 changeSet.changedSource(source); | |
| 551 } | |
| 552 } | |
| 553 } | |
| OLD | NEW |