Skip to content

Commit 4f5f558

Browse files
committed
Add to resolver_test, add to AnalysisDriverModel as needed to satisfy the tests.
Run the tests with both "shared" instances, re-used between tests (as before) and "new" instances.
1 parent 5c1ddd4 commit 4f5f558

File tree

4 files changed

+395
-67
lines changed

4 files changed

+395
-67
lines changed

build_resolvers/lib/src/analysis_driver_model.dart

Lines changed: 183 additions & 52 deletions
Original file line numberDiff line numberDiff line change
@@ -34,16 +34,24 @@ class AnalysisDriverModel {
3434
final MemoryResourceProvider resourceProvider =
3535
MemoryResourceProvider(context: p.posix);
3636

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+
3744
/// Notifies that [step] has completed.
3845
///
3946
/// All build steps must complete before [reset] is called.
4047
void notifyComplete(BuildStep step) {
41-
// TODO(davidmorgan): add test coverage, fix implementation.
48+
// This implementation doesn't keep state per `BuildStep`, nothing to do.
4249
}
4350

4451
/// Clear cached information specific to an individual build.
4552
void reset() {
46-
// TODO(davidmorgan): add test coverage, fix implementation.
53+
_graph.clear();
54+
_syncedOntoResourceProvider.clear();
4755
}
4856

4957
/// Attempts to parse [uri] into an [AssetId] and returns it if it is cached.
@@ -82,66 +90,84 @@ class AnalysisDriverModel {
8290
FutureOr<void> Function(AnalysisDriverForPackageBuild))
8391
withDriverResource,
8492
{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.
11196
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);
11899
});
119100
}
120101

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;
128112

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);
132118

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+
}
135130

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);
141161
}
142162
}
143163
}
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();
145171
}
146172
}
147173

@@ -167,3 +193,108 @@ extension _AssetIdExtensions on AssetId {
167193
/// Asset path for the in-memory filesystem.
168194
String get asPath => AnalysisDriverModelUriResolver.assetPath(this);
169195
}
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

Comments
 (0)