Skip to content

Commit a8fb66b

Browse files
committed
Move in-memory filesystem functionality into one class AnalysisDriverFilesystem.
Implement the analyzer interfaces rather than using it's in-memory filesystem, which provides a lot of unused functionality. Simplify `AnalysisDriverModel` and `BuildAssetUriResolver`. Add unit test.
1 parent 89a45a9 commit a8fb66b

7 files changed

+279
-161
lines changed

build_resolvers/CHANGELOG.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
## 2.4.4-wip
22

33
- Refactor `BuildAssetUriResolver` into `AnalysisDriverModel` and
4-
`AnalysisDriverModelUriResolver`. Add new implementation of
4+
`AnalysisDriverFilesystem`. Add new implementation of
55
`AnalysisDriverModel`.
66

77
## 2.4.3

build_resolvers/lib/src/analysis_driver.dart

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,6 @@ import 'package:path/path.dart' as p;
1313
import 'package:pub_semver/pub_semver.dart';
1414

1515
import 'analysis_driver_model.dart';
16-
import 'analysis_driver_model_uri_resolver.dart';
1716
import 'build_asset_uri_resolver.dart';
1817

1918
/// Builds an [AnalysisDriverForPackageBuild] backed by a summary SDK.
@@ -29,12 +28,12 @@ Future<AnalysisDriverForPackageBuild> analysisDriver(
2928
analysisOptions: analysisOptions,
3029
packages: _buildAnalyzerPackages(
3130
packageConfig,
32-
analysisDriverModel.resourceProvider,
31+
analysisDriverModel.filesystem,
3332
),
34-
resourceProvider: analysisDriverModel.resourceProvider,
33+
resourceProvider: analysisDriverModel.filesystem,
3534
sdkSummaryBytes: File(sdkSummaryPath).readAsBytesSync(),
3635
uriResolvers: [
37-
AnalysisDriverModelUriResolver(analysisDriverModel),
36+
analysisDriverModel.filesystem,
3837
],
3938
);
4039
}
Lines changed: 169 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,169 @@
1+
// Copyright (c) 2025, the Dart project authors. Please see the AUTHORS file
2+
// for details. All rights reserved. Use of this source code is governed by a
3+
// BSD-style license that can be found in the LICENSE file.
4+
5+
import 'dart:convert';
6+
import 'dart:typed_data';
7+
8+
import 'package:analyzer/file_system/file_system.dart';
9+
import 'package:analyzer/source/file_source.dart';
10+
// ignore: implementation_imports
11+
import 'package:analyzer/src/clients/build_resolvers/build_resolvers.dart';
12+
import 'package:build/build.dart' hide Resource;
13+
import 'package:path/path.dart' as p;
14+
15+
/// The in-memory filesystem that is the analyzer's view of the build.
16+
///
17+
/// Tracks modified paths, which should be passed to
18+
/// `AnalysisDriver.changeFile` to update the analyzer state.
19+
class AnalysisDriverFilesystem implements UriResolver, ResourceProvider {
20+
final Map<String, String> _data = {};
21+
final Set<String> _changedPaths = {};
22+
23+
// Methods for use by `AnalysisDriverModel`.
24+
25+
/// Whether [path] exists.
26+
bool exists(String path) => _data.containsKey(path);
27+
28+
/// Reads the data previously written to [path].
29+
///
30+
/// Throws if ![exists].
31+
String read(String path) => _data[path]!;
32+
33+
/// Deletes the data previously written to [path].
34+
///
35+
/// Records the change in [changedPaths].
36+
///
37+
/// Or, if it's missing, does nothing.
38+
void deleteFile(String path) {
39+
if (_data.remove(path) != null) _changedPaths.add(path);
40+
}
41+
42+
/// Writes [content] to [path].
43+
///
44+
/// Records the change in [changedPaths], only if the content actually
45+
/// changed.
46+
bool writeFile(String path, String content) {
47+
final oldContent = _data[path];
48+
_data[path] = content;
49+
if (content != oldContent) _changedPaths.add(path);
50+
return oldContent != content;
51+
}
52+
53+
/// Paths that were modified by [deleteFile] or [writeFile] since the last
54+
/// call to [clearChangedPaths].
55+
Iterable<String> get changedPaths => _changedPaths;
56+
57+
/// Clears [changedPaths].
58+
void clearChangedPaths() => _changedPaths.clear();
59+
60+
// `UriResolver` methods.
61+
62+
@override
63+
Uri pathToUri(String path) {
64+
var pathSegments = p.posix.split(path);
65+
var packageName = pathSegments[1];
66+
if (pathSegments[2] == 'lib') {
67+
return Uri(
68+
scheme: 'package',
69+
pathSegments: [packageName].followedBy(pathSegments.skip(3)),
70+
);
71+
} else {
72+
return Uri(
73+
scheme: 'asset',
74+
pathSegments: [packageName].followedBy(pathSegments.skip(2)),
75+
);
76+
}
77+
}
78+
79+
@override
80+
Source? resolveAbsolute(Uri uri, [Uri? actualUri]) {
81+
final assetId = parseAsset(uri);
82+
if (assetId == null) return null;
83+
84+
var file = getFile(assetPath(assetId));
85+
return FileSource(file, assetId.uri);
86+
}
87+
88+
/// Path of [assetId] for the in-memory filesystem.
89+
static String assetPath(AssetId assetId) =>
90+
p.posix.join('/${assetId.package}', assetId.path);
91+
92+
/// Attempts to parse [uri] into an [AssetId].
93+
///
94+
/// Handles 'package:' or 'asset:' URIs, as well as 'file:' URIs that have the
95+
/// same pattern used by [assetPath].
96+
///
97+
/// Returns null if the Uri cannot be parsed.
98+
static AssetId? parseAsset(Uri uri) {
99+
if (const ['dart', 'dart-ext'].any(uri.isScheme)) return null;
100+
if (uri.isScheme('package') || uri.isScheme('asset')) {
101+
return AssetId.resolve(uri);
102+
}
103+
if (uri.isScheme('file')) {
104+
final parts = p.split(uri.path);
105+
return AssetId(parts[1], p.posix.joinAll(parts.skip(2)));
106+
}
107+
return null;
108+
}
109+
110+
// `ResourceProvider` methods.
111+
112+
@override
113+
p.Context get pathContext => p.posix;
114+
115+
@override
116+
File getFile(String path) => _Resource(this, path);
117+
118+
@override
119+
Folder getFolder(String path) => _Resource(this, path);
120+
121+
// `ResourceProvider` methods that are not needed.
122+
123+
@override
124+
Link getLink(String path) => throw UnimplementedError();
125+
126+
@override
127+
Resource getResource(String path) => throw UnimplementedError();
128+
129+
@override
130+
Folder? getStateLocation(String pluginId) => throw UnimplementedError();
131+
}
132+
133+
/// Minimal implementation of [File] and [Folder].
134+
///
135+
/// Provides only what the analyzer actually uses during analysis.
136+
class _Resource implements File, Folder {
137+
final AnalysisDriverFilesystem filesystem;
138+
@override
139+
final String path;
140+
141+
_Resource(this.filesystem, this.path);
142+
143+
@override
144+
bool get exists => filesystem.exists(path);
145+
146+
@override
147+
String get shortName => filesystem.pathContext.basename(path);
148+
149+
@override
150+
Uint8List readAsBytesSync() {
151+
// TODO(davidmorgan): the analyzer reads as bytes in `FileContentCache`
152+
// then converts back to `String` and hashes. It should be possible to save
153+
// that work, for exampleby injecting a custom `FileContentCache`.
154+
return utf8.encode(filesystem.read(path));
155+
}
156+
157+
@override
158+
String readAsStringSync() => filesystem.read(path);
159+
160+
// Most `File` and/or `Folder` methods are not needed.
161+
162+
@override
163+
dynamic noSuchMethod(Invocation invocation) => super.noSuchMethod(invocation);
164+
165+
// Needs an explicit override to satisfy both `File.copyTo` and
166+
// `Folder.copyTo`.
167+
@override
168+
_Resource copyTo(Folder _) => throw UnimplementedError();
169+
}

