@@ -34,16 +34,24 @@ class AnalysisDriverModel {
34
34
final MemoryResourceProvider resourceProvider =
35
35
MemoryResourceProvider (context: p.posix);
36
36
37
+ /// The import graph of all sources needed for analysis.
38
+ final _graph = _Graph ();
39
+
40
+ /// Assets that have been synced into the in-memory filesystem
41
+ /// [resourceProvider] .
42
+ final _syncedOntoResourceProvider = < AssetId > {};
43
+
37
44
/// Notifies that [step] has completed.
38
45
///
39
46
/// All build steps must complete before [reset] is called.
40
47
void notifyComplete (BuildStep step) {
41
- // TODO(davidmorgan): add test coverage, fix implementation .
48
+ // This implementation doesn't keep state per `BuildStep`, nothing to do .
42
49
}
43
50
44
51
/// Clear cached information specific to an individual build.
45
52
void reset () {
46
- // TODO(davidmorgan): add test coverage, fix implementation.
53
+ _graph.clear ();
54
+ _syncedOntoResourceProvider.clear ();
47
55
}
48
56
49
57
/// Attempts to parse [uri] into an [AssetId] and returns it if it is cached.
@@ -82,66 +90,84 @@ class AnalysisDriverModel {
82
90
FutureOr <void > Function (AnalysisDriverForPackageBuild ))
83
91
withDriverResource,
84
92
{required bool transitive}) async {
85
- /// TODO(davidmorgan): add test coverage for whether transitive
86
- /// sources are read when [transitive] is false, fix the implementation
87
- /// here.
88
- /// TODO(davidmorgan): add test coverage for whether
89
- /// `.transitive_deps` files cut off the reporting of deps to the
90
- /// [buildStep] , fix the implementation here.
91
-
92
- // Find transitive deps, this also informs [buildStep] of all inputs).
93
- final ids = await _expandToTransitive (buildStep, entryPoints);
94
-
95
- // Apply changes to in-memory filesystem.
96
- for (final id in ids) {
97
- if (await buildStep.canRead (id)) {
98
- final content = await buildStep.readAsString (id);
99
-
100
- /// TODO(davidmorgan): add test coverage for when a file is
101
- /// modified rather than added, fix the implementation here.
102
- resourceProvider.newFile (id.asPath, content);
103
- } else {
104
- if (resourceProvider.getFile (id.asPath).exists) {
105
- resourceProvider.deleteFile (id.asPath);
106
- }
107
- }
108
- }
109
-
110
- // Notify the analyzer of changes.
93
+ // Immediately take the lock on `driver` so that the whole class state,
94
+ // `_graph` and `_readForAnalyzir`, is only mutated by one build step at a
95
+ // time. Otherwise, interleaved access complicates processing significantly.
111
96
await withDriverResource ((driver) async {
112
- for (final id in ids) {
113
- // TODO(davidmorgan): add test coverage for over-notification of
114
- // changes, fix the implementaion here.
115
- driver.changeFile (id.asPath);
116
- }
117
- await driver.applyPendingFileChanges ();
97
+ return _performResolve (driver, buildStep, entryPoints, withDriverResource,
98
+ transitive: transitive);
118
99
});
119
100
}
120
101
121
- /// Walks the import graph from [ids] , returns full transitive deps.
122
- Future <Set <AssetId >> _expandToTransitive (
123
- AssetReader reader, Iterable <AssetId > ids) async {
124
- final result = ids.toSet ();
125
- final nextIds = Queue .of (ids);
126
- while (nextIds.isNotEmpty) {
127
- final nextId = nextIds.removeFirst ();
102
+ Future <void > _performResolve (
103
+ AnalysisDriverForPackageBuild driver,
104
+ BuildStep buildStep,
105
+ List <AssetId > entryPoints,
106
+ Future <void > Function (
107
+ FutureOr <void > Function (AnalysisDriverForPackageBuild ))
108
+ withDriverResource,
109
+ {required bool transitive}) async {
110
+ var idsToSyncOntoResourceProvider = entryPoints;
111
+ Iterable <AssetId > inputIds = entryPoints;
128
112
129
- // Skip if not readable. Note that calling `canRead` still makes it a
130
- // dependency of the `BuildStep`.
131
- if (! await reader.canRead (nextId)) continue ;
113
+ // If requested, find transitive imports.
114
+ if (transitive) {
115
+ await _graph.load (buildStep, entryPoints);
116
+ idsToSyncOntoResourceProvider = _graph.nodes.keys.toList ();
117
+ inputIds = _graph.inputsFor (entryPoints);
132
118
133
- final content = await reader.readAsString (nextId);
134
- final deps = _parseDependencies (content, nextId);
119
+ // Check for missing inputs that were written during the build.
120
+ for (final id in inputIds
121
+ .where ((id) => ! id.path.endsWith (_transitiveDigestExtension))) {
122
+ if (_graph.nodes[id]! .isMissing) {
123
+ if (await buildStep.canRead (id)) {
124
+ idsToSyncOntoResourceProvider.add (id);
125
+ _syncedOntoResourceProvider.remove (id);
126
+ }
127
+ }
128
+ }
129
+ }
135
130
136
- // For each dep, if it's not in `result` yet, it's newly-discovered:
137
- // add it to `nextIds`.
138
- for (final dep in deps) {
139
- if (result.add (dep)) {
140
- nextIds.add (dep);
131
+ // Notify [buildStep] of its inputs.
132
+ for (final id in inputIds) {
133
+ await buildStep.canRead (id);
134
+ }
135
+
136
+ // Sync changes onto the "URI resolver", the in-memory filesystem.
137
+ final changedIds = < AssetId > [];
138
+ for (final id in idsToSyncOntoResourceProvider) {
139
+ if (! _syncedOntoResourceProvider.add (id)) continue ;
140
+ final content =
141
+ await buildStep.canRead (id) ? await buildStep.readAsString (id) : null ;
142
+ final inMemoryFile = resourceProvider.getFile (id.asPath);
143
+ final inMemoryContent =
144
+ inMemoryFile.exists ? inMemoryFile.readAsStringSync () : null ;
145
+
146
+ if (content != inMemoryContent) {
147
+ if (content == null ) {
148
+ // TODO(davidmorgan): per "globallySeenAssets" in
149
+ // BuildAssetUriResolver, deletes should only be applied at the end
150
+ // of the build, in case the file is actually there but not visible
151
+ // to the current reader.
152
+ resourceProvider.deleteFile (id.asPath);
153
+ changedIds.add (id);
154
+ } else {
155
+ if (inMemoryContent == null ) {
156
+ resourceProvider.newFile (id.asPath, content);
157
+ } else {
158
+ resourceProvider.modifyFile (id.asPath, content);
159
+ }
160
+ changedIds.add (id);
141
161
}
142
162
}
143
163
}
144
- return result;
164
+
165
+ // Notify the analyzer of changes and wait for it to update its internal
166
+ // state.
167
+ for (final id in changedIds) {
168
+ driver.changeFile (id.asPath);
169
+ }
170
+ await driver.applyPendingFileChanges ();
145
171
}
146
172
}
147
173
@@ -167,3 +193,108 @@ extension _AssetIdExtensions on AssetId {
167
193
/// Asset path for the in-memory filesystem.
168
194
String get asPath => AnalysisDriverModelUriResolver .assetPath (this );
169
195
}
196
+
197
+ /// The directive graph of all known sources.
198
+ ///
199
+ /// Also tracks whether there is a `.transitive_digest` file next to each source
200
+ /// asset, and tracks missing files.
201
+ class _Graph {
202
+ final Map <AssetId , _Node > nodes = {};
203
+
204
+ /// Walks the import graph from [ids] loading into [nodes] .
205
+ Future <void > load (AssetReader reader, Iterable <AssetId > ids) async {
206
+ final nextIds = Queue .of (ids);
207
+ while (nextIds.isNotEmpty) {
208
+ final nextId = nextIds.removeFirst ();
209
+
210
+ // Skip if already seen.
211
+ if (nodes.containsKey (nextId)) continue ;
212
+
213
+ final hasTransitiveDigestAsset =
214
+ await reader.canRead (nextId.addExtension (_transitiveDigestExtension));
215
+
216
+ // Skip if not readable.
217
+ if (! await reader.canRead (nextId)) {
218
+ nodes[nextId] = _Node .missing (
219
+ id: nextId, hasTransitiveDigestAsset: hasTransitiveDigestAsset);
220
+ continue ;
221
+ }
222
+
223
+ final content = await reader.readAsString (nextId);
224
+ final deps = _parseDependencies (content, nextId);
225
+ nodes[nextId] = _Node (
226
+ id: nextId,
227
+ deps: deps,
228
+ hasTransitiveDigestAsset: hasTransitiveDigestAsset);
229
+ nextIds.addAll (deps.where ((id) => ! nodes.containsKey (id)));
230
+ }
231
+ }
232
+
233
+ void clear () {
234
+ nodes.clear ();
235
+ }
236
+
237
+ /// The inputs for a build action analyzing [entryPoints] .
238
+ ///
239
+ /// This is transitive deps, but cut off by the presence of any
240
+ /// `.transitive_digest` file next to an asset.
241
+ Set <AssetId > inputsFor (Iterable <AssetId > entryPoints) {
242
+ final result = entryPoints.toSet ();
243
+ final nextIds = Queue .of (entryPoints);
244
+
245
+ while (nextIds.isNotEmpty) {
246
+ final nextId = nextIds.removeFirst ();
247
+ final node = nodes[nextId]! ;
248
+
249
+ // Add the transitive digest file as an input. If it exists, skip deps.
250
+ result.add (nextId.addExtension (_transitiveDigestExtension));
251
+ if (node.hasTransitiveDigestAsset) {
252
+ continue ;
253
+ }
254
+
255
+ // Skip if there are no deps because the file is missing.
256
+ if (node.isMissing) continue ;
257
+
258
+ // For each dep, if it's not in `result` yet, it's newly-discovered:
259
+ // add it to `nextIds`.
260
+ for (final dep in node.deps! ) {
261
+ if (result.add (dep)) {
262
+ nextIds.add (dep);
263
+ }
264
+ }
265
+ }
266
+ return result;
267
+ }
268
+
269
+ @override
270
+ String toString () => nodes.toString ();
271
+ }
272
+
273
+ /// A node in the directive graph.
274
+ class _Node {
275
+ final AssetId id;
276
+ final List <AssetId > deps;
277
+ final bool isMissing;
278
+ final bool hasTransitiveDigestAsset;
279
+
280
+ _Node (
281
+ {required this .id,
282
+ required this .deps,
283
+ required this .hasTransitiveDigestAsset})
284
+ : isMissing = false ;
285
+
286
+ _Node .missing ({required this .id, required this .hasTransitiveDigestAsset})
287
+ : isMissing = true ,
288
+ deps = const [];
289
+
290
+ @override
291
+ String toString () => '$id :'
292
+ '${hasTransitiveDigestAsset ? 'digest:' : '' }'
293
+ '${isMissing ? 'missing' : deps }' ;
294
+ }
295
+
296
+ // Transitive digest files are built next to source inputs. As the name
297
+ // suggests, they contain the transitive digest of all deps of the file.
298
+ // So, establishing a dependency on a transitive digest file is equivalent
299
+ // to establishing a dependency on all deps of the file.
300
+ const _transitiveDigestExtension = '.transitive_digest' ;
0 commit comments