@@ -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,85 @@ 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 inputs that were missing when the directive graph was read
120
+ // but have since been written by another build action.
121
+ for (final id in inputIds
122
+ .where ((id) => ! id.path.endsWith (_transitiveDigestExtension))) {
123
+ if (_graph.nodes[id]! .isMissing) {
124
+ if (await buildStep.canRead (id)) {
125
+ idsToSyncOntoResourceProvider.add (id);
126
+ _syncedOntoResourceProvider.remove (id);
127
+ }
128
+ }
129
+ }
130
+ }
135
131
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);
132
+ // Notify [buildStep] of its inputs.
133
+ for (final id in inputIds) {
134
+ await buildStep.canRead (id);
135
+ }
136
+
137
+ // Sync changes onto the "URI resolver", the in-memory filesystem.
138
+ final changedIds = < AssetId > [];
139
+ for (final id in idsToSyncOntoResourceProvider) {
140
+ if (! _syncedOntoResourceProvider.add (id)) continue ;
141
+ final content =
142
+ await buildStep.canRead (id) ? await buildStep.readAsString (id) : null ;
143
+ final inMemoryFile = resourceProvider.getFile (id.asPath);
144
+ final inMemoryContent =
145
+ inMemoryFile.exists ? inMemoryFile.readAsStringSync () : null ;
146
+
147
+ if (content != inMemoryContent) {
148
+ if (content == null ) {
149
+ // TODO(davidmorgan): per "globallySeenAssets" in
150
+ // BuildAssetUriResolver, deletes should only be applied at the end
151
+ // of the build, in case the file is actually there but not visible
152
+ // to the current reader.
153
+ resourceProvider.deleteFile (id.asPath);
154
+ changedIds.add (id);
155
+ } else {
156
+ if (inMemoryContent == null ) {
157
+ resourceProvider.newFile (id.asPath, content);
158
+ } else {
159
+ resourceProvider.modifyFile (id.asPath, content);
160
+ }
161
+ changedIds.add (id);
141
162
}
142
163
}
143
164
}
144
- return result;
165
+
166
+ // Notify the analyzer of changes and wait for it to update its internal
167
+ // state.
168
+ for (final id in changedIds) {
169
+ driver.changeFile (id.asPath);
170
+ }
171
+ await driver.applyPendingFileChanges ();
145
172
}
146
173
}
147
174
@@ -167,3 +194,109 @@ extension _AssetIdExtensions on AssetId {
167
194
/// Asset path for the in-memory filesystem.
168
195
String get asPath => AnalysisDriverModelUriResolver .assetPath (this );
169
196
}
197
+
198
+ /// The directive graph of all known sources.
199
+ ///
200
+ /// Also tracks whether there is a `.transitive_digest` file next to each source
201
+ /// asset, and tracks missing files.
202
+ class _Graph {
203
+ final Map <AssetId , _Node > nodes = {};
204
+
205
+ /// Walks the import graph from [ids] loading into [nodes] .
206
+ Future <void > load (AssetReader reader, Iterable <AssetId > ids) async {
207
+ // TODO(davidmorgan): check if List is faster.
208
+ final nextIds = Queue .of (ids);
209
+ while (nextIds.isNotEmpty) {
210
+ final nextId = nextIds.removeFirst ();
211
+
212
+ // Skip if already seen.
213
+ if (nodes.containsKey (nextId)) continue ;
214
+
215
+ final hasTransitiveDigestAsset =
216
+ await reader.canRead (nextId.addExtension (_transitiveDigestExtension));
217
+
218
+ // Skip if not readable.
219
+ if (! await reader.canRead (nextId)) {
220
+ nodes[nextId] = _Node .missing (
221
+ id: nextId, hasTransitiveDigestAsset: hasTransitiveDigestAsset);
222
+ continue ;
223
+ }
224
+
225
+ final content = await reader.readAsString (nextId);
226
+ final deps = _parseDependencies (content, nextId);
227
+ nodes[nextId] = _Node (
228
+ id: nextId,
229
+ deps: deps,
230
+ hasTransitiveDigestAsset: hasTransitiveDigestAsset);
231
+ nextIds.addAll (deps.where ((id) => ! nodes.containsKey (id)));
232
+ }
233
+ }
234
+
235
+ void clear () {
236
+ nodes.clear ();
237
+ }
238
+
239
+ /// The inputs for a build action analyzing [entryPoints] .
240
+ ///
241
+ /// This is transitive deps, but cut off by the presence of any
242
+ /// `.transitive_digest` file next to an asset.
243
+ Set <AssetId > inputsFor (Iterable <AssetId > entryPoints) {
244
+ final result = entryPoints.toSet ();
245
+ final nextIds = Queue .of (entryPoints);
246
+
247
+ while (nextIds.isNotEmpty) {
248
+ final nextId = nextIds.removeFirst ();
249
+ final node = nodes[nextId]! ;
250
+
251
+ // Add the transitive digest file as an input. If it exists, skip deps.
252
+ result.add (nextId.addExtension (_transitiveDigestExtension));
253
+ if (node.hasTransitiveDigestAsset) {
254
+ continue ;
255
+ }
256
+
257
+ // Skip if there are no deps because the file is missing.
258
+ if (node.isMissing) continue ;
259
+
260
+ // For each dep, if it's not in `result` yet, it's newly-discovered:
261
+ // add it to `nextIds`.
262
+ for (final dep in node.deps) {
263
+ if (result.add (dep)) {
264
+ nextIds.add (dep);
265
+ }
266
+ }
267
+ }
268
+ return result;
269
+ }
270
+
271
+ @override
272
+ String toString () => nodes.toString ();
273
+ }
274
+
275
+ /// A node in the directive graph.
276
+ class _Node {
277
+ final AssetId id;
278
+ final List <AssetId > deps;
279
+ final bool isMissing;
280
+ final bool hasTransitiveDigestAsset;
281
+
282
+ _Node (
283
+ {required this .id,
284
+ required this .deps,
285
+ required this .hasTransitiveDigestAsset})
286
+ : isMissing = false ;
287
+
288
+ _Node .missing ({required this .id, required this .hasTransitiveDigestAsset})
289
+ : isMissing = true ,
290
+ deps = const [];
291
+
292
+ @override
293
+ String toString () => '$id :'
294
+ '${hasTransitiveDigestAsset ? 'digest:' : '' }'
295
+ '${isMissing ? 'missing' : deps }' ;
296
+ }
297
+
298
+ // Transitive digest files are built next to source inputs. As the name
299
+ // suggests, they contain the transitive digest of all deps of the file.
300
+ // So, establishing a dependency on a transitive digest file is equivalent
301
+ // to establishing a dependency on all deps of the file.
302
+ const _transitiveDigestExtension = '.transitive_digest' ;
0 commit comments