-
Notifications
You must be signed in to change notification settings - Fork 210
/
Copy pathbuild_asset_uri_resolver.dart
187 lines (165 loc) · 6.91 KB
/
build_asset_uri_resolver.dart
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
// Copyright (c) 2018, 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.
import 'dart:async';
import 'dart:collection';
// ignore: deprecated_member_use
import 'package:analyzer/analyzer.dart' show parseDirectives;
import 'package:analyzer/dart/ast/ast.dart';
import 'package:analyzer/file_system/memory_file_system.dart';
import 'package:analyzer/src/dart/analysis/driver.dart' show AnalysisDriver;
import 'package:analyzer/src/generated/source.dart';
import 'package:build/build.dart' show AssetId, BuildStep;
import 'package:crypto/crypto.dart';
import 'package:graphs/graphs.dart';
import 'package:path/path.dart' as p;
const _ignoredSchemes = ['dart', 'dart-ext'];
class BuildAssetUriResolver extends UriResolver {
/// A cache of the directives for each Dart library.
///
/// This is stored across builds and is only invalidated if we read a file and
/// see that it's content is different from what it was last time it was read.
final _cachedAssetDependencies = <AssetId, Set<AssetId>>{};
/// A cache of the digest for each Dart asset.
///
/// This is stored across builds and used to invalidate the values in
/// [_cachedAssetDependencies] only when the actual content of the library
/// changed.
final _cachedAssetDigests = <AssetId, Digest>{};
final resourceProvider = MemoryResourceProvider(context: p.posix);
/// The assets which are known to be readable at some point during the current
/// build.
///
/// When actions can run out of order an asset can move from being readable
/// (in the later phase) to being unreadable (in the earlier phase which ran
/// later). If this happens we don't want to hide the asset from the analyzer.
final globallySeenAssets = HashSet<AssetId>();
/// The assets which have been resolved from a [BuildStep], either as an
/// input, subsequent calls to a resolver, or a transitive import thereof.
final _buildStepAssets = <BuildStep, HashSet<AssetId>>{};
/// Crawl the transitive imports from [entryPoints] and ensure that the
/// content of each asset is updated in [resourceProvider] and [driver].
Future<void> performResolve(BuildStep buildStep, List<AssetId> entryPoints,
AnalysisDriver driver) async {
final seenInBuildStep =
_buildStepAssets.putIfAbsent(buildStep, () => HashSet());
bool notCrawled(AssetId asset) => !seenInBuildStep.contains(asset);
final changedPaths = await crawlAsync<AssetId, _AssetState>(
entryPoints.where(notCrawled), (id) async {
final path = assetPath(id);
if (!await buildStep.canRead(id)) {
if (globallySeenAssets.contains(id)) {
// ignore from this graph, some later build step may still be using it
// so it shouldn't be removed from [resourceProvider], but we also
// don't care about it's transitive imports.
return null;
}
_cachedAssetDependencies.remove(id);
_cachedAssetDigests.remove(id);
if (resourceProvider.getFile(path).exists) {
resourceProvider.deleteFile(path);
}
return _AssetState.removed(path);
}
globallySeenAssets.add(id);
seenInBuildStep.add(id);
final digest = await buildStep.digest(id);
if (_cachedAssetDigests[id] == digest) {
return _AssetState.unchanged(path, _cachedAssetDependencies[id]);
} else {
final isChange = _cachedAssetDigests.containsKey(id);
final content = await buildStep.readAsString(id);
_cachedAssetDigests[id] = digest;
final dependencies =
_cachedAssetDependencies[id] = _parseDirectives(content, id);
if (isChange) {
resourceProvider.updateFile(path, content);
return _AssetState.changed(path, dependencies);
} else {
resourceProvider.newFile(path, content);
return _AssetState.newAsset(path, dependencies);
}
}
}, (id, state) => state.directives.where(notCrawled))
.where((state) => state.isAssetUpdate)
.map((state) => state.path)
.toList();
changedPaths.forEach(driver.changeFile);
}
/// Attempts to parse [uri] into an [AssetId].
///
/// Handles 'package:' or 'asset:' URIs, as well as 'file:' URIs that have the
/// same pattern used by [assetPath].
///
/// Returns null if the Uri cannot be parsed.
AssetId parseAsset(Uri uri) {
if (_ignoredSchemes.any(uri.isScheme)) return null;
if (uri.isScheme('package') || uri.isScheme('asset')) {
return AssetId.resolve('$uri');
}
if (uri.isScheme('file')) {
final parts = p.split(uri.path);
return AssetId(parts[1], p.posix.joinAll(parts.skip(2)));
}
return null;
}
/// Attempts to parse [uri] into an [AssetId] and returns it if it is cached.
///
/// Handles 'package:' or 'asset:' URIs, as well as 'file:' URIs that have the
/// same pattern used by [assetPath].
///
/// Returns null if the Uri cannot be parsed or is not cached.
AssetId lookupCachedAsset(Uri uri) {
final assetId = parseAsset(uri);
if (assetId == null || !_cachedAssetDigests.containsKey(assetId)) {
return null;
}
return assetId;
}
void notifyComplete(BuildStep step) {
_buildStepAssets.remove(step);
}
/// Clear cached information specific to an individual build.
void reset() {
assert(_buildStepAssets.isEmpty,
'Reset was called before all build steps completed');
globallySeenAssets.clear();
}
@override
Source resolveAbsolute(Uri uri, [Uri actualUri]) {
final assetId = parseAsset(uri);
if (assetId == null) return null;
return resourceProvider
.getFile(assetPath(assetId))
.createSource(assetId.uri);
}
@override
Uri restoreAbsolute(Source source) =>
lookupCachedAsset(source.uri)?.uri ?? source.uri;
}
String assetPath(AssetId assetId) =>
p.posix.join('/${assetId.package}', assetId.path);
/// Returns all the directives from a Dart library that can be resolved to an
/// [AssetId].
Set<AssetId> _parseDirectives(String content, AssetId from) =>
// ignore: deprecated_member_use
HashSet.of(parseDirectives(content, suppressErrors: true)
.directives
.whereType<UriBasedDirective>()
.where((directive) {
var uri = Uri.parse(directive.uri.stringValue);
return !_ignoredSchemes.any(uri.isScheme);
})
.map((d) => AssetId.resolve(d.uri.stringValue, from: from))
.where((id) => id != null));
class _AssetState {
final String path;
final bool isAssetUpdate;
final Iterable<AssetId> directives;
_AssetState.removed(this.path)
: isAssetUpdate = false,
directives = const [];
_AssetState.changed(this.path, this.directives) : isAssetUpdate = true;
_AssetState.unchanged(this.path, this.directives) : isAssetUpdate = false;
_AssetState.newAsset(this.path, this.directives) : isAssetUpdate = true;
}