build_resolvers/lib/src/analysis_driver_model.dart

Lines changed: 21 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -7,13 +7,11 @@ import 'dart:collection';
77

88
import 'package:analyzer/dart/analysis/utilities.dart';
99
import 'package:analyzer/dart/ast/ast.dart';
10-
import 'package:analyzer/file_system/memory_file_system.dart';
1110
// ignore: implementation_imports
1211
import 'package:analyzer/src/clients/build_resolvers/build_resolvers.dart';
1312
import 'package:build/build.dart';
14-
import 'package:path/path.dart' as p;
1513

16-
import 'analysis_driver_model_uri_resolver.dart';
14+
import 'analysis_driver_filesystem.dart';
1715

1816
/// Manages analysis driver and related build state.
1917
///
@@ -31,15 +29,14 @@ import 'analysis_driver_model_uri_resolver.dart';
3129
/// implementation.
3230
class AnalysisDriverModel {
3331
/// In-memory filesystem for the analyzer.
34-
final MemoryResourceProvider resourceProvider =
35-
MemoryResourceProvider(context: p.posix);
32+
final AnalysisDriverFilesystem filesystem = AnalysisDriverFilesystem();
3633

3734
/// The import graph of all sources needed for analysis.
3835
final _graph = _Graph();
3936

4037
/// Assets that have been synced into the in-memory filesystem
41-
/// [resourceProvider].
42-
final _syncedOntoResourceProvider = <AssetId>{};
38+
/// [filesystem].
39+
final _syncedOntoFilesystem = <AssetId>{};
4340

4441
/// Notifies that [step] has completed.
4542
///
@@ -51,7 +48,7 @@ class AnalysisDriverModel {
5148
/// Clear cached information specific to an individual build.
5249
void reset() {
5350
_graph.clear();
54-
_syncedOntoResourceProvider.clear();
51+
_syncedOntoFilesystem.clear();
5552
}
5653

5754
/// Attempts to parse [uri] into an [AssetId] and returns it if it is cached.
@@ -61,16 +58,16 @@ class AnalysisDriverModel {
6158
///
6259
/// Returns null if the `Uri` cannot be parsed or is not cached.
6360
AssetId? lookupCachedAsset(Uri uri) {
64-
final assetId = AnalysisDriverModelUriResolver.parseAsset(uri);
61+
final assetId = AnalysisDriverFilesystem.parseAsset(uri);
6562
// TODO(davidmorgan): not clear if this is the right "exists" check.
66-
if (assetId == null || !resourceProvider.getFile(assetId.asPath).exists) {
63+
if (assetId == null || !filesystem.getFile(assetId.asPath).exists) {
6764
return null;
6865
}
6966

7067
return assetId;
7168
}
7269

73-
/// Updates [resourceProvider] and the analysis driver given by
70+
/// Updates [filesystem] and the analysis driver given by
7471
/// `withDriverResource` with updated versions of [entryPoints].
7572
///
7673
/// If [transitive], then all the transitive imports from [entryPoints] are
@@ -107,14 +104,14 @@ class AnalysisDriverModel {
107104
FutureOr<void> Function(AnalysisDriverForPackageBuild))
108105
withDriverResource,
109106
{required bool transitive}) async {
110-
var idsToSyncOntoResourceProvider = entryPoints;
107+
var idsToSyncOntoFilesystem = entryPoints;
111108
Iterable<AssetId> inputIds = entryPoints;
112109

113110
// If requested, find transitive imports.
114111
if (transitive) {
115112
final previouslyMissingFiles = await _graph.load(buildStep, entryPoints);
116-
_syncedOntoResourceProvider.removeAll(previouslyMissingFiles);
117-
idsToSyncOntoResourceProvider = _graph.nodes.keys.toList();
113+
_syncedOntoFilesystem.removeAll(previouslyMissingFiles);
114+
idsToSyncOntoFilesystem = _graph.nodes.keys.toList();
118115
inputIds = _graph.inputsFor(entryPoints);
119116
}
120117

@@ -124,39 +121,23 @@ class AnalysisDriverModel {
124121
}
125122

126123
// Sync changes onto the "URI resolver", the in-memory filesystem.
127-
final changedIds = <AssetId>[];
128-
for (final id in idsToSyncOntoResourceProvider) {
129-
if (!_syncedOntoResourceProvider.add(id)) continue;
124+
for (final id in idsToSyncOntoFilesystem) {
125+
if (!_syncedOntoFilesystem.add(id)) continue;
130126
final content =
131127
await buildStep.canRead(id) ? await buildStep.readAsString(id) : null;
132-
final inMemoryFile = resourceProvider.getFile(id.asPath);
133-
final inMemoryContent =
134-
inMemoryFile.exists ? inMemoryFile.readAsStringSync() : null;
135-
136-
if (content != inMemoryContent) {
137-
if (content == null) {
138-
// TODO(davidmorgan): per "globallySeenAssets" in
139-
// BuildAssetUriResolver, deletes should only be applied at the end
140-
// of the build, in case the file is actually there but not visible
141-
// to the current reader.
142-
resourceProvider.deleteFile(id.asPath);
143-
changedIds.add(id);
144-
} else {
145-
if (inMemoryContent == null) {
146-
resourceProvider.newFile(id.asPath, content);
147-
} else {
148-
resourceProvider.modifyFile(id.asPath, content);
149-
}
150-
changedIds.add(id);
151-
}
128+
if (content == null) {
129+
filesystem.deleteFile(id.asPath);
130+
} else {
131+
filesystem.writeFile(id.asPath, content);
152132
}
153133
}
154134

155135
// Notify the analyzer of changes and wait for it to update its internal
156136
// state.
157-
for (final id in changedIds) {
158-
driver.changeFile(id.asPath);
137+
for (final path in filesystem.changedPaths) {
138+
driver.changeFile(path);
159139
}
140+
filesystem.clearChangedPaths();
160141
await driver.applyPendingFileChanges();
161142
}
162143
}
@@ -181,7 +162,7 @@ List<AssetId> _parseDependencies(String content, AssetId from) =>
181162

182163
extension _AssetIdExtensions on AssetId {
183164
/// Asset path for the in-memory filesystem.
184-
String get asPath => AnalysisDriverModelUriResolver.assetPath(this);
165+
String get asPath => AnalysisDriverFilesystem.assetPath(this);
185166
}
186167

187168
/// The directive graph of all known sources.

0 commit comments

Comments
 (0)