|
| 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 | +} |
0 commit comments