Index: observatory_pub_packages/code_transformers/src/resolver_impl.dart |
=================================================================== |
--- observatory_pub_packages/code_transformers/src/resolver_impl.dart (revision 0) |
+++ observatory_pub_packages/code_transformers/src/resolver_impl.dart (working copy) |
@@ -0,0 +1,569 @@ |
+// Copyright (c) 2014, the Dart project authors. Please see the AUTHORS file |
+// for details. All rights reserved. Use of this source code is governed by a |
+// BSD-style license that can be found in the LICENSE file. |
+ |
+library code_transformer.src.resolver_impl; |
+ |
+import 'dart:async'; |
+import 'package:analyzer/analyzer.dart' show parseDirectives; |
+import 'package:analyzer/src/generated/ast.dart' hide ConstantEvaluator; |
+import 'package:analyzer/src/generated/constant.dart' show ConstantEvaluator, |
+ EvaluationResult; |
+import 'package:analyzer/src/generated/element.dart'; |
+import 'package:analyzer/src/generated/engine.dart'; |
+import 'package:analyzer/src/generated/sdk.dart' show DartSdk; |
+import 'package:analyzer/src/generated/source.dart'; |
+import 'package:barback/barback.dart'; |
+import 'package:code_transformers/assets.dart'; |
+import 'package:path/path.dart' as native_path; |
+import 'package:source_maps/refactor.dart'; |
+import 'package:source_span/source_span.dart'; |
+ |
+import 'resolver.dart'; |
+import 'dart_sdk.dart' show UriAnnotatedSource; |
+ |
+// We should always be using url paths here since it's always Dart/pub code. |
+final path = native_path.url; |
+ |
+/// Resolves and updates an AST based on Barback-based assets. |
+/// |
+/// This also provides a handful of useful APIs for traversing and working |
+/// with the resolved AST. |
+class ResolverImpl implements Resolver { |
+ /// Cache of all asset sources currently referenced. |
+ final Map<AssetId, _AssetBasedSource> sources = |
+ <AssetId, _AssetBasedSource>{}; |
+ |
+ final InternalAnalysisContext _context = |
+ AnalysisEngine.instance.createAnalysisContext(); |
+ |
+ /// Transform for which this is currently updating, or null when not updating. |
+ Transform _currentTransform; |
+ |
+ /// The currently resolved entry libraries, or null if nothing is resolved. |
+ List<LibraryElement> _entryLibraries; |
+ Set<LibraryElement> _libraries; |
+ |
+ /// Future indicating when this resolver is done in the current phase. |
+ Future _lastPhaseComplete = new Future.value(); |
+ |
+ /// Completer for wrapping up the current phase. |
+ Completer _currentPhaseComplete; |
+ |
+ /// Creates a resolver with a given [sdk] implementation for resolving |
+ /// `dart:*` imports. |
+ ResolverImpl(DartSdk sdk, DartUriResolver dartUriResolver, |
+ {AnalysisOptions options}) { |
+ if (options == null) { |
+ options = new AnalysisOptionsImpl() |
+ ..cacheSize = 256 // # of sources to cache ASTs for. |
+ ..preserveComments = false |
+ ..analyzeFunctionBodies = true; |
+ } |
+ _context.analysisOptions = options; |
+ sdk.context.analysisOptions = options; |
+ _context.sourceFactory = new SourceFactory([dartUriResolver, |
+ new _AssetUriResolver(this)]); |
+ } |
+ |
+ LibraryElement getLibrary(AssetId assetId) { |
+ var source = sources[assetId]; |
+ return source == null ? null : _context.computeLibraryElement(source); |
+ } |
+ |
+ Future<Resolver> resolve(Transform transform, [List<AssetId> entryPoints]) { |
+ // Can only have one resolve in progress at a time, so chain the current |
+ // resolution to be after the last one. |
+ var phaseComplete = new Completer(); |
+ var future = _lastPhaseComplete.whenComplete(() { |
+ _currentPhaseComplete = phaseComplete; |
+ return _performResolve(transform, |
+ entryPoints == null ? [transform.primaryInput.id] : entryPoints); |
+ }).then((_) => this); |
+ // Advance the lastPhaseComplete to be done when this phase is all done. |
+ _lastPhaseComplete = phaseComplete.future; |
+ return future; |
+ } |
+ |
+ void release() { |
+ if (_currentPhaseComplete == null) { |
+ throw new StateError('Releasing without current lock.'); |
+ } |
+ _currentPhaseComplete.complete(null); |
+ _currentPhaseComplete = null; |
+ |
+ // Clear out libraries since they should not be referenced after release. |
+ _entryLibraries = null; |
+ _libraries = null; |
+ _currentTransform = null; |
+ } |
+ |
+ Future _performResolve(Transform transform, List<AssetId> entryPoints) { |
+ if (_currentTransform != null) { |
+ throw new StateError('Cannot be accessed by concurrent transforms'); |
+ } |
+ _currentTransform = transform; |
+ |
+ // Basic approach is to start at the first file, update it's contents |
+ // and see if it changed, then walk all files accessed by it. |
+ var visited = new Set<AssetId>(); |
+ var visiting = new FutureGroup(); |
+ var toUpdate = []; |
+ |
+ void processAsset(AssetId assetId) { |
+ visited.add(assetId); |
+ |
+ visiting.add(transform.readInputAsString(assetId).then((contents) { |
+ var source = sources[assetId]; |
+ if (source == null) { |
+ source = new _AssetBasedSource(assetId, this); |
+ sources[assetId] = source; |
+ } |
+ source.updateDependencies(contents); |
+ toUpdate.add(new _PendingUpdate(source, contents)); |
+ source.dependentAssets.where((id) => !visited.contains(id)) |
+ .forEach(processAsset); |
+ }, onError: (e) { |
+ var source = sources[assetId]; |
+ if (source != null && source.exists()) { |
+ _context.applyChanges( |
+ new ChangeSet()..removedSource(source)); |
+ sources[assetId].updateContents(null); |
+ } |
+ })); |
+ } |
+ entryPoints.forEach(processAsset); |
+ |
+ // Once we have all asset sources updated with the new contents then |
+ // resolve everything. |
+ return visiting.future.then((_) { |
+ var changeSet = new ChangeSet(); |
+ toUpdate.forEach((pending) => pending.apply(changeSet)); |
+ var unreachableAssets = sources.keys.toSet() |
+ .difference(visited) |
+ .map((id) => sources[id]); |
+ for (var unreachable in unreachableAssets) { |
+ changeSet.removedSource(unreachable); |
+ unreachable.updateContents(null); |
+ sources.remove(unreachable.assetId); |
+ } |
+ |
+ // Update the analyzer context with the latest sources |
+ _context.applyChanges(changeSet); |
+ // Force resolve each entry point (the getter will ensure the library is |
+ // computed first). |
+ _entryLibraries = entryPoints.map((id) { |
+ var source = sources[id]; |
+ if (source == null) return null; |
+ return _context.computeLibraryElement(source); |
+ }).toList(); |
+ }); |
+ } |
+ |
+ Iterable<LibraryElement> get libraries { |
+ if (_libraries == null) { |
+ // Note: we don't use `lib.visibleLibraries` because that excludes the |
+ // exports seen in the entry libraries. |
+ _libraries = new Set<LibraryElement>(); |
+ _entryLibraries.forEach(_collectLibraries); |
+ } |
+ return _libraries; |
+ } |
+ |
+ void _collectLibraries(LibraryElement lib) { |
+ if (lib == null || _libraries.contains(lib)) return; |
+ _libraries.add(lib); |
+ lib.importedLibraries.forEach(_collectLibraries); |
+ lib.exportedLibraries.forEach(_collectLibraries); |
+ } |
+ |
+ LibraryElement getLibraryByName(String libraryName) => |
+ libraries.firstWhere((l) => l.name == libraryName, orElse: () => null); |
+ |
+ LibraryElement getLibraryByUri(Uri uri) => |
+ libraries.firstWhere((l) => getImportUri(l) == uri, orElse: () => null); |
+ |
+ ClassElement getType(String typeName) { |
+ var dotIndex = typeName.lastIndexOf('.'); |
+ var libraryName = dotIndex == -1 ? '' : typeName.substring(0, dotIndex); |
+ |
+ var className = dotIndex == -1 ? |
+ typeName : typeName.substring(dotIndex + 1); |
+ |
+ for (var lib in libraries.where((l) => l.name == libraryName)) { |
+ var type = lib.getType(className); |
+ if (type != null) return type; |
+ } |
+ return null; |
+ } |
+ |
+ Element getLibraryVariable(String variableName) { |
+ var dotIndex = variableName.lastIndexOf('.'); |
+ var libraryName = dotIndex == -1 ? '' : variableName.substring(0, dotIndex); |
+ |
+ var name = dotIndex == -1 ? |
+ variableName : variableName.substring(dotIndex + 1); |
+ |
+ return libraries.where((lib) => lib.name == libraryName) |
+ .expand((lib) => lib.units) |
+ .expand((unit) => unit.topLevelVariables) |
+ .firstWhere((variable) => variable.name == name, |
+ orElse: () => null); |
+ } |
+ |
+ Element getLibraryFunction(String fnName) { |
+ var dotIndex = fnName.lastIndexOf('.'); |
+ var libraryName = dotIndex == -1 ? '' : fnName.substring(0, dotIndex); |
+ |
+ var name = dotIndex == -1 ? |
+ fnName : fnName.substring(dotIndex + 1); |
+ |
+ return libraries.where((lib) => lib.name == libraryName) |
+ .expand((lib) => lib.units) |
+ .expand((unit) => unit.functions) |
+ .firstWhere((fn) => fn.name == name, |
+ orElse: () => null); |
+ } |
+ |
+ EvaluationResult evaluateConstant( |
+ LibraryElement library, Expression expression) { |
+ return new ConstantEvaluator(library.source, _context.typeProvider) |
+ .evaluate(expression); |
+ } |
+ |
+ Uri getImportUri(LibraryElement lib, {AssetId from}) => |
+ _getSourceUri(lib, from: from); |
+ |
+ |
+ /// Similar to getImportUri but will get the part URI for parts rather than |
+ /// the library URI. |
+ Uri _getSourceUri(Element element, {AssetId from}) { |
+ var source = element.source; |
+ if (source is _AssetBasedSource) { |
+ return source.getSourceUri(from); |
+ } else if (source is UriAnnotatedSource) { |
+ return source.uri; |
+ } |
+ // Should not be able to encounter any other source types. |
+ throw new StateError('Unable to resolve URI for ${source.runtimeType}'); |
+ } |
+ |
+ AssetId getSourceAssetId(Element element) { |
+ var source = element.source; |
+ if (source is _AssetBasedSource) return source.assetId; |
+ return null; |
+ } |
+ |
+ SourceSpan getSourceSpan(Element element) { |
+ var sourceFile = getSourceFile(element); |
+ if (sourceFile == null) return null; |
+ return sourceFile.span(element.node.offset, element.node.end); |
+ } |
+ |
+ TextEditTransaction createTextEditTransaction(Element element) { |
+ if (element.source is! _AssetBasedSource) return null; |
+ |
+ // Cannot edit unless there is an active transformer. |
+ if (_currentTransform == null) return null; |
+ |
+ _AssetBasedSource source = element.source; |
+ // Cannot modify assets in other packages. |
+ if (source.assetId.package != _currentTransform.primaryInput.id.package) { |
+ return null; |
+ } |
+ |
+ var sourceFile = getSourceFile(element); |
+ if (sourceFile == null) return null; |
+ |
+ return new TextEditTransaction(source.rawContents, sourceFile); |
+ } |
+ |
+ /// Gets the SourceFile for the source of the element. |
+ SourceFile getSourceFile(Element element) { |
+ var assetId = getSourceAssetId(element); |
+ if (assetId == null) return null; |
+ |
+ var importUri = _getSourceUri(element); |
+ var spanPath = importUri != null ? importUri.toString() : assetId.path; |
+ return new SourceFile(sources[assetId].rawContents, url: spanPath); |
+ } |
+} |
+ |
+/// Implementation of Analyzer's Source for Barback based assets. |
+class _AssetBasedSource extends Source { |
+ |
+ /// Asset ID where this source can be found. |
+ final AssetId assetId; |
+ |
+ /// The resolver this is being used in. |
+ final ResolverImpl _resolver; |
+ |
+ /// Cache of dependent asset IDs, to avoid re-parsing the AST. |
+ Iterable<AssetId> _dependentAssets; |
+ |
+ /// The current revision of the file, incremented only when file changes. |
+ int _revision = 0; |
+ |
+ /// The file contents. |
+ String _contents; |
+ |
+ _AssetBasedSource(this.assetId, this._resolver); |
+ |
+ /// Update the dependencies of this source. This parses [contents] but avoids |
+ /// any analyzer resolution. |
+ void updateDependencies(String contents) { |
+ if (contents == _contents) return; |
+ var unit = parseDirectives(contents, suppressErrors: true); |
+ _dependentAssets = unit.directives |
+ .where((d) => (d is ImportDirective || d is PartDirective || |
+ d is ExportDirective)) |
+ .map((d) => _resolve(assetId, d.uri.stringValue, _logger, |
+ _getSpan(d, contents))) |
+ .where((id) => id != null).toSet(); |
+ } |
+ |
+ /// Update the contents of this file with [contents]. |
+ /// |
+ /// Returns true if the contents of this asset have changed. |
+ bool updateContents(String contents) { |
+ if (contents == _contents) return false; |
+ _contents = contents; |
+ ++_revision; |
+ return true; |
+ } |
+ |
+ /// Contents of the file. |
+ TimestampedData<String> get contents { |
+ if (!exists()) throw new StateError('$assetId does not exist'); |
+ |
+ return new TimestampedData<String>(modificationStamp, _contents); |
+ } |
+ |
+ /// Contents of the file. |
+ String get rawContents => _contents; |
+ |
+ Uri get uri => Uri.parse('asset:${assetId.package}/${assetId.path}'); |
+ |
+ /// Logger for the current transform. |
+ /// |
+ /// Only valid while the resolver is updating assets. |
+ TransformLogger get _logger => _resolver._currentTransform.logger; |
+ |
+ /// Gets all imports/parts/exports which resolve to assets (non-Dart files). |
+ Iterable<AssetId> get dependentAssets => _dependentAssets; |
+ |
+ bool exists() => _contents != null; |
+ |
+ bool operator ==(Object other) => |
+ other is _AssetBasedSource && assetId == other.assetId; |
+ |
+ int get hashCode => assetId.hashCode; |
+ |
+ void getContentsToReceiver(Source_ContentReceiver receiver) { |
+ receiver.accept(rawContents, modificationStamp); |
+ } |
+ |
+ String get encoding => |
+ "${uriKind.encoding}${assetId.package}/${assetId.path}"; |
+ |
+ String get fullName => assetId.toString(); |
+ |
+ int get modificationStamp => _revision; |
+ |
+ String get shortName => path.basename(assetId.path); |
+ |
+ UriKind get uriKind { |
+ if (assetId.path.startsWith('lib/')) return UriKind.PACKAGE_URI; |
+ return UriKind.FILE_URI; |
+ } |
+ |
+ bool get isInSystemLibrary => false; |
+ |
+ Source resolveRelative(Uri relativeUri) { |
+ var id = _resolve(assetId, relativeUri.toString(), _logger, null); |
+ if (id == null) return null; |
+ |
+ // The entire AST should have been parsed and loaded at this point. |
+ var source = _resolver.sources[id]; |
+ if (source == null) { |
+ _logger.error('Could not load asset $id'); |
+ } |
+ return source; |
+ } |
+ |
+ Uri resolveRelativeUri(Uri relativeUri) { |
+ var id = _resolve(assetId, relativeUri.toString(), _logger, null); |
+ if (id == null) return uri.resolveUri(relativeUri); |
+ |
+ // The entire AST should have been parsed and loaded at this point. |
+ var source = _resolver.sources[id]; |
+ if (source == null) { |
+ _logger.error('Could not load asset $id'); |
+ } |
+ return source.uri; |
+ } |
+ |
+ /// For logging errors. |
+ SourceSpan _getSpan(AstNode node, [String contents]) => |
+ _getSourceFile(contents).span(node.offset, node.end); |
+ /// For logging errors. |
+ SourceFile _getSourceFile([String contents]) { |
+ var uri = getSourceUri(); |
+ var path = uri != null ? uri.toString() : assetId.path; |
+ return new SourceFile(contents != null ? contents : rawContents, url: path); |
+ } |
+ |
+ /// Gets a URI which would be appropriate for importing this file. |
+ /// |
+ /// Note that this file may represent a non-importable file such as a part. |
+ Uri getSourceUri([AssetId from]) { |
+ if (!assetId.path.startsWith('lib/')) { |
+ // Cannot do absolute imports of non lib-based assets. |
+ if (from == null) return null; |
+ |
+ if (assetId.package != from.package) return null; |
+ return new Uri( |
+ path: path.relative(assetId.path, from: path.dirname(from.path))); |
+ } |
+ |
+ return Uri.parse('package:${assetId.package}/${assetId.path.substring(4)}'); |
+ } |
+} |
+ |
+/// Implementation of Analyzer's UriResolver for Barback based assets. |
+class _AssetUriResolver implements UriResolver { |
+ final ResolverImpl _resolver; |
+ _AssetUriResolver(this._resolver); |
+ |
+ Source resolveAbsolute(Uri uri) { |
+ assert(uri.scheme != 'dart'); |
+ var assetId; |
+ if (uri.scheme == 'asset') { |
+ var parts = path.split(uri.path); |
+ assetId = new AssetId(parts[0], path.joinAll(parts.skip(1))); |
+ } else { |
+ assetId = _resolve(null, uri.toString(), logger, null); |
+ if (assetId == null) { |
+ logger.error('Unable to resolve asset ID for "$uri"'); |
+ return null; |
+ } |
+ } |
+ var source = _resolver.sources[assetId]; |
+ // Analyzer expects that sources which are referenced but do not exist yet |
+ // still exist, so just make an empty source. |
+ if (source == null) { |
+ source = new _AssetBasedSource(assetId, _resolver); |
+ _resolver.sources[assetId] = source; |
+ } |
+ return source; |
+ } |
+ |
+ Source fromEncoding(UriKind kind, Uri uri) => |
+ throw new UnsupportedError('fromEncoding is not supported'); |
+ |
+ Uri restoreAbsolute(Source source) => |
+ throw new UnsupportedError('restoreAbsolute is not supported'); |
+ |
+ TransformLogger get logger => _resolver._currentTransform.logger; |
+} |
+ |
+/// Get an asset ID for a URL relative to another source asset. |
+AssetId _resolve(AssetId source, String url, TransformLogger logger, |
+ SourceSpan span) { |
+ if (url == null || url == '') return null; |
+ var uri = Uri.parse(url); |
+ |
+ // Workaround for dartbug.com/17156- pub transforms package: imports from |
+ // files of the transformers package to have absolute /packages/ URIs. |
+ if (uri.scheme == '' && path.isAbsolute(url) |
+ && uri.pathSegments[0] == 'packages') { |
+ uri = Uri.parse('package:${uri.pathSegments.skip(1).join(path.separator)}'); |
+ } |
+ |
+ if (uri.scheme == 'package') { |
+ var segments = new List.from(uri.pathSegments); |
+ var package = segments[0]; |
+ segments[0] = 'lib'; |
+ return new AssetId(package, segments.join(path.separator)); |
+ } |
+ // Dart SDK libraries do not have assets. |
+ if (uri.scheme == 'dart') return null; |
+ |
+ return uriToAssetId(source, url, logger, span); |
+} |
+ |
+ |
+/// A completer that waits until all added [Future]s complete. |
+// TODO(blois): Copied from quiver. Remove from here when it gets |
+// added to dart:core. (See #6626.) |
+class FutureGroup<E> { |
+ static const _FINISHED = -1; |
+ |
+ int _pending = 0; |
+ Future _failedTask; |
+ final Completer<List> _completer = new Completer<List>(); |
+ final List results = []; |
+ |
+ /** Gets the task that failed, if any. */ |
+ Future get failedTask => _failedTask; |
+ |
+ /** |
+ * Wait for [task] to complete. |
+ * |
+ * If this group has already been marked as completed, a [StateError] will be |
+ * thrown. |
+ * |
+ * If this group has a [failedTask], new tasks will be ignored, because the |
+ * error has already been signaled. |
+ */ |
+ void add(Future task) { |
+ if (_failedTask != null) return; |
+ if (_pending == _FINISHED) throw new StateError("Future already completed"); |
+ |
+ _pending++; |
+ var i = results.length; |
+ results.add(null); |
+ task.then((res) { |
+ results[i] = res; |
+ if (_failedTask != null) return; |
+ _pending--; |
+ if (_pending == 0) { |
+ _pending = _FINISHED; |
+ _completer.complete(results); |
+ } |
+ }, onError: (e, s) { |
+ if (_failedTask != null) return; |
+ _failedTask = task; |
+ _completer.completeError(e, s); |
+ }); |
+ } |
+ |
+ /** |
+ * A Future that completes with a List of the values from all the added |
+ * tasks, when they have all completed. |
+ * |
+ * If any task fails, this Future will receive the error. Only the first |
+ * error will be sent to the Future. |
+ */ |
+ Future<List<E>> get future => _completer.future; |
+} |
+ |
+/// A pending update to notify the resolver that a [Source] has been added or |
+/// changed. This is used by the `_performResolve` algorithm above to apply all |
+/// changes after it first discovers the transitive closure of files that are |
+/// reachable from the sources. |
+class _PendingUpdate { |
+ _AssetBasedSource source; |
+ String content; |
+ |
+ _PendingUpdate(this.source, this.content); |
+ |
+ void apply(ChangeSet changeSet) { |
+ if (!source.updateContents(content)) return; |
+ if (source._revision == 1 && source._contents != null) { |
+ changeSet.addedSource(source); |
+ } else { |
+ changeSet.changedSource(source); |
+ } |
+ } |
+} |