blob: 9fd787f0c4d49225183d2196313c5f05aae6da1f [file] [log] [blame]
// Copyright (c) 2016, 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:_fe_analyzer_shared/src/scanner/token_impl.dart'
show StringTokenImpl;
import 'package:analyzer/dart/analysis/declared_variables.dart';
import 'package:analyzer/dart/analysis/features.dart';
import 'package:analyzer/dart/ast/ast.dart';
import 'package:analyzer/dart/ast/token.dart';
import 'package:analyzer/dart/element/element.dart';
import 'package:analyzer/error/listener.dart';
import 'package:analyzer/file_system/file_system.dart';
import 'package:analyzer/src/dart/analysis/byte_store.dart';
import 'package:analyzer/src/dart/analysis/defined_names.dart';
import 'package:analyzer/src/dart/analysis/feature_set_provider.dart';
import 'package:analyzer/src/dart/analysis/file_content_cache.dart';
import 'package:analyzer/src/dart/analysis/library_graph.dart';
import 'package:analyzer/src/dart/analysis/performance_logger.dart';
import 'package:analyzer/src/dart/analysis/referenced_names.dart';
import 'package:analyzer/src/dart/analysis/unlinked_api_signature.dart';
import 'package:analyzer/src/dart/analysis/unlinked_data.dart';
import 'package:analyzer/src/dart/ast/ast.dart';
import 'package:analyzer/src/dart/scanner/reader.dart';
import 'package:analyzer/src/dart/scanner/scanner.dart';
import 'package:analyzer/src/exception/exception.dart';
import 'package:analyzer/src/generated/parser.dart';
import 'package:analyzer/src/generated/source.dart';
import 'package:analyzer/src/generated/utilities_dart.dart';
import 'package:analyzer/src/ignore_comments/ignore_info.dart';
import 'package:analyzer/src/source/source_resource.dart';
import 'package:analyzer/src/summary/api_signature.dart';
import 'package:analyzer/src/summary/package_bundle_reader.dart';
import 'package:analyzer/src/summary2/informative_data.dart';
import 'package:analyzer/src/util/either.dart';
import 'package:analyzer/src/util/file_paths.dart' as file_paths;
import 'package:analyzer/src/util/performance/operation_performance.dart';
import 'package:analyzer/src/util/uri.dart';
import 'package:analyzer/src/utilities/extensions/collection.dart';
import 'package:analyzer/src/workspace/workspace.dart';
import 'package:collection/collection.dart';
import 'package:convert/convert.dart';
import 'package:meta/meta.dart';
import 'package:path/path.dart' as package_path;
import 'package:pub_semver/pub_semver.dart';
/// The file has a `library augment` directive.
abstract class AugmentationFileStateKind extends LibraryOrAugmentationFileKind {
final UnlinkedLibraryAugmentationDirective directive;
AugmentationFileStateKind({
required super.file,
required this.directive,
});
@override
LibraryFileStateKind get asLibrary {
// TODO(scheglov): implement asLibrary
throw UnimplementedError();
}
/// Returns `true` if the `library augment` directive confirms [container].
bool isAugmentationOf(LibraryOrAugmentationFileKind container);
}
/// The URI of the [directive] can be resolved.
class AugmentationKnownFileStateKind extends AugmentationFileStateKind {
/// The file that is referenced by the [directive].
final FileState uriFile;
AugmentationKnownFileStateKind({
required super.file,
required super.directive,
required this.uriFile,
});
/// If the [uriFile] has `import augment` of this file, returns [uriFile].
/// Otherwise, this file is not a valid augmentation, returns `null`.
LibraryOrAugmentationFileKind? get augmented {
final uriKind = uriFile.kind;
if (uriKind is LibraryOrAugmentationFileKind) {
if (uriKind.hasAugmentation(this)) {
return uriKind;
}
}
return null;
}
@override
LibraryFileStateKind? get library {
final visited = Set<LibraryOrAugmentationFileKind>.identity();
var current = augmented;
while (current != null && visited.add(current)) {
if (current is LibraryFileStateKind) {
return current;
} else if (current is AugmentationKnownFileStateKind) {
current = current.augmented;
} else {
return null;
}
}
return null;
}
@override
bool isAugmentationOf(LibraryOrAugmentationFileKind container) {
return uriFile == container.file;
}
}
/// The URI of the [directive] can not be resolved.
class AugmentationUnknownFileStateKind extends AugmentationFileStateKind {
AugmentationUnknownFileStateKind({
required super.file,
required super.directive,
});
@override
LibraryFileStateKind? get library => null;
@override
bool isAugmentationOf(LibraryOrAugmentationFileKind container) => false;
}
/// Information about a directive that "includes" a file - `import`, `export`,
/// or `part`. But not `part of` or `library augment` - these are modelled as
/// kinds.
class DirectiveState {
void dispose() {}
}
/// Information about a single `import` directive.
class ExportDirectiveState extends DirectiveState {
final UnlinkedNamespaceDirective directive;
ExportDirectiveState({
required this.directive,
});
/// If [exportedSource] corresponds to a library, returns it.
Source? get exportedLibrarySource => null;
/// Returns a [Source] that is referenced by this directive. If the are
/// configurations, selects the one which satisfies the conditions.
///
/// Returns `null` if the selected URI is not valid, or cannot be resolved
/// into a [Source].
Source? get exportedSource => null;
}
/// [ExportDirectiveWithUri] that has a valid URI that references a file.
class ExportDirectiveWithFile extends ExportDirectiveWithUri {
final LibraryOrAugmentationFileKind container;
final FileState exportedFile;
ExportDirectiveWithFile({
required this.container,
required super.directive,
required this.exportedFile,
required super.selectedUriStr,
}) {
exportedFile.referencingFiles.add(container.file);
}
/// Returns [exportedFile] if it is a library.
LibraryFileStateKind? get exportedLibrary {
final kind = exportedFile.kind;
if (kind is LibraryFileStateKind) {
return kind;
}
return null;
}
@override
Source? get exportedLibrarySource {
if (exportedFile.kind is LibraryFileStateKind) {
return exportedSource;
}
return null;
}
@override
Source get exportedSource => exportedFile.source;
@override
void dispose() {
exportedFile.referencingFiles.remove(container.file);
}
}
/// [ExportDirectiveWithUri] with a URI that resolves to [InSummarySource].
class ExportDirectiveWithInSummarySource extends ExportDirectiveWithUri {
@override
final InSummarySource exportedSource;
ExportDirectiveWithInSummarySource({
required super.directive,
required this.exportedSource,
required super.selectedUriStr,
});
@override
Source? get exportedLibrarySource {
if (exportedSource.kind == InSummarySourceKind.library) {
return exportedSource;
} else {
return null;
}
}
}
/// [ExportDirectiveState] that has a valid URI string.
class ExportDirectiveWithUri extends ExportDirectiveState {
final String selectedUriStr;
ExportDirectiveWithUri({
required super.directive,
required this.selectedUriStr,
});
}
/// A library from [SummaryDataStore].
class ExternalLibrary {
final InSummarySource source;
ExternalLibrary._(this.source);
}
abstract class FileContent {
String get content;
String get contentHash;
bool get exists;
}
/// [FileContentOverlay] is used to temporary override content of files.
class FileContentOverlay {
final _map = <String, String>{};
/// Return the paths currently being overridden.
Iterable<String> get paths => _map.keys;
/// Return the content of the file with the given [path], or `null` the
/// overlay does not override the content of the file.
///
/// The [path] must be absolute and normalized.
String? operator [](String path) => _map[path];
/// Return the new [content] of the file with the given [path].
///
/// The [path] must be absolute and normalized.
void operator []=(String path, String? content) {
if (content == null) {
_map.remove(path);
} else {
_map[path] = content;
}
}
}
abstract class FileContentStrategy {
FileContent get(String path);
}
/// Information about a file being analyzed, explicitly or implicitly.
///
/// It provides a consistent view on its properties.
///
/// The properties are not guaranteed to represent the most recent state
/// of the file system. To update the file to the most recent state, [refresh]
/// should be called.
class FileState {
final FileSystemState _fsState;
/// The absolute path of the file.
final String path;
/// The absolute URI of the file.
final Uri uri;
/// Properties of the [uri].
final FileUriProperties uriProperties;
/// The [Source] of the file with the [uri].
final Source source;
/// The [WorkspacePackage] that contains this file.
///
/// It might be `null` if the file is outside of the workspace.
final WorkspacePackage? workspacePackage;
/// The [FeatureSet] for all files in the analysis context.
///
/// Usually it is the feature set of the latest language version, plus
/// possibly additional enabled experiments (from the analysis options file,
/// or from SDK allowed experiments).
///
/// This feature set is then restricted, with the [packageLanguageVersion],
/// or with a `@dart` language override token in the file header.
final FeatureSet _contextFeatureSet;
/// The language version for the package that contains this file.
final Version packageLanguageVersion;
FileContent? _fileContent;
LineInfo? _lineInfo;
Uint8List? _unlinkedSignature;
String? _unlinkedKey;
AnalysisDriverUnlinkedUnit? _driverUnlinkedUnit;
Uint8List? _apiSignature;
UnlinkedUnit? _unlinked2;
FileStateKind? _kind;
/// Files that reference this file.
final Set<FileState> referencingFiles = {};
List<FileState?>? _importedFiles;
List<FileState?>? _exportedFiles;
Set<FileState>? _directReferencedFiles;
/// The flag that shows whether the file has an error or warning that
/// might be fixed by a change to another file.
bool hasErrorOrWarning = false;
/// Set to `true` if this file contains code that might be executed by
/// a macro - declares a macro class itself, or is directly or indirectly
/// imported into a library that declares one.
bool mightBeExecutedByMacroClass = false;
FileState._(
this._fsState,
this.path,
this.uri,
this.source,
this.workspacePackage,
this._contextFeatureSet,
this.packageLanguageVersion,
) : uriProperties = FileUriProperties(uri);
/// The unlinked API signature of the file.
Uint8List get apiSignature => _apiSignature!;
/// The content of the file.
String get content => _fileContent!.content;
/// The MD5 hash of the [content].
String get contentHash => _fileContent!.contentHash;
/// The class member names defined by the file.
Set<String> get definedClassMemberNames {
return _driverUnlinkedUnit!.definedClassMemberNames;
}
/// The top-level names defined by the file.
Set<String> get definedTopLevelNames {
return _driverUnlinkedUnit!.definedTopLevelNames;
}
/// Return the set of all directly referenced files - imported, exported or
/// parted.
/// TODO(scheglov) Stop using [partedFiles].
Set<FileState> get directReferencedFiles {
return _directReferencedFiles ??= <FileState>{
...importedFiles.whereNotNull(),
...exportedFiles.whereNotNull(),
...partedFiles.whereNotNull(),
};
}
/// Return `true` if the file exists.
bool get exists => _fileContent!.exists;
/// The list of files this file exports.
List<FileState?> get exportedFiles {
return _exportedFiles ??= _unlinked2!.exports.map((directive) {
var uri = _selectRelativeUri(directive);
if (uri == null) {
return null;
}
return _fileForRelativeUri(uri).map(
(file) {
file?.referencingFiles.add(this);
return file;
},
(_) => null,
);
}).toList();
}
@override
int get hashCode => uri.hashCode;
/// The list of files this file imports.
List<FileState?> get importedFiles {
return _importedFiles ??= _unlinked2!.imports.map((directive) {
var uri = _selectRelativeUri(directive);
if (uri == null) {
return null;
}
return _fileForRelativeUri(uri).map(
(file) {
file?.referencingFiles.add(this);
return file;
},
(_) => null,
);
}).toList();
}
/// Return `true` if the file does not have a `library` directive, and has a
/// `part of` directive, so is probably a part.
bool get isPart {
if (_unlinked2!.libraryDirective != null) {
return false;
}
return _unlinked2!.partOfNameDirective != null ||
_unlinked2!.partOfUriDirective != null;
}
FileStateKind get kind => _kind!;
/// Return information about line in the file.
LineInfo get lineInfo => _lineInfo!;
/// The list of files this library file references as parts.
List<FileState?> get partedFiles {
final kind = _kind;
if (kind is LibraryFileStateKind) {
return kind.parts.map((part) {
if (part is PartDirectiveWithFile) {
return part.includedFile;
} else {
return null;
}
}).toList();
} else {
return [];
}
}
/// The external names referenced by the file.
Set<String> get referencedNames {
return _driverUnlinkedUnit!.referencedNames;
}
File get resource {
return _fsState._resourceProvider.getFile(path);
}
@visibleForTesting
FileStateTestView get test => FileStateTestView(this);
/// The [UnlinkedUnit] of the file.
UnlinkedUnit get unlinked2 => _unlinked2!;
String get unlinkedKey => _unlinkedKey!;
/// The MD5 signature based on the content, feature sets, language version.
Uint8List get unlinkedSignature => _unlinkedSignature!;
/// Return the [uri] string.
String get uriStr => uri.toString();
@override
bool operator ==(Object other) {
return other is FileState && other.uri == uri;
}
/// Return a new parsed unresolved [CompilationUnit].
CompilationUnitImpl parse([AnalysisErrorListener? errorListener]) {
errorListener ??= AnalysisErrorListener.NULL_LISTENER;
try {
return _parse(errorListener);
} catch (exception, stackTrace) {
throw CaughtExceptionWithFiles(
exception,
stackTrace,
{path: content},
);
}
}
/// TODO(scheglov) Remove it when [IgnoreInfo] is stored here.
CompilationUnitImpl parse2(
AnalysisErrorListener errorListener,
String content,
) {
CharSequenceReader reader = CharSequenceReader(content);
Scanner scanner = Scanner(source, reader, errorListener)
..configureFeatures(
featureSetForOverriding: _contextFeatureSet,
featureSet: _contextFeatureSet.restrictToVersion(
packageLanguageVersion,
),
);
Token token = scanner.tokenize(reportScannerErrors: false);
LineInfo lineInfo = LineInfo(scanner.lineStarts);
Parser parser = Parser(
source,
errorListener,
featureSet: scanner.featureSet,
lineInfo: lineInfo,
);
parser.enableOptionalNewAndConst = true;
var unit = parser.parseCompilationUnit(token);
unit.languageVersion = LibraryLanguageVersion(
package: packageLanguageVersion,
override: scanner.overrideVersion,
);
// StringToken uses a static instance of StringCanonicalizer, so we need
// to clear it explicitly once we are done using it for this file.
StringTokenImpl.canonicalizer.clear();
return unit;
}
/// Read the file content and ensure that all of the file properties are
/// consistent with the read content, including API signature.
///
/// Return how the file changed since the last refresh.
FileStateRefreshResult refresh() {
_invalidateCurrentUnresolvedData();
final rawFileState = _fsState.fileContentStrategy.get(path);
final contentChanged =
_fileContent?.contentHash != rawFileState.contentHash;
_fileContent = rawFileState;
// Prepare the unlinked bundle key.
{
var signature = ApiSignature();
signature.addUint32List(_fsState._saltForUnlinked);
signature.addFeatureSet(_contextFeatureSet);
signature.addLanguageVersion(packageLanguageVersion);
signature.addString(contentHash);
signature.addBool(exists);
_unlinkedSignature = signature.toByteList();
var signatureHex = hex.encode(_unlinkedSignature!);
// TODO(scheglov) Use the path as the key, and store the signature.
_unlinkedKey = '$signatureHex.unlinked2';
}
// Prepare the unlinked unit.
_driverUnlinkedUnit = _getUnlinkedUnit();
_unlinked2 = _driverUnlinkedUnit!.unit;
_lineInfo = LineInfo(_unlinked2!.lineStarts);
_prefetchDirectReferences();
// Prepare API signature.
var newApiSignature = _unlinked2!.apiSignature;
bool apiSignatureChanged = _apiSignature != null &&
!_equalByteLists(_apiSignature, newApiSignature);
_apiSignature = newApiSignature;
// Read imports/exports on demand.
_importedFiles = null;
_exportedFiles = null;
_directReferencedFiles = null;
// Read parts eagerly to link parts to libraries.
_updateKind();
// Update mapping from subtyped names to files.
for (var name in _driverUnlinkedUnit!.subtypedNames) {
var files = _fsState._subtypedNameToFiles[name];
if (files == null) {
files = <FileState>{};
_fsState._subtypedNameToFiles[name] = files;
}
files.add(this);
}
// Return how the file changed.
if (apiSignatureChanged) {
return FileStateRefreshResult.apiChanged;
} else if (contentChanged) {
return FileStateRefreshResult.contentChanged;
} else {
return FileStateRefreshResult.nothing;
}
}
@override
String toString() {
return '$uri = $path';
}
/// Return the [FileState] for the given [relativeUri], or `null` if the
/// URI cannot be parsed, cannot correspond any file, etc.
Either2<FileState?, ExternalLibrary> _fileForRelativeUri(
String relativeUri,
) {
Uri absoluteUri;
try {
absoluteUri = resolveRelativeUri(uri, Uri.parse(relativeUri));
} on FormatException {
return Either2.t1(null);
}
return _fsState.getFileForUri(absoluteUri);
}
/// Return the unlinked unit, from bytes or new.
AnalysisDriverUnlinkedUnit _getUnlinkedUnit() {
final testData = _fsState.testData?.forFile(resource, uri);
var bytes = _fsState._byteStore.get(_unlinkedKey!);
if (bytes != null && bytes.isNotEmpty) {
testData?.unlinkedKeyGet.add(unlinkedKey);
return AnalysisDriverUnlinkedUnit.fromBytes(bytes);
}
var unit = parse();
return _fsState._logger.run('Create unlinked for $path', () {
var unlinkedUnit = serializeAstUnlinked2(
unit,
isDartCore: uriStr == 'dart:core',
);
var definedNames = computeDefinedNames(unit);
var referencedNames = computeReferencedNames(unit);
var subtypedNames = computeSubtypedNames(unit);
var driverUnlinkedUnit = AnalysisDriverUnlinkedUnit(
definedTopLevelNames: definedNames.topLevelNames,
definedClassMemberNames: definedNames.classMemberNames,
referencedNames: referencedNames,
subtypedNames: subtypedNames,
unit: unlinkedUnit,
);
var bytes = driverUnlinkedUnit.toBytes();
_fsState._byteStore.putGet(_unlinkedKey!, bytes);
testData?.unlinkedKeyPut.add(unlinkedKey);
return driverUnlinkedUnit;
});
}
/// Invalidate any data that depends on the current unlinked data of the file,
/// because [refresh] is going to recompute the unlinked data.
void _invalidateCurrentUnresolvedData() {
if (_driverUnlinkedUnit != null) {
for (var name in _driverUnlinkedUnit!.subtypedNames) {
var files = _fsState._subtypedNameToFiles[name];
files?.remove(this);
}
}
}
CompilationUnitImpl _parse(AnalysisErrorListener errorListener) {
return parse2(errorListener, content);
}
/// TODO(scheglov) write tests
void _prefetchDirectReferences() {
final prefetchFiles = _fsState.prefetchFiles;
if (prefetchFiles == null) {
return;
}
var paths = <String>{};
void addRelativeUri(String? relativeUriStr) {
if (relativeUriStr == null) {
return;
}
final Uri absoluteUri;
try {
final relativeUri = Uri.parse(relativeUriStr);
absoluteUri = resolveRelativeUri(uri, relativeUri);
} on FormatException {
return;
}
final path = _fsState._sourceFactory.forUri2(absoluteUri)?.fullName;
if (path != null) {
paths.add(path);
}
}
for (final directive in unlinked2.imports) {
addRelativeUri(directive.uri);
}
for (final directive in unlinked2.exports) {
addRelativeUri(directive.uri);
}
for (final directive in unlinked2.parts) {
addRelativeUri(directive.uri);
}
prefetchFiles(paths.toList());
}
/// TODO(scheglov) move to _fsState?
String? _selectRelativeUri(UnlinkedNamespaceDirective directive) {
for (var configuration in directive.configurations) {
var name = configuration.name;
var value = configuration.value;
if (value.isEmpty) {
value = 'true';
}
if (_fsState._declaredVariables.get(name) == value) {
return configuration.uri;
}
}
return directive.uri;
}
void _updateKind() {
_kind?.dispose();
final libraryAugmentationDirective = unlinked2.libraryAugmentationDirective;
final libraryDirective = unlinked2.libraryDirective;
final partOfNameDirective = unlinked2.partOfNameDirective;
final partOfUriDirective = unlinked2.partOfUriDirective;
if (libraryAugmentationDirective != null) {
final uriStr = libraryAugmentationDirective.uri;
// TODO(scheglov) This could be a useful method of `Either`.
final uriFile = uriStr != null
? _fileForRelativeUri(uriStr).map(
(file) => file,
(_) => null,
)
: null;
if (uriFile != null) {
_kind = AugmentationKnownFileStateKind(
file: this,
directive: libraryAugmentationDirective,
uriFile: uriFile,
);
} else {
_kind = AugmentationUnknownFileStateKind(
file: this,
directive: libraryAugmentationDirective,
);
}
} else if (libraryDirective != null) {
_kind = LibraryFileStateKind(
file: this,
name: libraryDirective.name,
);
} else if (partOfNameDirective != null) {
_kind = PartOfNameFileStateKind(
file: this,
directive: partOfNameDirective,
);
} else if (partOfUriDirective != null) {
final uriStr = partOfUriDirective.uri;
final uriFile = uriStr != null
? _fileForRelativeUri(uriStr).map(
(file) => file,
(_) => null,
)
: null;
if (uriFile != null) {
_kind = PartOfUriKnownFileStateKind(
file: this,
directive: partOfUriDirective,
uriFile: uriFile,
);
} else {
_kind = PartOfUriUnknownFileStateKind(
file: this,
directive: partOfUriDirective,
);
}
} else {
_kind = LibraryFileStateKind(
file: this,
name: null,
);
}
}
static UnlinkedUnit serializeAstUnlinked2(
CompilationUnit unit, {
required bool isDartCore,
}) {
UnlinkedLibraryDirective? libraryDirective;
UnlinkedLibraryAugmentationDirective? libraryAugmentationDirective;
UnlinkedPartOfNameDirective? partOfNameDirective;
UnlinkedPartOfUriDirective? partOfUriDirective;
var augmentations = <UnlinkedImportAugmentationDirective>[];
var exports = <UnlinkedNamespaceDirective>[];
var imports = <UnlinkedNamespaceDirective>[];
var parts = <UnlinkedPartDirective>[];
var macroClasses = <MacroClass>[];
var hasDartCoreImport = false;
for (var directive in unit.directives) {
if (directive is ExportDirective) {
var builder = _serializeNamespaceDirective(directive);
exports.add(builder);
} else if (directive is ImportDirectiveImpl) {
if (directive.augmentKeyword != null) {
augmentations.add(
UnlinkedImportAugmentationDirective(
uri: directive.uri.stringValue,
),
);
} else {
var builder = _serializeNamespaceDirective(directive);
imports.add(builder);
if (builder.uri == 'dart:core') {
hasDartCoreImport = true;
}
}
} else if (directive is LibraryAugmentationDirective) {
final uri = directive.uri;
final uriStr = uri.stringValue;
if (uriStr != null) {
libraryAugmentationDirective = UnlinkedLibraryAugmentationDirective(
uri: uriStr,
uriRange: UnlinkedSourceRange(
offset: uri.offset,
length: uri.length,
),
);
}
} else if (directive is LibraryDirective) {
libraryDirective = UnlinkedLibraryDirective(
name: directive.name.name,
);
} else if (directive is PartDirective) {
parts.add(
UnlinkedPartDirective(
uri: directive.uri.stringValue,
),
);
} else if (directive is PartOfDirective) {
final libraryName = directive.libraryName;
final uri = directive.uri;
if (libraryName != null) {
partOfNameDirective = UnlinkedPartOfNameDirective(
name: libraryName.name,
nameRange: UnlinkedSourceRange(
offset: libraryName.offset,
length: libraryName.length,
),
);
} else if (uri != null) {
final uriStr = uri.stringValue;
if (uriStr != null) {
partOfUriDirective = UnlinkedPartOfUriDirective(
uri: uriStr,
uriRange: UnlinkedSourceRange(
offset: uri.offset,
length: uri.length,
),
);
}
}
}
}
for (var declaration in unit.declarations) {
if (declaration is ClassDeclarationImpl) {
if (declaration.macroKeyword != null) {
var constructors = declaration.members
.whereType<ConstructorDeclaration>()
.map((e) => e.name?.name ?? '')
.where((e) => !e.startsWith('_'))
.toList();
if (constructors.isNotEmpty) {
macroClasses.add(
MacroClass(
name: declaration.name.name,
constructors: constructors,
),
);
}
}
}
}
if (!isDartCore && !hasDartCoreImport) {
imports.add(
UnlinkedNamespaceDirective(
configurations: [],
isSyntheticDartCoreImport: true,
uri: 'dart:core',
),
);
}
final topLevelDeclarations = <String>{};
for (final declaration in unit.declarations) {
if (declaration is ClassDeclaration) {
topLevelDeclarations.add(declaration.name.name);
} else if (declaration is EnumDeclaration) {
topLevelDeclarations.add(declaration.name.name);
} else if (declaration is ExtensionDeclaration) {
var name = declaration.name;
if (name != null) {
topLevelDeclarations.add(name.name);
}
} else if (declaration is FunctionDeclaration) {
topLevelDeclarations.add(declaration.name.name);
} else if (declaration is MixinDeclaration) {
topLevelDeclarations.add(declaration.name.name);
} else if (declaration is TopLevelVariableDeclaration) {
for (var variable in declaration.variables.variables) {
topLevelDeclarations.add(variable.name.name);
}
}
}
return UnlinkedUnit(
apiSignature: Uint8List.fromList(computeUnlinkedApiSignature(unit)),
augmentations: augmentations,
exports: exports,
imports: imports,
informativeBytes: writeUnitInformative(unit),
libraryAugmentationDirective: libraryAugmentationDirective,
libraryDirective: libraryDirective,
lineStarts: Uint32List.fromList(unit.lineInfo.lineStarts),
macroClasses: macroClasses,
parts: parts,
partOfNameDirective: partOfNameDirective,
partOfUriDirective: partOfUriDirective,
topLevelDeclarations: topLevelDeclarations,
);
}
/// Return `true` if the given byte lists are equal.
static bool _equalByteLists(List<int>? a, List<int>? b) {
if (a == null) {
return b == null;
} else if (b == null) {
return false;
}
if (a.length != b.length) {
return false;
}
for (int i = 0; i < a.length; i++) {
if (a[i] != b[i]) {
return false;
}
}
return true;
}
static UnlinkedNamespaceDirective _serializeNamespaceDirective(
NamespaceDirective directive) {
return UnlinkedNamespaceDirective(
configurations: directive.configurations.map((configuration) {
var name = configuration.name.components.join('.');
var value = configuration.value?.stringValue ?? '';
return UnlinkedNamespaceDirectiveConfiguration(
name: name,
value: value,
uri: configuration.uri.stringValue,
);
}).toList(),
uri: directive.uri.stringValue,
);
}
}
abstract class FileStateKind {
final FileState file;
FileStateKind({
required this.file,
});
/// When [library] returns `null`, this getter is used to look at this
/// file itself as a library.
LibraryFileStateKind get asLibrary {
return LibraryFileStateKind(
file: file,
name: null,
);
}
/// Returns the library in which this file should be analyzed.
LibraryFileStateKind? get library;
@mustCallSuper
void dispose() {}
}
enum FileStateRefreshResult {
/// No changes to the content, so no changes at all.
nothing,
/// The content changed, but the API of the file is the same.
contentChanged,
/// The content changed, and the API of the file is different.
apiChanged,
}
@visibleForTesting
class FileStateTestView {
final FileState file;
FileStateTestView(this.file);
String get unlinkedKey => file._unlinkedKey!;
}
/// Information about known file system state.
class FileSystemState {
final PerformanceLog _logger;
final ResourceProvider _resourceProvider;
final String contextName;
final ByteStore _byteStore;
final SourceFactory _sourceFactory;
final Workspace? _workspace;
final DeclaredVariables _declaredVariables;
final Uint32List _saltForUnlinked;
final Uint32List _saltForElements;
final FeatureSetProvider featureSetProvider;
/// Mapping from a URI to the corresponding [FileState].
final Map<Uri, FileState> _uriToFile = {};
/// All known file paths.
final Set<String> knownFilePaths = <String>{};
/// All known files.
final List<FileState> knownFiles = [];
/// Mapping from a path to the flag whether there is a URI for the path.
final Map<String, bool> _hasUriForPath = {};
/// Mapping from a path to the corresponding [FileState].
final Map<String, FileState> _pathToFile = {};
/// Mapping from a library name to the [LibraryFileStateKind] that have it.
final _LibraryNameToFiles _libraryNameToFiles = _LibraryNameToFiles();
/// The map of subtyped names to files where these names are subtyped.
final Map<String, Set<FileState>> _subtypedNameToFiles = {};
/// The value of this field is incremented when the set of files is updated.
int fileStamp = 0;
final FileContentStrategy fileContentStrategy;
/// 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;
/// 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;
late final FileSystemStateTestView _testView;
final FileSystemTestData? testData;
FileSystemState(
this._logger,
this._byteStore,
this._resourceProvider,
this.contextName,
this._sourceFactory,
this._workspace,
this._declaredVariables,
this._saltForUnlinked,
this._saltForElements,
this.featureSetProvider, {
required this.fileContentStrategy,
required this.prefetchFiles,
required this.isGenerated,
required this.testData,
}) {
_testView = FileSystemStateTestView(this);
}
package_path.Context get pathContext => _resourceProvider.pathContext;
@visibleForTesting
FileSystemStateTestView get test => _testView;
/// Update the state to reflect the fact that the file with the given [path]
/// was changed. Specifically this means that we evict this file and every
/// file that referenced it.
void changeFile(String path, Set<FileState> removedFiles) {
var file = _pathToFile.remove(path);
if (file == null) {
return;
}
if (!removedFiles.add(file)) {
return;
}
_uriToFile.remove(file.uri);
// The removed file does not reference other file anymore.
for (var referencedFile in file.directReferencedFiles) {
referencedFile.referencingFiles.remove(file);
}
// Recursively remove files that reference the removed file.
for (var reference in file.referencingFiles.toList()) {
changeFile(reference.path, removedFiles);
}
file._kind?.dispose();
}
/// Collected files that transitively reference a file with the [path].
/// These files are potentially affected by the change.
void collectAffected(String path, Set<FileState> affected) {
// TODO(scheglov) This should not be necessary.
// We use affected files to remove library elements, and we can only get
// these library elements when we link or load them, using library cycles.
// And we get library cycles by asking `directReferencedFiles`.
for (var file in knownFiles.toList()) {
file.directReferencedFiles;
}
collectAffected(FileState file) {
if (affected.add(file)) {
for (var other in file.referencingFiles) {
collectAffected(other);
}
}
}
var file = _pathToFile[path];
if (file != null) {
collectAffected(file);
}
}
FeatureSet contextFeatureSet(
String path,
Uri uri,
WorkspacePackage? workspacePackage,
) {
var workspacePackageExperiments = workspacePackage?.enabledExperiments;
if (workspacePackageExperiments != null) {
return featureSetProvider.featureSetForExperiments(
workspacePackageExperiments,
);
}
return featureSetProvider.getFeatureSet(path, uri);
}
Version contextLanguageVersion(
String path,
Uri uri,
WorkspacePackage? workspacePackage,
) {
var workspaceLanguageVersion = workspacePackage?.languageVersion;
if (workspaceLanguageVersion != null) {
return workspaceLanguageVersion;
}
return featureSetProvider.getLanguageVersion(path, uri);
}
/// Notifies this object that it is about to be discarded.
///
/// Returns the keys of the artifacts that are no longer used.
Set<String> dispose() {
final result = <String>{};
for (final file in _pathToFile.values) {
result.add(file._unlinkedKey!);
}
_pathToFile.clear();
_uriToFile.clear();
return result;
}
FileState? getExisting(File file) {
return _pathToFile[file.path];
}
/// Return the [FileState] for the given absolute [path]. The returned file
/// has the last known state since if was last refreshed.
/// TODO(scheglov) Merge with [getFileForPath2].
FileState getFileForPath(String path) {
return getFileForPath2(
path: path,
performance: OperationPerformanceImpl('<root>'),
);
}
/// Return the [FileState] for the given absolute [path]. The returned file
/// has the last known state since if was last refreshed.
FileState getFileForPath2({
required String path,
required OperationPerformanceImpl performance,
}) {
var file = _pathToFile[path];
if (file == null) {
File resource = _resourceProvider.getFile(path);
Uri uri = _sourceFactory.pathToUri(path)!;
file = _newFile(resource, path, uri);
}
return file;
}
/// The given [uri] must be absolute.
///
/// If [uri] corresponds to a library from the summary store, return a
/// [ExternalLibrary].
///
/// Otherwise the [uri] is resolved to a file, and the corresponding
/// [FileState] is returned. Might be `null` if the [uri] cannot be resolved
/// to a file, for example because it is invalid (e.g. a `package:` URI
/// without a package name), or we don't know this package. The returned
/// file has the last known state since if was last refreshed.
Either2<FileState?, ExternalLibrary> getFileForUri(Uri uri) {
final uriSource = _sourceFactory.forUri2(uri);
// If the external store has this URI, create a stub file for it.
// We are given all required unlinked and linked summaries for it.
if (uriSource is InSummarySource) {
return Either2.t2(ExternalLibrary._(uriSource));
}
FileState? file = _uriToFile[uri];
if (file == null) {
// If the URI cannot be resolved, for example because the factory
// does not understand the scheme, return the unresolved file instance.
if (uriSource == null) {
return Either2.t1(null);
}
String path = uriSource.fullName;
// Check if already resolved to this path via different URI.
// That different URI must be the canonical one.
file = _pathToFile[path];
if (file != null) {
return Either2.t1(file);
}
File resource = _resourceProvider.getFile(path);
var rewrittenUri = rewriteToCanonicalUri(_sourceFactory, uri);
if (rewrittenUri == null) {
return Either2.t1(null);
}
file = _newFile(resource, path, rewrittenUri);
}
return Either2.t1(file);
}
/// Returns a list of files whose contents contains the given string.
/// Generated files are not included in the search.
List<String> getFilesContaining(String value) {
var result = <String>[];
_pathToFile.forEach((path, file) {
// TODO(scheglov) tests for excluding generated
if (!isGenerated(path)) {
if (file.content.contains(value)) {
result.add(path);
}
}
});
return result;
}
/// Return files where the given [name] is subtyped, i.e. used in `extends`,
/// `with` or `implements` clauses.
Set<FileState>? getFilesSubtypingName(String name) {
return _subtypedNameToFiles[name];
}
/// Return files that have a top-level declaration with the [name].
List<FileState> getFilesWithTopLevelDeclarations(String name) {
final result = <FileState>[];
for (final file in _pathToFile.values) {
if (file.unlinked2.topLevelDeclarations.contains(name)) {
result.add(file);
}
}
return result;
}
/// Return `true` if there is a URI that can be resolved to the [path].
///
/// When a file exists, but for the URI that corresponds to the file is
/// resolved to another file, e.g. a generated one in Bazel, Gn, etc, we
/// cannot analyze the original file.
bool hasUri(String path) {
bool? flag = _hasUriForPath[path];
if (flag == null) {
Uri uri = _sourceFactory.pathToUri(path)!;
Source? uriSource = _sourceFactory.forUri2(uri);
flag = uriSource?.fullName == path;
_hasUriForPath[path] = flag;
}
return flag;
}
/// When printing the state for testing, we want to see all files.
@visibleForTesting
void pullReferencedFiles() {
while (true) {
final fileCount = _pathToFile.length;
for (final file in _pathToFile.values.toList()) {
final kind = file.kind;
if (kind is LibraryOrAugmentationFileKind) {
kind.imports;
kind.exports;
}
}
if (_pathToFile.length == fileCount) {
break;
}
}
}
/// Remove the file with the given [path].
void removeFile(String path) {
_clearFiles();
}
/// Computes the set of [FileState]'s used/not used to analyze the given
/// [paths]. Removes the [FileState]'s of the files not used for analysis from
/// the cache. Returns the set of unused [FileState]'s.
Set<FileState> removeUnusedFiles(List<String> paths) {
final referenced = <FileState>{};
for (final path in paths) {
final library = _pathToFile[path]?.kind.library;
library?.collectTransitive(referenced);
}
final removed = <FileState>{};
for (final file in _pathToFile.values.toList()) {
if (!referenced.contains(file)) {
changeFile(file.path, removed);
}
}
return removed;
}
/// Clear all [FileState] data - all maps from path or URI, etc.
void _clearFiles() {
_uriToFile.clear();
knownFilePaths.clear();
knownFiles.clear();
_hasUriForPath.clear();
_pathToFile.clear();
_subtypedNameToFiles.clear();
_libraryNameToFiles.clear();
}
FileState _newFile(File resource, String path, Uri uri) {
FileSource uriSource = FileSource(resource, uri);
WorkspacePackage? workspacePackage = _workspace?.findPackageFor(path);
FeatureSet featureSet = contextFeatureSet(path, uri, workspacePackage);
Version packageLanguageVersion =
contextLanguageVersion(path, uri, workspacePackage);
var file = FileState._(this, path, uri, uriSource, workspacePackage,
featureSet, packageLanguageVersion);
_pathToFile[path] = file;
_uriToFile[uri] = file;
knownFilePaths.add(path);
knownFiles.add(file);
fileStamp++;
file.refresh();
return file;
}
}
@visibleForTesting
class FileSystemStateTestView {
final FileSystemState state;
FileSystemStateTestView(this.state);
}
class FileSystemTestData {
final Map<File, FileTestData> files = {};
FileTestData forFile(File file, Uri uri) {
return files[file] ??= FileTestData._(file, uri);
}
}
class FileTestData {
final File file;
final Uri uri;
/// We add the key every time we get unlinked data from the byte store.
final List<String> unlinkedKeyGet = [];
/// We add the key every time we put unlinked data into the byte store.
final List<String> unlinkedKeyPut = [];
FileTestData._(this.file, this.uri);
@override
int get hashCode => file.hashCode;
@override
bool operator ==(Object other) {
return other is FileTestData && other.file == file && other.uri == uri;
}
}
/// Precomputed properties of a file URI, used because [Uri] is relatively
/// expensive to work with, if we do this thousand times.
class FileUriProperties {
static const int _isDart = 1 << 0;
static const int _isDartInternal = 1 << 1;
static const int _isSrc = 1 << 2;
final int _flags;
final String? packageName;
factory FileUriProperties(Uri uri) {
if (uri.isScheme('dart')) {
var dartName = uri.pathSegments.firstOrNull;
return FileUriProperties._dart(
isInternal: dartName != null && dartName.startsWith('_'),
);
} else if (uri.isScheme('package')) {
var segments = uri.pathSegments;
if (segments.length >= 2) {
return FileUriProperties._package(
packageName: segments[0],
isSrc: segments[1] == 'src',
);
}
}
return const FileUriProperties._unknown();
}
const FileUriProperties._dart({
required bool isInternal,
}) : _flags = _isDart | (isInternal ? _isDartInternal : 0),
packageName = null;
FileUriProperties._package({
required this.packageName,
required bool isSrc,
}) : _flags = isSrc ? _isSrc : 0;
const FileUriProperties._unknown()
: _flags = 0,
packageName = null;
bool get isDart => (_flags & _isDart) != 0;
bool get isDartInternal => (_flags & _isDartInternal) != 0;
bool get isSrc => (_flags & _isSrc) != 0;
}
/// Information about a single `import augment` directive.
class ImportAugmentationDirectiveState extends DirectiveState {
final UnlinkedImportAugmentationDirective directive;
ImportAugmentationDirectiveState({
required this.directive,
});
/// Returns a [Source] that is referenced by this directive.
///
/// Returns `null` if the URI cannot be resolved into a [Source].
Source? get importedSource => null;
}
/// [ImportAugmentationWithUri] that has a valid URI that references a file.
class ImportAugmentationDirectiveWithFile
extends ImportAugmentationDirectiveState {
final LibraryOrAugmentationFileKind container;
final FileState importedFile;
ImportAugmentationDirectiveWithFile({
required this.container,
required super.directive,
required this.importedFile,
}) {
importedFile.referencingFiles.add(container.file);
}
/// If [importedFile] is a [AugmentationFileStateKind], and it confirms that
/// it is an augmentation of the [container], returns the [importedFile].
AugmentationFileStateKind? get importedAugmentation {
final kind = importedFile.kind;
if (kind is AugmentationFileStateKind && kind.isAugmentationOf(container)) {
return kind;
}
return null;
}
@override
Source? get importedSource => importedFile.source;
@override
void dispose() {
importedFile.referencingFiles.remove(container.file);
}
}
/// [ImportAugmentationDirectiveState] that has a valid URI.
class ImportAugmentationWithUri extends ImportAugmentationDirectiveState {
final String uriStr;
ImportAugmentationWithUri({
required super.directive,
required this.uriStr,
});
}
/// Information about a single `import` directive.
class ImportDirectiveState extends DirectiveState {
final UnlinkedNamespaceDirective directive;
ImportDirectiveState({
required this.directive,
});
/// If [importedSource] corresponds to a library, returns it.
Source? get importedLibrarySource => null;
/// Returns a [Source] that is referenced by this directive. If the are
/// configurations, selects the one which satisfies the conditions.
///
/// Returns `null` if the selected URI is not valid, or cannot be resolved
/// into a [Source].
Source? get importedSource => null;
bool get isSyntheticDartCoreImport => directive.isSyntheticDartCoreImport;
}
/// [ImportDirectiveWithUri] that has a valid URI that references a file.
class ImportDirectiveWithFile extends ImportDirectiveWithUri {
final LibraryOrAugmentationFileKind container;
final FileState importedFile;
ImportDirectiveWithFile({
required this.container,
required super.directive,
required this.importedFile,
required super.selectedUriStr,
}) {
importedFile.referencingFiles.add(container.file);
}
/// Returns [importedFile] if it is a library.
LibraryFileStateKind? get importedLibrary {
final kind = importedFile.kind;
if (kind is LibraryFileStateKind) {
return kind;
}
return null;
}
@override
Source? get importedLibrarySource {
if (importedFile.kind is LibraryFileStateKind) {
return importedSource;
}
return null;
}
@override
Source get importedSource => importedFile.source;
@override
void dispose() {
importedFile.referencingFiles.remove(container.file);
}
}
/// [ImportDirectiveWithUri] with a URI that resolves to [InSummarySource].
class ImportDirectiveWithInSummarySource extends ImportDirectiveWithUri {
@override
final InSummarySource importedSource;
ImportDirectiveWithInSummarySource({
required super.directive,
required this.importedSource,
required super.selectedUriStr,
});
@override
Source? get importedLibrarySource {
if (importedSource.kind == InSummarySourceKind.library) {
return importedSource;
} else {
return null;
}
}
}
/// [ImportDirectiveState] that has a valid URI.
class ImportDirectiveWithUri extends ImportDirectiveState {
final String selectedUriStr;
ImportDirectiveWithUri({
required super.directive,
required this.selectedUriStr,
});
}
class LibraryFileStateKind extends LibraryOrAugmentationFileKind {
/// The name of the library from the `library` directive.
/// Or `null` if no `library` directive.
final String? name;
List<PartDirectiveState>? _parts;
LibraryCycle? _libraryCycle;
LibraryFileStateKind({
required super.file,
required this.name,
}) {
file._fsState._libraryNameToFiles.add(this);
}
/// The list of files files that this library consists of, i.e. this library
/// file itself, its [parts], and augmentations.
List<FileState> get files {
final files = [
file,
];
// TODO(scheglov) When we include only valid parts, use this instead.
final includeOnlyValidParts = 0 > 1;
for (final part in parts) {
if (part is PartDirectiveWithFile) {
if (includeOnlyValidParts) {
files.addIfNotNull(part.includedPart?.file);
} else {
files.add(part.includedFile);
}
}
}
// TODO(scheglov) Include augmentations.
return files;
}
LibraryCycle? get internal_libraryCycle => _libraryCycle;
@override
LibraryFileStateKind get library => this;
/// Return the [LibraryCycle] this file belongs to, even if it consists of
/// just this file. If the library cycle is not known yet, compute it.
LibraryCycle get libraryCycle {
if (_libraryCycle == null) {
computeLibraryCycle(file._fsState._saltForElements, this);
}
return _libraryCycle!;
}
List<PartDirectiveState> get parts {
return _parts ??= file.unlinked2.parts.map((directive) {
final uriStr = directive.uri;
if (uriStr != null) {
return file._fileForRelativeUri(uriStr).map(
(refFile) {
if (refFile != null) {
return PartDirectiveWithFile(
library: this,
directive: directive,
includedFile: refFile,
uriStr: uriStr,
);
} else {
return PartDirectiveWithUri(
library: this,
directive: directive,
uriStr: uriStr,
);
}
},
(externalLibrary) {
return PartDirectiveWithUri(
library: this,
directive: directive,
uriStr: uriStr,
);
},
);
} else {
return PartDirectiveState(
library: this,
directive: directive,
);
}
}).toList();
}
@override
void collectTransitive(Set<FileState> files) {
super.collectTransitive(files);
for (final part in parts) {
if (part is PartDirectiveWithFile) {
files.add(part.includedFile);
}
}
}
@override
void discoverReferencedFiles() {
super.discoverReferencedFiles();
parts;
}
@override
void dispose() {
invalidateLibraryCycle();
file._fsState._libraryNameToFiles.remove(this);
_parts?.disposeAll();
super.dispose();
}
bool hasPart(PartFileStateKind partKind) {
for (final partDirective in parts) {
if (partDirective is PartDirectiveWithFile) {
if (partDirective.includedFile == partKind.file) {
return true;
}
}
}
return false;
}
void internal_setLibraryCycle(LibraryCycle? cycle) {
_libraryCycle = cycle;
}
void invalidateLibraryCycle() {
_libraryCycle?.invalidate();
_libraryCycle = null;
}
}
abstract class LibraryOrAugmentationFileKind extends FileStateKind {
List<ImportAugmentationDirectiveState>? _augmentations;
List<ExportDirectiveState>? _exports;
List<ImportDirectiveState>? _imports;
LibraryOrAugmentationFileKind({
required super.file,
});
List<ImportAugmentationDirectiveState> get augmentations {
return _augmentations ??= file.unlinked2.augmentations.map((directive) {
final uriStr = directive.uri;
if (uriStr != null) {
return file._fileForRelativeUri(uriStr).map(
(refFile) {
if (refFile != null) {
return ImportAugmentationDirectiveWithFile(
container: this,
directive: directive,
importedFile: refFile,
);
} else {
return ImportAugmentationWithUri(
directive: directive,
uriStr: uriStr,
);
}
},
(externalLibrary) {
return ImportAugmentationWithUri(
directive: directive,
uriStr: uriStr,
);
},
);
} else {
return ImportAugmentationDirectiveState(
directive: directive,
);
}
}).toList();
}
List<ExportDirectiveState> get exports {
return _exports ??= file.unlinked2.exports.map((directive) {
final uriStr = file._selectRelativeUri(directive);
if (uriStr != null) {
return file._fileForRelativeUri(uriStr).map(
(refFile) {
if (refFile != null) {
return ExportDirectiveWithFile(
container: this,
directive: directive,
exportedFile: refFile,
selectedUriStr: uriStr,
);
} else {
return ExportDirectiveWithUri(
directive: directive,
selectedUriStr: uriStr,
);
}
},
(externalLibrary) {
return ExportDirectiveWithInSummarySource(
directive: directive,
exportedSource: externalLibrary.source,
selectedUriStr: uriStr,
);
},
);
} else {
return ExportDirectiveState(
directive: directive,
);
}
}).toList();
}
List<ImportDirectiveState> get imports {
return _imports ??= file.unlinked2.imports.map((directive) {
final uriStr = file._selectRelativeUri(directive);
if (uriStr != null) {
return file._fileForRelativeUri(uriStr).map(
(refFile) {
if (refFile != null) {
return ImportDirectiveWithFile(
container: this,
directive: directive,
importedFile: refFile,
selectedUriStr: uriStr,
);
} else {
return ImportDirectiveWithUri(
directive: directive,
selectedUriStr: uriStr,
);
}
},
(externalLibrary) {
return ImportDirectiveWithInSummarySource(
directive: directive,
importedSource: externalLibrary.source,
selectedUriStr: uriStr,
);
},
);
} else {
return ImportDirectiveState(
directive: directive,
);
}
}).toList();
}
/// Collect files that are transitively referenced by this library.
@mustCallSuper
void collectTransitive(Set<FileState> files) {
if (files.add(file)) {
for (final augmentation in augmentations) {
if (augmentation is ImportAugmentationDirectiveWithFile) {
augmentation.importedAugmentation?.collectTransitive(files);
}
}
for (final export in exports) {
if (export is ExportDirectiveWithFile) {
export.exportedLibrary?.collectTransitive(files);
}
}
for (final import in imports) {
if (import is ImportDirectiveWithFile) {
import.importedLibrary?.collectTransitive(files);
}
}
}
}
/// Directives are usually pulled lazily (so that we can parse a file
/// without pulling all its transitive references), but when we output
/// textual dumps we want to check that we reference only objects that
/// are available. So, we need to discover all referenced files before
/// we register available objects.
@visibleForTesting
void discoverReferencedFiles() {
exports;
imports;
for (final import in augmentations) {
if (import is ImportAugmentationDirectiveWithFile) {
import.importedAugmentation?.discoverReferencedFiles();
}
}
}
@override
void dispose() {
_augmentations?.disposeAll();
_exports?.disposeAll();
_imports?.disposeAll();
super.dispose();
}
bool hasAugmentation(AugmentationFileStateKind augmentation) {
for (final import in augmentations) {
if (import is ImportAugmentationDirectiveWithFile) {
if (import.importedFile == augmentation.file) {
return true;
}
}
}
return false;
}
}
/// Information about a single `part` directive.
class PartDirectiveState extends DirectiveState {
final LibraryFileStateKind library;
final UnlinkedPartDirective directive;
PartDirectiveState({
required this.library,
required this.directive,
});
/// Returns a [Source] that is referenced by this directive.
///
/// Returns `null` if the URI cannot be resolved into a [Source].
Source? get includedSource => null;
}
/// [PartDirectiveWithUri] that has a valid URI that references a file.
class PartDirectiveWithFile extends PartDirectiveWithUri {
final FileState includedFile;
PartDirectiveWithFile({
required super.library,
required super.directive,
required super.uriStr,
required this.includedFile,
}) {
includedFile.referencingFiles.add(library.file);
}
/// If [includedFile] is a [PartFileStateKind], and it confirms that it
/// is a part of the [library], returns the [includedFile].
PartFileStateKind? get includedPart {
final kind = includedFile.kind;
if (kind is PartFileStateKind && kind.isPartOf(library)) {
return kind;
}
return null;
}
@override
Source? get includedSource => includedFile.source;
@override
void dispose() {
includedFile.referencingFiles.remove(library.file);
}
}
/// [PartDirectiveState] that has a valid URI.
class PartDirectiveWithUri extends PartDirectiveState {
final String uriStr;
PartDirectiveWithUri({
required super.library,
required super.directive,
required this.uriStr,
});
}
/// The file has `part of` directive.
abstract class PartFileStateKind extends FileStateKind {
PartFileStateKind({
required super.file,
}) {
_invalidateLibraries();
}
@override
void dispose() {
_invalidateLibraries();
super.dispose();
}
/// Returns `true` if the `part of` directive confirms the [library].
bool isPartOf(LibraryFileStateKind library);
/// This method is invoked when the part file is updated.
/// The file either becomes a part, or might stop being a part.
void _invalidateLibraries() {
for (final reference in file.referencingFiles) {
final referenceKind = reference.kind;
if (referenceKind is LibraryFileStateKind) {
referenceKind.invalidateLibraryCycle();
}
}
}
}
/// The file has `part of name` directive.
class PartOfNameFileStateKind extends PartFileStateKind {
final UnlinkedPartOfNameDirective directive;
PartOfNameFileStateKind({
required super.file,
required this.directive,
});
/// Libraries with the same name as in [directive].
List<LibraryFileStateKind> get libraries {
final files = file._fsState._libraryNameToFiles;
return files[directive.name] ?? [];
}
/// If there are libraries that include this file as a part, return the
/// first one as if sorted by path.
@override
LibraryFileStateKind? get library {
discoverLibraries();
LibraryFileStateKind? result;
for (final library in libraries) {
if (library.hasPart(this)) {
if (result == null) {
result = library;
} else if (library.file.path.compareTo(result.file.path) < 0) {
result = library;
}
}
}
return result;
}
@visibleForTesting
void discoverLibraries() {
if (libraries.isEmpty) {
var resourceProvider = file._fsState._resourceProvider;
var pathContext = resourceProvider.pathContext;
var siblings = <Resource>[];
try {
siblings = file.resource.parent.getChildren();
} catch (_) {}
for (final sibling in siblings) {
if (file_paths.isDart(pathContext, sibling.path)) {
file._fsState.getFileForPath2(
path: sibling.path,
performance: OperationPerformanceImpl('<root>'),
);
}
}
}
}
@override
bool isPartOf(LibraryFileStateKind library) {
return directive.name == library.name;
}
}
/// The file has `part of URI` directive.
abstract class PartOfUriFileStateKind extends PartFileStateKind {
final UnlinkedPartOfUriDirective directive;
PartOfUriFileStateKind({
required super.file,
required this.directive,
});
}
/// The file has `part of URI` directive, and the URI can be resolved.
class PartOfUriKnownFileStateKind extends PartOfUriFileStateKind {
final FileState uriFile;
PartOfUriKnownFileStateKind({
required super.file,
required super.directive,
required this.uriFile,
});
@override
LibraryFileStateKind? get library {
final uriKind = uriFile.kind;
if (uriKind is LibraryFileStateKind) {
if (uriKind.hasPart(this)) {
return uriKind;
}
}
return null;
}
@override
bool isPartOf(LibraryFileStateKind library) {
return uriFile == library.file;
}
}
/// The file has `part of URI` directive, and the URI cannot be resolved.
class PartOfUriUnknownFileStateKind extends PartOfUriFileStateKind {
PartOfUriUnknownFileStateKind({
required super.file,
required super.directive,
});
@override
LibraryFileStateKind? get library => null;
@override
bool isPartOf(LibraryFileStateKind library) => false;
}
class StoredFileContent implements FileContent {
@override
final String content;
@override
final String contentHash;
@override
final bool exists;
StoredFileContent({
required this.content,
required this.contentHash,
required this.exists,
});
}
class StoredFileContentStrategy implements FileContentStrategy {
final FileContentCache _fileContentCache;
StoredFileContentStrategy(this._fileContentCache);
@override
FileContent get(String path) {
final fileContent = _fileContentCache.get(path);
return StoredFileContent(
content: fileContent.content,
contentHash: fileContent.contentHash,
exists: fileContent.exists,
);
}
/// The file with the given [path] might have changed, so ensure that it is
/// read the next time it is refreshed.
void markFileForReading(String path) {
_fileContentCache.invalidate(path);
}
}
class _LibraryNameToFiles {
final Map<String, List<LibraryFileStateKind>> _map = {};
List<LibraryFileStateKind>? operator [](String name) {
return _map[name];
}
/// If [kind] is a named library, register it.
void add(LibraryFileStateKind kind) {
final name = kind.name;
if (name != null) {
final libraries = _map[name] ??= [];
libraries.add(kind);
}
}
void clear() {
_map.clear();
}
/// If [kind] is a named library, unregister it.
void remove(LibraryFileStateKind kind) {
final name = kind.name;
if (name != null) {
final libraries = _map[name];
if (libraries != null) {
libraries.remove(kind);
if (libraries.isEmpty) {
_map.remove(name);
}
}
}
}
}
extension on List<DirectiveState> {
void disposeAll() {
for (final directive in this) {
directive.dispose();
}
}
}