Skip to content

Commit 7995cdd

Browse files
authored
Simpler in memory filesystem (#3823)
* 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. * Implement "Folder.contains". * Address review comments. * Address review comments.
1 parent b7a9a35 commit 7995cdd

7 files changed

+301
-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: 191 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,191 @@
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+
void writeFile(String path, String content) {
47+
final oldContent = _data[path];
48+
_data[path] = content;
49+
if (content != oldContent) _changedPaths.add(path);
50+
}
51+
52+
/// Paths that were modified by [deleteFile] or [writeFile] since the last
53+
/// call to [clearChangedPaths].
54+
Iterable<String> get changedPaths => _changedPaths;
55+
56+
/// Clears [changedPaths].
57+
void clearChangedPaths() => _changedPaths.clear();
58+
59+
// `UriResolver` methods.
60+
61+
/// Converts [path] to [Uri].
62+
///
63+
/// [path] must be absolute and matches one of two formats:
64+
///
65+
/// ```
66+
/// /<package>/lib/<rest> --> package:<package>/<rest>
67+
/// /<package>/<rest> --> asset:<package>/<rest>
68+
/// ```
69+
@override
70+
Uri pathToUri(String path) {
71+
if (!path.startsWith('/')) {
72+
throw ArgumentError.value('path', path, 'Must start with "/". ');
73+
}
74+
final pathSegments = path.split('/');
75+
// First segment is empty because of the starting `/`.
76+
final packageName = pathSegments[1];
77+
if (pathSegments[2] == 'lib') {
78+
return Uri(
79+
scheme: 'package',
80+
pathSegments: [packageName].followedBy(pathSegments.skip(3)),
81+
);
82+
} else {
83+
return Uri(
84+
scheme: 'asset',
85+
pathSegments: [packageName].followedBy(pathSegments.skip(2)),
86+
);
87+
}
88+
}
89+
90+
@override
91+
Source? resolveAbsolute(Uri uri, [Uri? actualUri]) {
92+
final assetId = parseAsset(uri);
93+
if (assetId == null) return null;
94+
95+
var file = getFile(assetPath(assetId));
96+
return FileSource(file, assetId.uri);
97+
}
98+
99+
/// Path of [assetId] for the in-memory filesystem.
100+
static String assetPath(AssetId assetId) =>
101+
'/${assetId.package}/${assetId.path}';
102+
103+
/// Attempts to parse [uri] into an [AssetId].
104+
///
105+
/// Handles 'package:' or 'asset:' URIs, as well as 'file:' URIs that have the
106+
/// same pattern used by [assetPath].
107+
///
108+
/// Returns null if the Uri cannot be parsed.
109+
static AssetId? parseAsset(Uri uri) {
110+
if (uri.isScheme('package') || uri.isScheme('asset')) {
111+
return AssetId.resolve(uri);
112+
}
113+
if (uri.isScheme('file')) {
114+
if (!uri.path.startsWith('/')) {
115+
throw ArgumentError.value(
116+
'uri.path', uri.path, 'Must start with "/". ');
117+
}
118+
final parts = uri.path.split('/');
119+
// First part is empty because of the starting `/`, second is package,
120+
// remainder is path in package.
121+
return AssetId(parts[1], parts.skip(2).join('/'));
122+
}
123+
return null;
124+
}
125+
126+
// `ResourceProvider` methods.
127+
128+
@override
129+
p.Context get pathContext => p.posix;
130+
131+
@override
132+
File getFile(String path) => _Resource(this, path);
133+
134+
@override
135+
Folder getFolder(String path) => _Resource(this, path);
136+
137+
// `ResourceProvider` methods that are not needed.
138+
139+
@override
140+
Link getLink(String path) => throw UnimplementedError();
141+
142+
@override
143+
Resource getResource(String path) => throw UnimplementedError();
144+
145+
@override
146+
Folder? getStateLocation(String pluginId) => throw UnimplementedError();
147+
}
148+
149+
/// Minimal implementation of [File] and [Folder].
150+
///
151+
/// Provides only what the analyzer actually uses during analysis.
152+
class _Resource implements File, Folder {
153+
final AnalysisDriverFilesystem filesystem;
154+
@override
155+
final String path;
156+
157+
_Resource(this.filesystem, this.path);
158+
159+
@override
160+
bool get exists => filesystem.exists(path);
161+
162+
@override
163+
String get shortName => filesystem.pathContext.basename(path);
164+
165+
@override
166+
Uint8List readAsBytesSync() {
167+
// TODO(davidmorgan): the analyzer reads as bytes in `FileContentCache`
168+
// then converts back to `String` and hashes. It should be possible to save
169+
// that work, for example by injecting a custom `FileContentCache`.
170+
return utf8.encode(filesystem.read(path));
171+
}
172+
173+
@override
174+
String readAsStringSync() => filesystem.read(path);
175+
176+
// `Folder` methods.
177+
178+
@override
179+
bool contains(String path) =>
180+
filesystem.pathContext.isWithin(this.path, path);
181+
182+
// Most `File` and/or `Folder` methods are not needed.
183+
184+
@override
185+
dynamic noSuchMethod(Invocation invocation) => super.noSuchMethod(invocation);
186+
187+
// Needs an explicit override to satisfy both `File.copyTo` and
188+
// `Folder.copyTo`.
189+
@override
190+
_Resource copyTo(Folder _) => throw UnimplementedError();
191+
}

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)