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 |