Index: pkg/code_transformers/lib/src/resolver_impl.dart |
diff --git a/pkg/code_transformers/lib/src/resolver_impl.dart b/pkg/code_transformers/lib/src/resolver_impl.dart |
new file mode 100644 |
index 0000000000000000000000000000000000000000..5918c95933ee4e2aa5c48472aa5b2fb94bc0336a |
--- /dev/null |
+++ b/pkg/code_transformers/lib/src/resolver_impl.dart |
@@ -0,0 +1,492 @@ |
+// 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/src/generated/ast.dart'; |
+import 'package:analyzer/src/generated/element.dart'; |
+import 'package:analyzer/src/generated/engine.dart'; |
+import 'package:analyzer/src/generated/error.dart'; |
+import 'package:analyzer/src/generated/java_io.dart'; |
+import 'package:analyzer/src/generated/parser.dart' show Parser; |
+import 'package:analyzer/src/generated/scanner.dart'; |
+import 'package:analyzer/src/generated/sdk.dart' show DartSdk; |
+import 'package:analyzer/src/generated/sdk_io.dart' show DirectoryBasedDartSdk; |
+import 'package:analyzer/src/generated/source.dart'; |
+import 'package:barback/barback.dart'; |
+import 'package:path/path.dart' as path; |
+import 'package:source_maps/refactor.dart'; |
+import 'package:source_maps/span.dart' show SourceFile, Span; |
+ |
+import 'resolver.dart'; |
+ |
+/// 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>{}; |
+ /// The Dart entry point file where parsing begins. |
Siggi Cherem (dart-lang)
2014/02/14 02:13:37
nit: add empty line above comment
blois
2014/02/19 21:42:58
Done.
|
+ final AssetId entryPoint; |
+ |
+ final AnalysisContext _context = |
+ AnalysisEngine.instance.createAnalysisContext(); |
+ |
+ /// Transform for which this is currently updating, or null when not updating. |
+ Transform _currentTransform; |
+ /// The currently resolved library, or null if unresolved. |
Siggi Cherem (dart-lang)
2014/02/14 02:13:37
same here and below
blois
2014/02/19 21:42:58
Done.
|
+ LibraryElement _entryLibrary; |
+ /// Handler for all Dart SDK (dart:) sources. |
+ DirectoryBasedDartSdk _dartSdk; |
+ |
+ /// Creates a resolver that will resolve the Dart code starting at |
+ /// [entryPoint]. |
+ /// |
+ /// [sdkDir] is the root directory of the Dart SDK, for resolving dart: |
+ /// imports. |
+ ResolverImpl(this.entryPoint, String sdkDir, {AnalysisOptions options}) { |
+ if (options == null) { |
+ options = new AnalysisOptionsImpl() |
+ ..cacheSize = 256 // # of sources to cache ASTs for. |
Siggi Cherem (dart-lang)
2014/02/14 02:13:37
can we make it bigger :)?
blois
2014/02/19 21:42:58
I actually just cloned this from Angular's extract
|
+ ..preserveComments = false |
+ ..analyzeFunctionBodies = true; |
Siggi Cherem (dart-lang)
2014/02/14 02:13:37
should this be an option too? Seems like a common
blois
2014/02/19 21:42:58
Added, but the options themselves can be provided
Siggi Cherem (dart-lang)
2014/02/19 22:36:06
Ah - good point, I overlooked that if options is p
|
+ } |
+ _context.analysisOptions = options; |
+ |
+ _dartSdk = new _DirectoryBasedDartSdkProxy(new JavaFile(sdkDir)); |
+ _dartSdk.context.analysisOptions = options; |
+ |
+ _context.sourceFactory = new SourceFactory.con2([ |
+ new DartUriResolverProxy(_dartSdk), |
+ new _AssetUriResolver(this)]); |
+ } |
+ |
+ LibraryElement get entryLibrary => _entryLibrary; |
+ |
+ |
+ /// Update the status of all the sources referenced by the entryPoint and |
+ /// update the resolved library. |
+ /// |
+ /// This will be invoked automatically by [ResolverTransformer]. Only one |
+ /// transformer may update this at a time. |
+ Future updateSources(Transform transform) { |
+ if (_currentTransform != null) { |
+ throw new StateError('Cannot be accessed by concurrent transforms'); |
+ } |
+ _currentTransform = transform; |
+ // Clear this out and update once all asset changes have been processed. |
+ _entryLibrary = null; |
+ |
+ // 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 toVisit = new Set<AssetId>(); |
+ var changedSources = <Source>[]; |
+ var addedSources = <Source>[]; |
+ var removedSources = <Source>[]; |
+ toVisit.add(entryPoint); |
+ |
+ Future visitNext() { |
+ if (toVisit.length == 0) return null; |
+ |
+ var assetId = toVisit.first; |
Siggi Cherem (dart-lang)
2014/02/14 02:13:37
might be cheaper to use a queue instead of a Set f
blois
2014/02/19 21:42:58
Since order is not critical better to optimize for
Siggi Cherem (dart-lang)
2014/02/19 22:36:06
oh - I think you can remove the check for 'toVisit
blois
2014/02/20 01:03:12
Ended up switching over to FutureGroup which allow
|
+ toVisit.remove(assetId); |
+ visited.add(assetId); |
+ |
+ return transform.readInputAsString(assetId).then((contents) { |
+ var source = sources[assetId]; |
+ if (source == null) { |
+ source = new _AssetBasedSource(assetId, this); |
+ sources[assetId] = source; |
+ addedSources.add(source); |
+ } |
+ var changed = source.updateContents(contents); |
+ if (changed) { |
+ changedSources.add(source); |
+ } |
+ |
+ for (var id in source.dependentAssets) { |
+ if (!visited.contains(id) && !toVisit.contains(id)) { |
+ toVisit.add(id); |
+ } |
+ } |
+ |
+ return visitNext(); |
Siggi Cherem (dart-lang)
2014/02/14 02:13:37
FYI - this is fine, but it might be worth using Fu
blois
2014/02/19 21:42:58
Would prefer to leave as-is for now, though switch
|
+ }, onError: (e) { |
+ removedSources.add(sources[assetId]); |
+ return visitNext(); |
+ }); |
+ } |
+ |
+ // Once we have all asset sources updated with the new contents then |
+ // resolve everything. |
+ return new Future(visitNext).then((_) { |
+ ChangeSet changeSet = new ChangeSet(); |
Siggi Cherem (dart-lang)
2014/02/14 02:13:37
-> var
blois
2014/02/19 21:42:58
Done.
|
+ |
+ // Add all sources which were not reached to the removed set. |
+ removedSources.addAll(new Set.from(sources.keys) |
+ .difference(visited) |
+ .map((id) => sources[id])); |
+ |
+ // Clear out removed sources from our cache. |
+ removedSources.map((source) => source.assetId).forEach(sources.remove); |
+ |
+ addedSources.forEach(changeSet.added); |
+ changedSources.forEach(changeSet.changed); |
+ removedSources.forEach(changeSet.removed); |
+ |
+ // Update the analyzer context with the latest sources |
+ _context.applyChanges(changeSet); |
+ // Resolve the AST |
+ _entryLibrary = _context.computeLibraryElement(sources[entryPoint]); |
+ |
+ _currentTransform = null; |
+ }); |
+ } |
+ |
+ Iterable<LibraryElement> get libraries => entryLibrary.visibleLibraries; |
+ |
+ 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); |
+ } |
+ |
+ Uri getImportUri(LibraryElement lib, {AssetId from}) { |
+ var source = lib.source; |
+ if (source is _AssetBasedSource) { |
+ var id = source.assetId; |
+ |
+ if (!id.path.startsWith('lib/')) { |
+ // Cannot do absolute imports of non lib-based assets. |
+ if (from == null) return null; |
+ |
+ if (id.package != from.package) return null; |
+ return new Uri( |
+ path: path.relative(id.path, from: path.dirname(from.path))); |
Siggi Cherem (dart-lang)
2014/02/14 02:13:37
remove extra space
blois
2014/02/19 21:42:58
Done.
|
+ } |
+ |
+ return Uri.parse('package:${id.package}/${id.path.substring(4)}'); |
+ } else if (source is _DartSourceProxy) { |
+ 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; |
+ } |
+ |
+ Span getSourceSpan(Element element) { |
+ var assetId = getSourceAssetId(element); |
+ if (assetId == null) return null; |
+ |
+ var sourceFile = new SourceFile.text(assetId.path, |
Siggi Cherem (dart-lang)
2014/02/14 02:13:37
let's change this first argument to match what we
blois
2014/02/19 21:42:58
Done.
|
+ sources[assetId].contents); |
+ return sourceFile.span(element.node.offset, element.node.end); |
+ } |
+ |
+ TextEditTransaction createTextEditTransaction(Element element) { |
+ if (element.source is! _AssetBasedSource) return null; |
+ |
+ _AssetBasedSource source = element.source; |
+ // Cannot modify assets in other packages. |
+ if (source.assetId.package != entryPoint.package) return null; |
+ |
+ var sourceFile = new SourceFile.text(source.assetId.path, source.contents); |
Siggi Cherem (dart-lang)
2014/02/14 02:13:37
same here
blois
2014/02/19 21:42:58
Done.
|
+ return new TextEditTransaction(source.contents, sourceFile); |
+ } |
+} |
+ |
+/// 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. |
Siggi Cherem (dart-lang)
2014/02/14 02:13:37
similar style nit here: add empty line above each
blois
2014/02/19 21:42:58
Done.
|
+ 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 contents of this file with [contents]. |
+ /// |
+ /// Returns true if the contents of this asset have changed. |
+ bool updateContents(String contents) { |
+ if (contents != _contents) { |
+ _contents = contents; |
+ ++_revision; |
Siggi Cherem (dart-lang)
2014/02/14 02:13:37
_revision++?
It's style nit, I think we use i++ m
blois
2014/02/19 21:42:58
Old habits die hard. Prefix operator is sometimes
Siggi Cherem (dart-lang)
2014/02/19 22:36:06
Yeah, I recall that was the case in C++, good thin
|
+ // Invalidate the imports so we only parse the AST when needed. |
+ _dependentAssets = null; |
+ return true; |
+ } |
+ return false; |
+ } |
+ |
+ /// Contents of the file. |
+ String get contents => _contents; |
+ |
+ /// 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 { |
Siggi Cherem (dart-lang)
2014/02/14 02:13:37
since you already have a cache of _AssetBasedSourc
|
+ // Use the cached imports if we have them. |
+ if (_dependentAssets != null) return _dependentAssets; |
+ |
+ var errorListener = new _ErrorCollector(); |
+ var reader = new CharSequenceReader(contents); |
+ var scanner = new Scanner(null, reader, errorListener); |
+ var token = scanner.tokenize(); |
+ var parser = new Parser(null, errorListener); |
+ |
+ var compilationUnit = parser.parseCompilationUnit(token); |
+ |
+ // Walk the AST looking for import/export/part directives. |
+ _dependentAssets = compilationUnit.directives |
+ .where((d) => (d is ImportDirective || d is PartDirective || |
+ d is ExportDirective)) |
+ .map((d) => _resolve(assetId, d.uri.stringValue, |
+ _logger, _getSpan(d))) |
+ .where((id) => id != null); |
+ |
+ return _dependentAssets; |
+ } |
+ |
+ bool exists() => true; |
+ |
+ bool operator ==(Object other) => |
+ other is _AssetBasedSource && assetId == other.assetId; |
+ |
+ int get hashCode => assetId.hashCode; |
+ |
+ void getContents(Source_ContentReceiver receiver) { |
+ receiver.accept(contents, 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; |
+ } |
+ |
+ /// For logging errors. |
+ Span _getSpan(ASTNode node) => _sourceFile.span(node.offset, node.end); |
+ /// For logging errors. |
+ SourceFile get _sourceFile => new SourceFile.text(assetId.path, contents); |
Siggi Cherem (dart-lang)
2014/02/14 02:13:37
same here: use something like spanUriFor(...)
blois
2014/02/19 21:42:58
Done.
|
+} |
+ |
+/// Implementation of Analyzer's UriResolver for Barback based assets. |
+class _AssetUriResolver implements UriResolver { |
+ final ResolverImpl _resolver; |
+ _AssetUriResolver(this._resolver); |
+ |
+ Source resolveAbsolute(ContentCache contentCache, Uri uri) { |
+ var assetId = _resolve(null, uri.toString(), logger, null); |
+ var source = _resolver.sources[assetId]; |
+ /// All resolved assets should be available by this point. |
+ if (source == null) { |
+ logger.error('Unable to find asset for "$uri"'); |
+ } |
+ return source; |
+ } |
+ |
+ Source fromEncoding(ContentCache contentCache, 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; |
+} |
+ |
+ |
+/// Dart SDK which wraps all Dart sources to ensure they are tracked with Uris. |
+/// |
+/// Just a simple wrapper to make it easy to make sure that all sources we |
+/// encounter are either _AssetBasedSource or _DartSourceProxy. |
+class _DirectoryBasedDartSdkProxy extends DirectoryBasedDartSdk { |
+ _DirectoryBasedDartSdkProxy(JavaFile sdkDirectory) : super(sdkDirectory); |
+ |
+ Source mapDartUri(String dartUri) => |
+ _DartSourceProxy.wrap(super.mapDartUri(dartUri), Uri.parse(dartUri)); |
+} |
+ |
+ |
+/// Dart SDK resolver which wraps all Dart sources to ensure they are tracked |
+/// with URIs. |
+class DartUriResolverProxy implements DartUriResolver { |
+ final DartUriResolver _proxy; |
+ DartUriResolverProxy(DirectoryBasedDartSdk sdk) : |
+ _proxy = new DartUriResolver(sdk); |
+ |
+ Source resolveAbsolute(ContentCache contentCache, Uri uri) => |
+ _DartSourceProxy.wrap(_proxy.resolveAbsolute(contentCache, uri), uri); |
+ |
+ DartSdk get dartSdk => _proxy.dartSdk; |
+ |
+ Source fromEncoding(ContentCache contentCache, UriKind kind, Uri uri) => |
+ throw new UnsupportedError('fromEncoding is not supported'); |
+ |
+ Uri restoreAbsolute(Source source) => |
+ throw new UnsupportedError('restoreAbsolute is not supported'); |
+} |
+ |
+/// Source file for dart: sources which track the sources with dart: URIs. |
+/// |
+/// This is primarily to support [Resolver.getImportUri] for Dart SDK (dart:) |
+/// based libraries. |
+class _DartSourceProxy implements Source { |
+ /// Absolute URI which this source can be imported from |
+ final Uri uri; |
+ /// Underlying source object. |
Siggi Cherem (dart-lang)
2014/02/14 02:13:37
+line
blois
2014/02/19 21:42:58
Done.
|
+ final Source _proxy; |
+ |
+ _DartSourceProxy(this._proxy, this.uri); |
+ |
+ /// Ensures that [source] is a _DartSourceProxy. |
+ static _DartSourceProxy wrap(Source source, Uri uri) { |
+ if (source == null || source is _DartSourceProxy) return source; |
+ return new _DartSourceProxy(source, uri); |
+ } |
+ |
+ Source resolveRelative(Uri relativeUri) { |
+ // Assume that the type can be accessed via this URI, since these |
+ // should only be parts for dart core files. |
+ return wrap(_proxy.resolveRelative(relativeUri), uri); |
+ } |
+ |
+ bool exists() => _proxy.exists(); |
+ |
+ bool operator ==(Object other) => |
+ (other is _DartSourceProxy && _proxy == other._proxy); |
+ |
+ int get hashCode => _proxy.hashCode; |
+ |
+ void getContents(Source_ContentReceiver receiver) { |
+ _proxy.getContents(receiver); |
+ } |
+ |
+ String get encoding => _proxy.encoding; |
+ |
+ String get fullName => _proxy.fullName; |
+ |
+ int get modificationStamp => _proxy.modificationStamp; |
+ |
+ String get shortName => _proxy.shortName; |
+ |
+ UriKind get uriKind => _proxy.uriKind; |
+ |
+ bool get isInSystemLibrary => _proxy.isInSystemLibrary; |
+} |
+ |
+ |
+class _ErrorCollector extends AnalysisErrorListener { |
+ final errors = <AnalysisError>[]; |
+ onError(error) => errors.add(error); |
+} |
+ |
+/// Get an asset ID for a URL relative to another source asset. |
+AssetId _resolve(AssetId source, String url, TransformLogger logger, |
Siggi Cherem (dart-lang)
2014/02/14 02:13:37
this seems similar to what we have in common, is t
blois
2014/02/19 21:42:58
It's actually slightly different- this goes from a
|
+ Span span) { |
+ if (url == null || url == '') return null; |
+ var urlBuilder = path.url; |
+ var uri = Uri.parse(url); |
+ |
+ if (uri.scheme == 'package') { |
+ var segments = new List.from(uri.pathSegments); |
+ var package = segments[0]; |
+ segments[0] = 'lib'; |
+ return new AssetId(package, segments.join(urlBuilder.separator)); |
+ } |
+ // Dart SDK libraries do not have assets. |
+ if (uri.scheme == 'dart') { |
Siggi Cherem (dart-lang)
2014/02/14 02:13:37
style nit: fast-exit style
if (uri.scheme == 'dar
blois
2014/02/19 21:42:58
Done.
|
+ return null; |
+ } |
+ |
+ if (uri.host != '' || uri.scheme != '' || urlBuilder.isAbsolute(url)) { |
+ logger.error('absolute paths not allowed: "$url"', span: span); |
+ return null; |
+ } |
+ |
+ var targetPath = urlBuilder.normalize( |
+ urlBuilder.join(urlBuilder.dirname(source.path), url)); |
+ return new AssetId(source.package, targetPath); |
+} |