blob: 055b89403c7b144b599da702cd746d3e3b2fc3ad [file] [log] [blame]
// Copyright (c) 2020, the Dart project authors. Please see the AUTHORS file
// for details. All rights reserved. Use of this source code is governed by a
// BSD-style license that can be found in the LICENSE file.
import 'dart:typed_data';
import 'package:analyzer/dart/analysis/declared_variables.dart';
import 'package:analyzer/dart/analysis/results.dart';
import 'package:analyzer/dart/element/element.dart';
import 'package:analyzer/error/error.dart';
import 'package:analyzer/file_system/file_system.dart';
import 'package:analyzer/source/line_info.dart';
import 'package:analyzer/src/analysis_options/analysis_options_provider.dart';
import 'package:analyzer/src/context/packages.dart';
import 'package:analyzer/src/dart/analysis/byte_store.dart';
import 'package:analyzer/src/dart/analysis/cache.dart';
import 'package:analyzer/src/dart/analysis/context_root.dart';
import 'package:analyzer/src/dart/analysis/driver.dart' show ErrorEncoding;
import 'package:analyzer/src/dart/analysis/experiments.dart';
import 'package:analyzer/src/dart/analysis/feature_set_provider.dart';
import 'package:analyzer/src/dart/analysis/file_state.dart';
import 'package:analyzer/src/dart/analysis/library_analyzer.dart';
import 'package:analyzer/src/dart/analysis/library_context.dart';
import 'package:analyzer/src/dart/analysis/performance_logger.dart';
import 'package:analyzer/src/dart/analysis/results.dart';
import 'package:analyzer/src/dart/analysis/search.dart';
import 'package:analyzer/src/dart/micro/analysis_context.dart';
import 'package:analyzer/src/dart/micro/utils.dart';
import 'package:analyzer/src/generated/engine.dart' show AnalysisOptionsImpl;
import 'package:analyzer/src/generated/source.dart';
import 'package:analyzer/src/summary/api_signature.dart';
import 'package:analyzer/src/summary/format.dart';
import 'package:analyzer/src/summary/idl.dart';
import 'package:analyzer/src/summary/package_bundle_reader.dart';
import 'package:analyzer/src/task/options.dart';
import 'package:analyzer/src/util/performance/operation_performance.dart';
import 'package:analyzer/src/utilities/extensions/file_system.dart';
import 'package:analyzer/src/workspace/workspace.dart';
import 'package:collection/collection.dart';
import 'package:meta/meta.dart';
import 'package:yaml/yaml.dart';
class CiderFileContent implements FileContent {
final CiderFileContentStrategy strategy;
final String path;
final String digestStr;
CiderFileContent({
required this.strategy,
required this.path,
required this.digestStr,
});
@override
String get content {
final contentWithDigest = _getContent();
if (contentWithDigest.digestStr != digestStr) {
throw StateError('File was changed, but not invalidated: $path');
}
return contentWithDigest.content;
}
@override
String get contentHash => digestStr;
@override
bool get exists => digestStr.isNotEmpty;
_ContentWithDigest _getContent() {
String content;
try {
final file = strategy.resourceProvider.getFile(path);
content = file.readAsStringSync();
} catch (_) {
content = '';
}
final digestStr = strategy.getFileDigest(path);
return _ContentWithDigest(
content: content,
digestStr: digestStr,
);
}
}
class CiderFileContentStrategy implements FileContentStrategy {
final ResourceProvider resourceProvider;
/// A function that returns the digest for a file as a String. The function
/// returns a non null value, returns an empty string if file does
/// not exist/has no contents.
final String Function(String path) getFileDigest;
CiderFileContentStrategy({
required this.resourceProvider,
required this.getFileDigest,
});
@override
CiderFileContent get(String path) {
final digestStr = getFileDigest(path);
return CiderFileContent(
strategy: this,
path: path,
digestStr: digestStr,
);
}
}
class CiderSearchInfo {
final CharacterLocation startPosition;
final int length;
final MatchKind kind;
CiderSearchInfo(this.startPosition, this.length, this.kind);
@override
bool operator ==(Object other) =>
other is CiderSearchInfo &&
startPosition == other.startPosition &&
length == other.length &&
kind == other.kind;
}
class CiderSearchMatch {
final String path;
final List<CiderSearchInfo> references;
CiderSearchMatch(this.path, this.references);
@override
bool operator ==(Object other) =>
other is CiderSearchMatch &&
path == other.path &&
const ListEquality<CiderSearchInfo>()
.equals(references, other.references);
@override
String toString() {
return '($path, $references)';
}
}
class FileContext {
final AnalysisOptionsImpl analysisOptions;
final FileState file;
FileContext(this.analysisOptions, this.file);
}
class FileResolver {
final PerformanceLog logger;
final ResourceProvider resourceProvider;
ByteStore byteStore;
final SourceFactory sourceFactory;
/// A function that returns the digest for a file as a String. The function
/// returns a non null value, can return an empty string if file does
/// not exist/has no contents.
final String Function(String path) getFileDigest;
/// A function that returns true if the given file path is likely to be that
/// of a file that is generated.
final bool Function(String path) isGenerated;
/// A function that fetches the given list of files. This function can be used
/// to batch file reads in systems where file fetches are expensive.
final void Function(List<String> paths)? prefetchFiles;
final Workspace workspace;
/// This field gets value only during testing.
final FileResolverTestData? testData;
FileSystemState? fsState;
MicroContextObjects? contextObjects;
LibraryContext? libraryContext;
/// List of keys for cache elements that are invalidated. Track elements that
/// are invalidated during [changeFiles]. Used in [releaseAndClearRemovedIds]
/// to release the cache items and is then cleared.
final Set<String> removedCacheKeys = {};
/// The cache of file results, cleared on [changeFiles].
///
/// It is used to allow assists and fixes without resolving the same file
/// multiple times, as we compute more than one assist, or fixes when there
/// are more than one error on a line.
@visibleForTesting
final Map<String, ResolvedLibraryResult> cachedResults = {};
/// The cache of error results.
final Cache<String, Uint8List> _errorResultsCache =
Cache(128 * 1024, (bytes) => bytes.length);
FileResolver({
required this.logger,
required this.resourceProvider,
required this.sourceFactory,
required this.getFileDigest,
required this.prefetchFiles,
required this.workspace,
required this.isGenerated,
required this.byteStore,
this.testData,
});
/// Update the resolver to reflect the fact that the files with the given
/// [paths] were changed. For each specified file we need to make sure that
/// when the file, of any file that directly or indirectly referenced it,
/// is resolved, we use the new state of the file.
void changeFiles(List<String> paths) {
if (fsState == null) {
return;
}
// Forget all results, anything is potentially affected.
cachedResults.clear();
// Remove the specified files and files that transitively depend on it.
final removedFiles = <FileState>{};
for (final path in paths) {
fsState!.changeFile(path, removedFiles);
}
// Schedule disposing references to cached unlinked data.
for (final removedFile in removedFiles) {
removedCacheKeys.add(removedFile.unlinkedKey);
}
// Remove libraries represented by removed files.
// If we need these libraries later, we will relink and reattach them.
libraryContext?.remove(removedFiles, removedCacheKeys);
releaseAndClearRemovedIds();
}
/// Collects all the cached artifacts and add all the cache id's for the
/// removed artifacts to [removedCacheKeys].
@Deprecated('Use dispose() instead')
void collectSharedDataIdentifiers() {
removedCacheKeys.addAll(fsState!.dispose());
removedCacheKeys.addAll(libraryContext!.dispose());
}
/// Notifies this object that it is about to be discarded, so it should
/// release any shared data.
void dispose() {
removedCacheKeys.addAll(fsState!.dispose());
removedCacheKeys.addAll(libraryContext!.dispose());
releaseAndClearRemovedIds();
}
/// Looks for references to the given Element. All the files currently
/// cached by the resolver are searched, generated files are ignored.
Future<List<CiderSearchMatch>> findReferences2(Element element,
{OperationPerformanceImpl? performance}) async {
return logger.runAsync('findReferences for ${element.name}', () async {
var references = <CiderSearchMatch>[];
Future<void> collectReferences2(
String path, OperationPerformanceImpl performance) async {
await performance.runAsync('collectReferences', (_) async {
var resolved = await resolve2(path: path);
var collector = ReferencesCollector(element);
resolved.unit.accept(collector);
var matches = collector.references;
if (matches.isNotEmpty) {
var lineInfo = resolved.unit.lineInfo;
references.add(CiderSearchMatch(
path,
matches
.map((match) => CiderSearchInfo(
lineInfo.getLocation(match.offset),
match.length,
match.matchKind))
.toList()));
}
});
}
performance ??= OperationPerformanceImpl('<default>');
// TODO(keertip): check if element is named constructor.
if (element is LocalVariableElement ||
(element is ParameterElement && !element.isNamed)) {
await collectReferences2(element.source!.fullName, performance!);
} else if (element is ImportElement) {
return await _searchReferences_Import(element);
} else {
var result = performance!.run('getFilesContaining', (performance) {
return fsState!.getFilesContaining(element.displayName);
});
for (var filePath in result) {
await collectReferences2(filePath, performance!);
}
}
return references;
});
}
Future<ErrorsResult> getErrors2({
required String path,
OperationPerformanceImpl? performance,
}) async {
_throwIfNotAbsoluteNormalizedPath(path);
performance ??= OperationPerformanceImpl('<default>');
return logger.runAsync('Get errors for $path', () async {
var fileContext = getFileContext(
path: path,
performance: performance!,
);
var file = fileContext.file;
// TODO(scheglov) Casts are unsafe.
final kind = file.kind as LibraryFileStateKind;
final errorsSignatureBuilder = ApiSignature();
errorsSignatureBuilder.addString(kind.libraryCycle.apiSignature);
errorsSignatureBuilder.addString(file.contentHash);
final errorsKey = '${errorsSignatureBuilder.toHex()}.errors';
final List<AnalysisError> errors;
final bytes = _errorResultsCache.get(errorsKey);
if (bytes != null) {
var data = CiderUnitErrors.fromBuffer(bytes);
errors = data.errors.map((error) {
return ErrorEncoding.decode(file.source, error)!;
}).toList();
} else {
var unitResult = await resolve2(
path: path,
performance: performance,
);
errors = unitResult.errors;
_errorResultsCache.put(
errorsKey,
CiderUnitErrorsBuilder(
errors: errors.map(ErrorEncoding.encode).toList(),
).toBuffer(),
);
}
return ErrorsResultImpl(
contextObjects!.analysisSession,
path,
file.uri,
file.lineInfo,
false, // isPart
errors,
);
});
}
FileContext getFileContext({
required String path,
required OperationPerformanceImpl performance,
}) {
return performance.run('fileContext', (performance) {
var analysisOptions = performance.run('analysisOptions', (performance) {
return _getAnalysisOptions(
path: path,
performance: performance,
);
});
performance.run('createContext', (_) {
_createContext(path, analysisOptions);
});
var file = performance.run('fileForPath', (performance) {
return fsState!.getFileForPath2(
path: path,
performance: performance,
);
});
return FileContext(analysisOptions, file);
});
}
/// Return files that have a top-level declaration with the [name].
List<FileState> getFilesWithTopLevelDeclarations(String name) {
final fsState = this.fsState;
if (fsState == null) {
return const [];
}
return fsState.getFilesWithTopLevelDeclarations(name);
}
Future<LibraryElement> getLibraryByUri2({
required String uriStr,
OperationPerformanceImpl? performance,
}) async {
performance ??= OperationPerformanceImpl('<default>');
var uri = Uri.parse(uriStr);
var path = sourceFactory.forUri2(uri)?.fullName;
if (path == null) {
throw ArgumentError('$uri cannot be resolved to a file.');
}
var fileContext = getFileContext(
path: path,
performance: performance,
);
var file = fileContext.file;
final kind = file.kind;
if (kind is! LibraryFileStateKind) {
throw ArgumentError('$uri is not a library.');
}
await performance.runAsync('libraryContext', (performance) async {
await libraryContext!.load(
targetLibrary: kind,
performance: performance,
);
});
return libraryContext!.elementFactory.libraryOfUri2(uri);
}
String getLibraryLinkedSignature({
required String path,
required OperationPerformanceImpl performance,
}) {
_throwIfNotAbsoluteNormalizedPath(path);
var file = fsState!.getFileForPath2(
path: path,
performance: performance,
);
// TODO(scheglov) Casts are unsafe.
final kind = file.kind as LibraryFileStateKind;
return kind.libraryCycle.apiSignature;
}
/// Ensure that libraries necessary for resolving [path] are linked.
///
/// Libraries are linked in library cycles, from the bottom to top, so that
/// when we link a cycle, everything it transitively depends is ready. We
/// load newly linked libraries from bytes, and when we link a new library
/// cycle we partially resynthesize AST and elements from previously
/// loaded libraries.
///
/// But when we are done linking libraries, and want to resolve just the
/// very top library that transitively depends on the whole dependency
/// tree, this library will not reference as many elements in the
/// dependencies as we needed for linking. Most probably it references
/// elements from directly imported libraries, and a couple of layers below.
/// So, keeping all previously resynthesized data is usually a waste.
///
/// This method ensures that we discard the libraries context, with all its
/// partially resynthesized data, and so prepare for loading linked summaries
/// from bytes, which will be done by [getErrors2]. It is OK for it to
/// spend some more time on this.
Future<void> linkLibraries2({
required String path,
}) async {
_throwIfNotAbsoluteNormalizedPath(path);
var performance = OperationPerformanceImpl('<unused>');
var fileContext = getFileContext(
path: path,
performance: performance,
);
var file = fileContext.file;
final libraryKind = file.kind.library ?? file.kind.asLibrary;
// Load the library, link if necessary.
await libraryContext!.load(
targetLibrary: libraryKind,
performance: performance,
);
// Unload libraries, but don't release the linked data.
// If we are the only consumer of it, we will lose it.
final linkedKeysToRelease = libraryContext!.unloadAll();
// Load the library again, the reference count is `>= 2`.
await libraryContext!.load(
targetLibrary: libraryKind,
performance: performance,
);
// Release the linked data, the reference count is `>= 1`.
byteStore.release(linkedKeysToRelease);
}
/// Releases from the cache and clear [removedCacheKeys].
void releaseAndClearRemovedIds() {
byteStore.release(removedCacheKeys);
removedCacheKeys.clear();
}
/// Remove cached [FileState]'s that were not used in the current analysis
/// session. The list of files analyzed is used to compute the set of unused
/// [FileState]'s. Adds the cache id's for the removed [FileState]'s to
/// [removedCacheKeys].
void removeFilesNotNecessaryForAnalysisOf(List<String> files) {
var removedFiles = fsState!.removeUnusedFiles(files);
for (var removedFile in removedFiles) {
removedCacheKeys.add(removedFile.unlinkedKey);
}
libraryContext?.remove(removedFiles, removedCacheKeys);
releaseAndClearRemovedIds();
}
Future<ResolvedUnitResult> resolve2({
required String path,
OperationPerformanceImpl? performance,
}) async {
_throwIfNotAbsoluteNormalizedPath(path);
performance ??= OperationPerformanceImpl('<default>');
return logger.runAsync('Resolve $path', () async {
var fileContext = getFileContext(
path: path,
performance: performance!,
);
var file = fileContext.file;
final libraryKind = file.kind.library ?? file.kind.asLibrary;
final libraryFile = libraryKind.file;
var libraryResult = await resolveLibrary2(
path: libraryFile.path,
performance: performance,
);
return libraryResult.units.firstWhere(
(unitResult) => unitResult.path == path,
);
});
}
/// The [completionLine] and [completionColumn] are zero based.
Future<ResolvedForCompletionResultImpl> resolveForCompletion({
required int completionLine,
required int completionColumn,
required String path,
OperationPerformanceImpl? performance,
}) async {
_throwIfNotAbsoluteNormalizedPath(path);
performance ??= OperationPerformanceImpl('<default>');
return logger.runAsync('Resolve $path', () async {
final fileContext = getFileContext(
path: path,
performance: performance!,
);
final file = fileContext.file;
final libraryKind = file.kind.library ?? file.kind.asLibrary;
final lineOffset = file.lineInfo.getOffsetOfLine(completionLine);
final completionOffset = lineOffset + completionColumn;
await performance.runAsync('libraryContext', (performance) async {
await libraryContext!.load(
targetLibrary: libraryKind,
performance: performance,
);
});
final unitElement = libraryContext!.computeUnitElement(libraryKind, file);
return logger.run('Compute analysis results', () {
final elementFactory = libraryContext!.elementFactory;
final analysisSession = elementFactory.analysisSession;
var libraryAnalyzer = LibraryAnalyzer(
fileContext.analysisOptions,
contextObjects!.declaredVariables,
elementFactory.libraryOfUri2(libraryKind.file.uri),
analysisSession.inheritanceManager,
libraryKind,
);
final analysisResult = performance!.run('analyze', (performance) {
return libraryAnalyzer.analyzeForCompletion(
file: file,
offset: completionOffset,
unitElement: unitElement,
performance: performance,
);
});
return ResolvedForCompletionResultImpl(
analysisSession: analysisSession,
path: path,
uri: file.uri,
exists: file.exists,
content: file.content,
lineInfo: file.lineInfo,
parsedUnit: analysisResult.parsedUnit,
unitElement: unitElement,
resolvedNodes: analysisResult.resolvedNodes,
);
});
});
}
Future<ResolvedLibraryResult> resolveLibrary2({
required String path,
OperationPerformanceImpl? performance,
}) async {
_throwIfNotAbsoluteNormalizedPath(path);
performance ??= OperationPerformanceImpl('<default>');
var cachedResult = cachedResults[path];
if (cachedResult != null) {
return cachedResult;
}
return logger.runAsync('Resolve $path', () async {
var fileContext = getFileContext(
path: path,
performance: performance!,
);
var file = fileContext.file;
final libraryKind = file.kind.library ?? file.kind.asLibrary;
await performance.runAsync('libraryContext', (performance) async {
await libraryContext!.load(
targetLibrary: libraryKind,
performance: performance,
);
});
testData?.addResolvedLibrary(path);
late List<UnitAnalysisResult> results;
logger.run('Compute analysis results', () {
var libraryAnalyzer = LibraryAnalyzer(
fileContext.analysisOptions,
contextObjects!.declaredVariables,
libraryContext!.elementFactory.libraryOfUri2(libraryKind.file.uri),
libraryContext!.elementFactory.analysisSession.inheritanceManager,
libraryKind,
);
results = performance!.run('analyze', (performance) {
return libraryAnalyzer.analyze();
});
});
var resolvedUnits = results.map((fileResult) {
var file = fileResult.file;
return ResolvedUnitResultImpl(
contextObjects!.analysisSession,
file.path,
file.uri,
file.exists,
file.content,
file.lineInfo,
file.isPart,
fileResult.unit,
fileResult.errors,
);
}).toList();
var libraryUnit = resolvedUnits.first;
var result = ResolvedLibraryResultImpl(contextObjects!.analysisSession,
libraryUnit.libraryElement, resolvedUnits);
cachedResults[path] = result;
return result;
});
}
/// Make sure that [fsState], [contextObjects], and [libraryContext] are
/// created and configured with the given [fileAnalysisOptions].
///
/// The [fsState] is not affected by [fileAnalysisOptions].
///
/// The [fileAnalysisOptions] only affect reported diagnostics, but not
/// elements and types. So, we really need to reconfigure only when we are
/// going to resolve some files using these new options.
///
/// Specifically, "implicit casts" and "strict inference" affect the type
/// system. And there are lints that are enabled for one package, but not
/// for another.
void _createContext(String path, AnalysisOptionsImpl fileAnalysisOptions) {
if (contextObjects != null) {
libraryContext!.analysisContext.analysisOptions = fileAnalysisOptions;
return;
}
var analysisOptions = AnalysisOptionsImpl()
..implicitCasts = fileAnalysisOptions.implicitCasts
..strictInference = fileAnalysisOptions.strictInference;
if (fsState == null) {
var featureSetProvider = FeatureSetProvider.build(
sourceFactory: sourceFactory,
resourceProvider: resourceProvider,
packages: Packages.empty,
packageDefaultFeatureSet: analysisOptions.contextFeatures,
nonPackageDefaultLanguageVersion: ExperimentStatus.currentVersion,
nonPackageDefaultFeatureSet: analysisOptions.nonPackageFeatureSet,
);
fsState = FileSystemState(
logger,
byteStore,
resourceProvider,
'contextName',
sourceFactory,
workspace,
DeclaredVariables.fromMap({}),
Uint32List(0), // _saltForUnlinked
Uint32List(0), // _saltForElements
featureSetProvider,
fileContentStrategy: CiderFileContentStrategy(
resourceProvider: resourceProvider,
getFileDigest: getFileDigest,
),
prefetchFiles: prefetchFiles,
isGenerated: isGenerated,
testData: testData?.fileSystem,
);
}
if (contextObjects == null) {
var rootFolder = resourceProvider.getFolder(workspace.root);
var root = ContextRootImpl(resourceProvider, rootFolder, workspace);
root.included.add(rootFolder);
contextObjects = createMicroContextObjects(
fileResolver: this,
analysisOptions: analysisOptions,
sourceFactory: sourceFactory,
root: root,
resourceProvider: resourceProvider,
);
libraryContext = LibraryContext(
declaredVariables: contextObjects!.declaredVariables,
byteStore: byteStore,
analysisOptions: contextObjects!.analysisOptions,
analysisSession: contextObjects!.analysisSession,
logger: logger,
fileSystemState: fsState!,
sourceFactory: sourceFactory,
externalSummaries: SummaryDataStore(),
macroExecutor: null,
macroKernelBuilder: null,
testData: testData?.libraryContext,
);
contextObjects!.analysisSession.elementFactory =
libraryContext!.elementFactory;
}
}
/// Return the analysis options.
///
/// If the [path] is not `null`, read it.
///
/// If the [workspace] is a [WorkspaceWithDefaultAnalysisOptions], get the
/// default options, if the file exists.
///
/// Otherwise, return the default options.
AnalysisOptionsImpl _getAnalysisOptions({
required String path,
required OperationPerformanceImpl performance,
}) {
YamlMap? optionMap;
var separator = resourceProvider.pathContext.separator;
var isThirdParty = path
.contains('${separator}third_party${separator}dart$separator') ||
path.contains('${separator}third_party${separator}dart_lang$separator');
File? optionsFile;
if (!isThirdParty) {
optionsFile = performance.run('findAnalysisOptionsYamlFile', (_) {
var folder = resourceProvider.getFile(path).parent;
return folder.findAnalysisOptionsYamlFile();
});
}
if (optionsFile != null) {
performance.run('getOptionsFromFile', (_) {
try {
var optionsProvider = AnalysisOptionsProvider(sourceFactory);
optionMap = optionsProvider.getOptionsFromFile(optionsFile!);
} catch (_) {}
});
} else {
var source = performance.run('defaultOptions', (_) {
if (workspace is WorkspaceWithDefaultAnalysisOptions) {
if (isThirdParty) {
return sourceFactory.forUri(
WorkspaceWithDefaultAnalysisOptions.thirdPartyUri,
);
} else {
return sourceFactory.forUri(
WorkspaceWithDefaultAnalysisOptions.uri,
);
}
}
return null;
});
if (source != null && source.exists()) {
performance.run('getOptionsFromFile', (_) {
try {
var optionsProvider = AnalysisOptionsProvider(sourceFactory);
optionMap = optionsProvider.getOptionsFromSource(source);
} catch (_) {}
});
}
}
var options = AnalysisOptionsImpl();
if (optionMap != null) {
performance.run('applyToAnalysisOptions', (_) {
applyToAnalysisOptions(options, optionMap!);
});
}
if (isThirdParty) {
options.hint = false;
}
return options;
}
Future<List<CiderSearchMatch>> _searchReferences_Import(
ImportElement element) async {
var results = <CiderSearchMatch>[];
LibraryElement libraryElement = element.library;
for (CompilationUnitElement unitElement in libraryElement.units) {
String unitPath = unitElement.source.fullName;
var unitResult = await resolve2(path: unitPath);
var visitor = ImportElementReferencesVisitor(element, unitElement);
unitResult.unit.accept(visitor);
var lineInfo = unitResult.lineInfo;
var infos = visitor.results
.map((searchResult) => CiderSearchInfo(
lineInfo.getLocation(searchResult.offset),
searchResult.length,
MatchKind.REFERENCE))
.toList();
results.add(CiderSearchMatch(unitPath, infos));
}
return results;
}
void _throwIfNotAbsoluteNormalizedPath(String path) {
var pathContext = resourceProvider.pathContext;
if (pathContext.normalize(path) != path) {
throw ArgumentError(
'Only normalized paths are supported: $path',
);
}
}
}
class FileResolverTestData {
final fileSystem = FileSystemTestData();
late final libraryContext = LibraryContextTestData(
fileSystemTestData: fileSystem,
);
/// The paths of libraries which were resolved.
///
/// The library path is added every time when it is resolved.
final List<String> resolvedLibraries = [];
void addResolvedLibrary(String path) {
resolvedLibraries.add(path);
}
}
class _ContentWithDigest {
final String content;
final String digestStr;
_ContentWithDigest({
required this.content,
required this.digestStr,
});
}