@@ -15,6 +15,8 @@ import 'package:path/path.dart' as p;
15
15
16
16
import 'analysis_driver_model_uri_resolver.dart' ;
17
17
18
+ const _transitiveDigestExtension = '.transitive_digest' ;
19
+
18
20
/// Manages analysis driver and related build state.
19
21
///
20
22
/// - Tracks the import graph of all sources needed for analysis.
@@ -34,16 +36,18 @@ class AnalysisDriverModel {
34
36
final MemoryResourceProvider resourceProvider =
35
37
MemoryResourceProvider (context: p.posix);
36
38
39
+ final _graph = _Graph ();
40
+ final _readForAnalyzer = < AssetId > {};
41
+
37
42
/// Notifies that [step] has completed.
38
43
///
39
44
/// All build steps must complete before [reset] is called.
40
- void notifyComplete (BuildStep step) {
41
- // TODO(davidmorgan): add test coverage, fix implementation.
42
- }
45
+ void notifyComplete (BuildStep step) {}
43
46
44
47
/// Clear cached information specific to an individual build.
45
48
void reset () {
46
- // TODO(davidmorgan): add test coverage, fix implementation.
49
+ _graph.clear ();
50
+ _readForAnalyzer.clear ();
47
51
}
48
52
49
53
/// Attempts to parse [uri] into an [AssetId] and returns it if it is cached.
@@ -82,66 +86,70 @@ class AnalysisDriverModel {
82
86
FutureOr <void > Function (AnalysisDriverForPackageBuild ))
83
87
withDriverResource,
84
88
{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);
89
+ await withDriverResource ((driver) async {
90
+ var analyzerIds = entryPoints;
91
+ Iterable <AssetId > inputIds = entryPoints;
92
+
93
+ // If requested, find transitive imports.
94
+ if (transitive) {
95
+ await _graph.load (buildStep, entryPoints);
96
+ // TODO(davidmorgan): use just the set for the entrypoints.
97
+ analyzerIds = _graph.nodes.keys.toList ();
98
+ inputIds = _graph.inputsFor (entryPoints);
99
+
100
+ // Check for missing inputs that were written during the build.
101
+ for (final id in inputIds
102
+ .where ((id) => ! id.path.endsWith (_transitiveDigestExtension))) {
103
+ if (_graph.nodes[id]! .isMissing) {
104
+ if (await buildStep.canRead (id)) {
105
+ analyzerIds.add (id);
106
+ _readForAnalyzer.remove (id);
107
+ }
108
+ }
106
109
}
107
110
}
108
- }
109
111
110
- // Notify the analyzer of changes.
111
- 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);
112
+ // Notify [buildStep] of its inputs.
113
+ for (final id in inputIds) {
114
+ await buildStep.canRead (id);
116
115
}
117
- await driver.applyPendingFileChanges ();
118
- });
119
- }
120
116
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 ();
128
-
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 ;
132
-
133
- final content = await reader.readAsString (nextId);
134
- final deps = _parseDependencies (content, nextId);
117
+ // Apply changes to in-memory filesystem.
118
+ final changedIds = < AssetId > [];
119
+ for (final id in analyzerIds) {
120
+ if (! _readForAnalyzer.add (id)) continue ;
121
+ final content = await buildStep.canRead (id)
122
+ ? await buildStep.readAsString (id)
123
+ : null ;
124
+ final inMemoryFile = resourceProvider.getFile (id.asPath);
125
+ final inMemoryContent =
126
+ inMemoryFile.exists ? inMemoryFile.readAsStringSync () : null ;
135
127
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);
128
+ if (content != inMemoryContent) {
129
+ if (content == null ) {
130
+ // TODO(davidmorgan): per "globallySeenAssets" in
131
+ // BuildAssetUriResolver, deletes should only be applied at the end
132
+ // of the build, in case the file is actually there but not visible
133
+ // to the current reader.
134
+ resourceProvider.deleteFile (id.asPath);
135
+ changedIds.add (id);
136
+ } else {
137
+ if (inMemoryContent == null ) {
138
+ resourceProvider.newFile (id.asPath, content);
139
+ } else {
140
+ resourceProvider.modifyFile (id.asPath, content);
141
+ }
142
+ changedIds.add (id);
143
+ }
141
144
}
142
145
}
143
- }
144
- return result;
146
+
147
+ // Notify the analyzer of changes.
148
+ for (final id in changedIds) {
149
+ driver.changeFile (id.asPath);
150
+ }
151
+ await driver.applyPendingFileChanges ();
152
+ });
145
153
}
146
154
}
147
155
@@ -167,3 +175,95 @@ extension _AssetIdExtensions on AssetId {
167
175
/// Asset path for the in-memory filesystem.
168
176
String get asPath => AnalysisDriverModelUriResolver .assetPath (this );
169
177
}
178
+
179
+ class _Graph {
180
+ final Map <AssetId , _Node > nodes = {};
181
+
182
+ /// Walks the import graph from [ids] loading into [nodes] .
183
+ Future <void > load (AssetReader reader, Iterable <AssetId > ids) async {
184
+ final nextIds = Queue .of (ids);
185
+ while (nextIds.isNotEmpty) {
186
+ final nextId = nextIds.removeFirst ();
187
+
188
+ // Skip if already seen.
189
+ if (nodes.containsKey (nextId)) continue ;
190
+
191
+ final hasTransitiveDigestAsset =
192
+ await reader.canRead (nextId.addExtension (_transitiveDigestExtension));
193
+
194
+ // Skip if not readable.
195
+ if (! await reader.canRead (nextId)) {
196
+ nodes[nextId] = _Node (
197
+ id: nextId,
198
+ deps: null ,
199
+ hasTransitiveDigestAsset: hasTransitiveDigestAsset);
200
+ continue ;
201
+ }
202
+
203
+ final content = await reader.readAsString (nextId);
204
+ final deps = _parseDependencies (content, nextId);
205
+ nodes[nextId] = _Node (
206
+ id: nextId,
207
+ deps: deps,
208
+ hasTransitiveDigestAsset: hasTransitiveDigestAsset);
209
+ nextIds.addAll (deps.where ((id) => ! nodes.containsKey (id)));
210
+ }
211
+ }
212
+
213
+ void clear () {
214
+ nodes.clear ();
215
+ }
216
+
217
+ /// The inputs for a build action analyzing [entryPoints] .
218
+ ///
219
+ /// This is transitive deps, but cut off by the presence of any
220
+ /// `.transitive_digest` file next to an asset.
221
+ Set <AssetId > inputsFor (Iterable <AssetId > entryPoints) {
222
+ final result = entryPoints.toSet ();
223
+ final nextIds = Queue .of (entryPoints);
224
+
225
+ while (nextIds.isNotEmpty) {
226
+ final nextId = nextIds.removeFirst ();
227
+ final node = nodes[nextId]! ;
228
+
229
+ // Add the transitive digest file as an input. If it exists, skip deps.
230
+ result.add (nextId.addExtension (_transitiveDigestExtension));
231
+ if (node.hasTransitiveDigestAsset) {
232
+ continue ;
233
+ }
234
+
235
+ // Skip if there are no deps because the file is missing.
236
+ if (node.deps == null ) continue ;
237
+
238
+ // For each dep, if it's not in `result` yet, it's newly-discovered:
239
+ // add it to `nextIds`.
240
+ for (final dep in node.deps! ) {
241
+ if (result.add (dep)) {
242
+ nextIds.add (dep);
243
+ }
244
+ }
245
+ }
246
+ return result;
247
+ }
248
+
249
+ @override
250
+ String toString () => nodes.toString ();
251
+ }
252
+
253
+ class _Node {
254
+ final AssetId id;
255
+ final List <AssetId >? deps;
256
+ final bool hasTransitiveDigestAsset;
257
+
258
+ _Node (
259
+ {required this .id,
260
+ required this .deps,
261
+ required this .hasTransitiveDigestAsset});
262
+
263
+ bool get isMissing => deps == null ;
264
+
265
+ @override
266
+ String toString () => '$id :'
267
+ '${hasTransitiveDigestAsset ? 'digest:' : '' }'
268
+ '${deps ?.toString () ?? 'missing' }' ;
269
+ }
0 commit comments