| // 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:async'; | |
| import 'dart:collection'; | |
| import 'dart:math'; | |
| import 'dart:typed_data'; | |
| import 'package:analyzer/dart/analysis/declared_variables.dart'; | |
| import 'package:analyzer/dart/analysis/results.dart'; | |
| import 'package:analyzer/diagnostic/diagnostic.dart'; | |
| import 'package:analyzer/error/error.dart'; | |
| import 'package:analyzer/error/listener.dart'; | |
| import 'package:analyzer/exception/exception.dart'; | |
| import 'package:analyzer/file_system/file_system.dart'; | |
| import 'package:analyzer/source/file_source.dart'; | |
| import 'package:analyzer/source/source.dart'; | |
| import 'package:analyzer/src/analysis_options/analysis_options.dart'; | |
| import 'package:analyzer/src/context/packages.dart'; | |
| import 'package:analyzer/src/dart/analysis/analysis_options_map.dart'; | |
| import 'package:analyzer/src/dart/analysis/byte_store.dart'; | |
| import 'package:analyzer/src/dart/analysis/driver_based_analysis_context.dart'; | |
| import 'package:analyzer/src/dart/analysis/driver_event.dart' as events; | |
| 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/file_state.dart'; | |
| import 'package:analyzer/src/dart/analysis/file_state_filter.dart'; | |
| import 'package:analyzer/src/dart/analysis/file_tracker.dart'; | |
| import 'package:analyzer/src/dart/analysis/index.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/library_diagnostics.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/analysis/session.dart'; | |
| import 'package:analyzer/src/dart/analysis/status.dart'; | |
| import 'package:analyzer/src/dart/analysis/testing_data.dart'; | |
| import 'package:analyzer/src/dart/analysis/unlinked_unit_store.dart'; | |
| import 'package:analyzer/src/dart/ast/ast.dart'; | |
| import 'package:analyzer/src/dart/element/element.dart'; | |
| import 'package:analyzer/src/dartdoc/dartdoc_directive_info.dart'; | |
| import 'package:analyzer/src/diagnostic/diagnostic.dart' | |
| show DiagnosticMessageImpl; | |
| import 'package:analyzer/src/exception/exception.dart'; | |
| import 'package:analyzer/src/fine/manifest_id.dart'; | |
| import 'package:analyzer/src/fine/requirements.dart'; | |
| import 'package:analyzer/src/generated/engine.dart' | |
| show AnalysisContext, AnalysisEngine; | |
| import 'package:analyzer/src/generated/source.dart' show SourceFactory; | |
| import 'package:analyzer/src/lint/registry.dart' as linter; | |
| 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/summary2/bundle_writer.dart'; | |
| import 'package:analyzer/src/summary2/package_bundle_format.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/utilities/extensions/async.dart'; | |
| import 'package:analyzer/src/utilities/extensions/collection.dart'; | |
| import 'package:analyzer/src/utilities/extensions/object.dart'; | |
| import 'package:analyzer/src/utilities/uri_cache.dart'; | |
| import 'package:analyzer/src/workspace/pub.dart'; | |
| import 'package:collection/collection.dart'; | |
| import 'package:meta/meta.dart'; | |
| import 'package:pub_semver/pub_semver.dart'; | |
| /// This function is used to test recording requirements during analysis. | |
| /// | |
| /// Some [ElementImpl] APIs are not trivial, or maybe even impossible, to | |
| /// trigger. For example because this API is not used during normal resolution | |
| /// of Dart code, but can be used by a linter rule. | |
| @visibleForTesting | |
| void Function(List<CompilationUnitImpl> units)? | |
| testFineAfterLibraryAnalyzerHook; | |
| /// This class computes analysis results for Dart files. | |
| /// | |
| /// Let the set of "explicitly analyzed files" denote the set of paths that have | |
| /// been passed to [addFile] but not subsequently passed to [removeFile]. Let | |
| /// the "current analysis results" denote the map from the set of explicitly | |
| /// analyzed files to the most recent [AnalysisResult] delivered to `events` | |
| /// for each file. Let the "current file state" represent a map from file path | |
| /// to the file contents most recently read from that file, or fetched from the | |
| /// content cache (considering all possible file paths, regardless of | |
| /// whether they're in the set of explicitly analyzed files). Let the | |
| /// "analysis state" be either "working" or "idle". | |
| /// | |
| /// (These are theoretical constructs; they may not necessarily reflect data | |
| /// structures maintained explicitly by the driver.) | |
| /// | |
| /// Then we make the following guarantees: | |
| /// | |
| /// - Whenever the analysis state is idle, the current analysis results are | |
| /// consistent with the current file state. | |
| /// | |
| /// - A call to [addFile] or [changeFile] causes the analysis state to | |
| /// transition to "working", and schedules the contents of the given | |
| /// files to be read into the current file state prior to the next time | |
| /// the analysis state transitions back to "idle". | |
| /// | |
| /// - If at any time the client stops making calls to [addFile], [changeFile], | |
| /// and [removeFile], the analysis state will eventually transition back to | |
| /// "idle" after a finite amount of processing. | |
| /// | |
| /// As a result of these guarantees, a client may ensure that the analysis | |
| /// results are "eventually consistent" with the file system by simply calling | |
| /// [changeFile] any time the contents of a file on the file system have changed. | |
| /// | |
| // TODO(scheglov): Clean up the list of implicitly analyzed files. | |
| class AnalysisDriver { | |
| /// The version of data format, should be incremented on every format change. | |
| static const int DATA_VERSION = 745; | |
| /// The number of exception contexts allowed to write. Once this field is | |
| /// zero, we stop writing any new exception contexts in this process. | |
| static int allowedNumberOfContextsToWrite = 10; | |
| /// The scheduler that schedules analysis work in this, and possibly other | |
| /// analysis drivers. | |
| final AnalysisDriverScheduler _scheduler; | |
| /// The logger to write performed operations and performance to. | |
| final PerformanceLog _logger; | |
| /// The resource provider for working with files. | |
| final ResourceProvider _resourceProvider; | |
| /// The byte storage to get and put serialized data. | |
| /// | |
| /// It can be shared with other [AnalysisDriver]s. | |
| final ByteStore _byteStore; | |
| /// The optional store with externally provided unlinked and corresponding | |
| /// linked summaries. These summaries are always added to the store for any | |
| /// file analysis. | |
| final SummaryDataStore? _externalSummaries; | |
| /// This [FileContentCache] is consulted for a file content before reading | |
| /// the content from the file. | |
| final FileContentCache _fileContentCache; | |
| /// The already loaded unlinked units, consulted before deserializing | |
| /// from file again. | |
| final UnlinkedUnitStore _unlinkedUnitStore; | |
| late final StoredFileContentStrategy _fileContentStrategy; | |
| /// The [Packages] object with packages and their language versions. | |
| final Packages _packages; | |
| /// Replaces the discovered default language version for non-SDK files. | |
| final Version? _languageVersionOverride; | |
| /// Whether fine-grained dependencies experiment is enabled. | |
| final bool withFineDependencies; | |
| /// The [SourceFactory] is used to resolve URIs to paths and restore URIs | |
| /// from file paths. | |
| final SourceFactory _sourceFactory; | |
| /// The container, shared with other drivers within the same collection, | |
| /// into which all drivers record files ownership. | |
| final OwnedFiles? ownedFiles; | |
| /// The declared environment variables. | |
| final DeclaredVariables declaredVariables; | |
| /// The analysis context that created this driver / session. | |
| final DriverBasedAnalysisContext? analysisContext; | |
| /// The salt to mix into all hashes used as keys for unlinked data. | |
| final Uint32List _saltForUnlinked; | |
| /// The salt to mix into all hashes used as keys for elements. | |
| final Uint32List _saltForElements; | |
| /// The salt to mix into all hashes used as keys for linked data. | |
| final Uint32List _saltForResolution; | |
| /// The set of priority files, that should be analyzed sooner. | |
| final _priorityFiles = <String>{}; | |
| /// The file changes that should be applied before processing requests. | |
| final List<FileChange> _pendingFileChanges = []; | |
| /// The completers to complete after [_pendingFileChanges] are applied. | |
| final _pendingFileChangesCompleters = <Completer<List<String>>>[]; | |
| /// The mapping from the files for which analysis was requested using | |
| /// [getResolvedUnit] to the [Completer]s to report the result. | |
| final _requestedFiles = <String, List<Completer<SomeResolvedUnitResult>>>{}; | |
| /// The mapping from the files for which analysis was requested using | |
| /// [getResolvedUnit] which are not interactive to the [Completer]s to report | |
| /// the result. | |
| final _requestedFilesNonInteractive = | |
| <String, List<Completer<SomeResolvedUnitResult>>>{}; | |
| /// The mapping from the files for which analysis was requested using | |
| /// [getResolvedLibrary] to the [Completer]s to report the result. | |
| final _requestedLibraries = | |
| <String, List<Completer<SomeResolvedLibraryResult>>>{}; | |
| /// The queue of requests for completion. | |
| final List<_ResolveForCompletionRequest> _resolveForCompletionRequests = []; | |
| /// The task that incrementally discovers files available to this driver. | |
| _DiscoverAvailableFilesTask? _discoverAvailableFilesTask; | |
| /// The requests to compute files defining a class member with the name. | |
| final _definingClassMemberNameRequests = | |
| <_GetFilesDefiningClassMemberNameRequest>[]; | |
| /// The requests to compute files referencing any of a set of names. | |
| final _referencingNameRequests = <_GetFilesReferencingNamesRequest>[]; | |
| /// The mapping from the files for which errors were requested using | |
| /// [getErrors] to the [Completer]s to report the result. | |
| final _errorsRequestedFiles = <String, List<Completer<SomeErrorsResult>>>{}; | |
| /// The mapping from the files for which the index was requested using | |
| /// [getIndex] to the [Completer]s to report the result. | |
| final _indexRequestedFiles = | |
| <String, List<Completer<AnalysisDriverUnitIndex?>>>{}; | |
| /// The mapping from the files for which the unit element was requested using | |
| /// [getUnitElement] to the [Completer]s to report the result. | |
| final _unitElementRequestedFiles = | |
| <String, List<Completer<SomeUnitElementResult>>>{}; | |
| /// The list of dispose requests, added in [dispose2], almost always empty. | |
| /// We expect that at most one is added, at the very end of the life cycle. | |
| final List<Completer<void>> _disposeRequests = []; | |
| /// Resolution signatures of the most recently produced results for files. | |
| final Map<String, String> _lastProducedSignatures = {}; | |
| /// Key: a file in [LibraryDiagnosticsBundle]. | |
| /// Value: last [LibraryDiagnosticsBundle] ID. | |
| /// | |
| /// Used in [_produceErrors] to avoid producing the same diagnostics. | |
| final Map<File, ManifestItemId> _lastProducedDiagnosticIds = {}; | |
| /// Cached results for [_priorityFiles]. | |
| final Map<String, ResolvedUnitResult> _priorityResults = {}; | |
| /// Cached results of [getResolvedLibrary]. | |
| final Map<String, ResolvedLibraryResultImpl> _resolvedLibraryCache = {}; | |
| /// The controller for the [exceptions] stream. | |
| final StreamController<ExceptionResult> _exceptionController = | |
| StreamController<ExceptionResult>(); | |
| /// The instance of the [Search] helper. | |
| late final Search _search; | |
| final AnalysisDriverTestView? testView; | |
| late final FileSystemState _fsState; | |
| /// The [FileTracker] used by this driver. | |
| late final FileTracker _fileTracker; | |
| /// Whether resolved units should be indexed. | |
| final bool enableIndex; | |
| /// Whether analysis sessions should report inconsistent analysis. | |
| final bool shouldReportInconsistentAnalysisException; | |
| /// The context in which libraries should be analyzed. | |
| LibraryContext? _libraryContext; | |
| /// Whether `dart:core` has been transitively discovered. | |
| bool _hasDartCoreDiscovered = false; | |
| /// This flag is reset to `false` when a new file is added, because it | |
| /// might be a library, so that some files that were disconnected parts | |
| /// could be analyzed now. | |
| bool _hasLibrariesDiscovered = false; | |
| /// If testing data is being retained, a pointer to the object that is | |
| /// retaining the testing data. Otherwise `null`. | |
| final TestingData? testingData; | |
| bool _disposed = false; | |
| /// Whether memory-expensive data has been discarded. | |
| bool _hasDiscardedMemoryExpensiveData = false; | |
| /// A map that associates files to corresponding analysis options. | |
| final AnalysisOptionsMap analysisOptionsMap; | |
| /// Whether timing data should be gathered during lint rule execution. | |
| final bool _enableLintRuleTiming; | |
| /// Create a new instance of [AnalysisDriver]. | |
| /// | |
| /// The given [SourceFactory] is cloned to ensure that it does not contain a | |
| /// reference to an [AnalysisContext] in which it could have been used. | |
| AnalysisDriver({ | |
| required AnalysisDriverScheduler scheduler, | |
| required PerformanceLog logger, | |
| required ResourceProvider resourceProvider, | |
| required ByteStore byteStore, | |
| required SourceFactory sourceFactory, | |
| required Packages packages, | |
| required this.withFineDependencies, | |
| this.ownedFiles, | |
| this.analysisContext, | |
| required this.analysisOptionsMap, | |
| FileContentCache? fileContentCache, | |
| UnlinkedUnitStore? unlinkedUnitStore, | |
| this.enableIndex = false, | |
| this.shouldReportInconsistentAnalysisException = true, | |
| SummaryDataStore? externalSummaries, | |
| DeclaredVariables? declaredVariables, | |
| Version? languageVersionOverride, | |
| bool retainDataForTesting = false, | |
| this.testView, | |
| bool enableLintRuleTiming = false, | |
| }) : _scheduler = scheduler, | |
| _resourceProvider = resourceProvider, | |
| _byteStore = byteStore, | |
| _fileContentCache = | |
| fileContentCache ?? FileContentCache.ephemeral(resourceProvider), | |
| _unlinkedUnitStore = unlinkedUnitStore ?? UnlinkedUnitStoreImpl(), | |
| _logger = logger, | |
| _packages = packages, | |
| _languageVersionOverride = languageVersionOverride, | |
| _sourceFactory = sourceFactory, | |
| _externalSummaries = externalSummaries, | |
| declaredVariables = declaredVariables ?? DeclaredVariables(), | |
| testingData = retainDataForTesting ? TestingData() : null, | |
| _enableLintRuleTiming = enableLintRuleTiming, | |
| _saltForUnlinked = _calculateSaltForUnlinked(enableIndex: enableIndex), | |
| _saltForElements = _calculateSaltForElements( | |
| declaredVariables ?? DeclaredVariables(), | |
| ), | |
| _saltForResolution = _calculateSaltForResolution( | |
| enableIndex: enableIndex, | |
| analysisContext: analysisContext, | |
| declaredVariables: declaredVariables ?? DeclaredVariables(), | |
| ) { | |
| analysisContext?.driver = this; | |
| testView?.driver = this; | |
| _fileContentStrategy = StoredFileContentStrategy(_fileContentCache); | |
| _createFileTracker(); | |
| _scheduler.add(this); | |
| _search = Search(this); | |
| } | |
| /// Return the set of files explicitly added to analysis using [addFile]. | |
| Set<String> get addedFiles => _fileTracker.addedFiles; | |
| /// See [addedFiles]. | |
| Set<File> get addedFiles2 { | |
| return addedFiles.map(resourceProvider.getFile).toSet(); | |
| } | |
| /// Return the current analysis session. | |
| AnalysisSessionImpl get currentSession { | |
| return libraryContext.elementFactory.analysisSession; | |
| } | |
| /// The dartdoc directives in this context. | |
| DartdocDirectiveInfo get dartdocDirectiveInfo { | |
| return _fsState.dartdocDirectiveInfo; | |
| } | |
| /// The set of legacy plugin names enabled in analysis options in this driver. | |
| Set<String> get enabledLegacyPluginNames { | |
| // We currently only support legacy plugins enabled at the very root of a | |
| // context (and we create contexts for any analysis options that changes | |
| // plugins from its parent context). | |
| var rootOptionsFile = analysisContext?.contextRoot.optionsFile; | |
| return rootOptionsFile != null | |
| ? getAnalysisOptionsForFile( | |
| rootOptionsFile, | |
| ).enabledLegacyPluginNames.toSet() | |
| : const {}; | |
| } | |
| /// Return the stream that produces [ExceptionResult]s. | |
| Stream<ExceptionResult> get exceptions => _exceptionController.stream; | |
| /// The current file system state. | |
| FileSystemState get fsState => _fsState; | |
| bool get hasPendingFileChanges => _pendingFileChanges.isNotEmpty; | |
| /// Return the set of files that are known at this moment. This set does not | |
| /// always include all added files or all implicitly used file. If a file has | |
| /// not been processed yet, it might be missing. | |
| Set<FileState> get knownFiles => _fsState.knownFiles; | |
| /// Return the context in which libraries should be analyzed. | |
| LibraryContext get libraryContext { | |
| return _libraryContext ??= LibraryContext( | |
| testData: testView?.libraryContext, | |
| analysisSession: AnalysisSessionImpl(this), | |
| logger: _logger, | |
| byteStore: _byteStore, | |
| eventsController: _scheduler.eventsController, | |
| analysisOptionsMap: analysisOptionsMap, | |
| declaredVariables: declaredVariables, | |
| sourceFactory: _sourceFactory, | |
| packagesFile: analysisContext?.contextRoot.packagesFile, | |
| externalSummaries: _externalSummaries, | |
| fileSystemState: _fsState, | |
| withFineDependencies: withFineDependencies, | |
| ); | |
| } | |
| /// The path of the folder at the root of the context. | |
| String get name => analysisContext?.contextRoot.root.path ?? ''; | |
| /// The number of files scheduled for analysis. | |
| int get numberOfFilesToAnalyze => _fileTracker.numberOfPendingFiles; | |
| /// The plugins options specified in the root analysis options, if any. | |
| PluginsOptions? get pluginsOptions { | |
| // We currently only support plugins which are specified at the root of a | |
| // context. | |
| var rootOptionsFile = analysisContext?.contextRoot.optionsFile; | |
| if (rootOptionsFile == null) { | |
| return null; | |
| } | |
| return getAnalysisOptionsForFile(rootOptionsFile).pluginsOptions; | |
| } | |
| /// The list of files that the driver should try to analyze sooner. | |
| List<String> get priorityFiles => _priorityFiles.toList(growable: false); | |
| /// Sets the list of files that the driver should try to analyze sooner. | |
| /// | |
| /// Every path in the list must be absolute and normalized. | |
| /// | |
| /// The driver will produce the results through the `events` stream. The | |
| /// exact order in which results are produced is not defined, neither | |
| /// between priority files, nor between priority and non-priority files. | |
| set priorityFiles(List<String> priorityPaths) { | |
| _priorityResults.keys | |
| .toSet() | |
| .difference(priorityPaths.toSet()) | |
| .forEach(_priorityResults.remove); | |
| _priorityFiles.clear(); | |
| _priorityFiles.addAll(priorityPaths); | |
| _scheduler.notify(); | |
| } | |
| /// See [priorityFiles]. | |
| set priorityFiles2(List<File> files) { | |
| priorityFiles = files.map((e) => e.path).toList(); | |
| } | |
| /// Return the [ResourceProvider] that is used to access the file system. | |
| ResourceProvider get resourceProvider => _resourceProvider; | |
| AnalysisDriverScheduler get scheduler => _scheduler; | |
| /// Return the search support for the driver. | |
| Search get search => _search; | |
| /// Return the source factory used to resolve URIs to paths and restore URIs | |
| /// from file paths. | |
| SourceFactory get sourceFactory => _sourceFactory; | |
| /// Return the priority of work that the driver needs to perform. | |
| AnalysisDriverPriority get workPriority { | |
| if (_disposeRequests.isNotEmpty) { | |
| return AnalysisDriverPriority.interactive; | |
| } | |
| if (_resolveForCompletionRequests.isNotEmpty) { | |
| return AnalysisDriverPriority.completion; | |
| } | |
| if (_requestedFiles.isNotEmpty) { | |
| return AnalysisDriverPriority.interactive; | |
| } | |
| if (_requestedLibraries.isNotEmpty) { | |
| return AnalysisDriverPriority.interactive; | |
| } | |
| if (_definingClassMemberNameRequests.isNotEmpty) { | |
| return AnalysisDriverPriority.interactive; | |
| } | |
| if (_referencingNameRequests.isNotEmpty) { | |
| return AnalysisDriverPriority.interactive; | |
| } | |
| if (_errorsRequestedFiles.isNotEmpty) { | |
| return AnalysisDriverPriority.interactive; | |
| } | |
| if (_indexRequestedFiles.isNotEmpty) { | |
| return AnalysisDriverPriority.interactive; | |
| } | |
| if (_unitElementRequestedFiles.isNotEmpty) { | |
| return AnalysisDriverPriority.interactive; | |
| } | |
| if (_hasPendingAvailableFilesDiscovery) { | |
| return AnalysisDriverPriority.interactive; | |
| } | |
| if (_priorityFiles.isNotEmpty) { | |
| for (String path in _priorityFiles) { | |
| if (_fileTracker.isFilePending(path)) { | |
| return AnalysisDriverPriority.priority; | |
| } | |
| } | |
| } | |
| if (_requestedFilesNonInteractive.isNotEmpty) { | |
| return AnalysisDriverPriority.general; | |
| } | |
| if (_pendingFileChanges.isNotEmpty) { | |
| return AnalysisDriverPriority.general; | |
| } | |
| if (_fileTracker.hasChangedFiles) { | |
| return AnalysisDriverPriority.changedFiles; | |
| } | |
| if (_fileTracker.hasPendingChangedFiles) { | |
| return AnalysisDriverPriority.generalChanged; | |
| } | |
| if (_fileTracker.hasPendingImportFiles) { | |
| return AnalysisDriverPriority.generalImportChanged; | |
| } | |
| if (_fileTracker.hasPendingErrorFiles) { | |
| return AnalysisDriverPriority.generalWithErrors; | |
| } | |
| if (_fileTracker.hasPendingFiles) { | |
| return AnalysisDriverPriority.general; | |
| } | |
| if (_pendingFileChangesCompleters.isNotEmpty) { | |
| return AnalysisDriverPriority.general; | |
| } | |
| return AnalysisDriverPriority.nothing; | |
| } | |
| /// Whether the driver has a file to analyze. | |
| bool get _hasFilesToAnalyze { | |
| return hasPendingFileChanges || | |
| _fileTracker.hasPendingFiles || | |
| _fileTracker.hasChangedFiles || | |
| _requestedLibraries.isNotEmpty || | |
| _requestedFiles.isNotEmpty || | |
| _requestedFilesNonInteractive.isNotEmpty || | |
| _errorsRequestedFiles.isNotEmpty || | |
| _definingClassMemberNameRequests.isNotEmpty || | |
| _referencingNameRequests.isNotEmpty || | |
| _indexRequestedFiles.isNotEmpty || | |
| _unitElementRequestedFiles.isNotEmpty || | |
| _hasPendingAvailableFilesDiscovery || | |
| _disposeRequests.isNotEmpty; | |
| } | |
| bool get _hasPendingAvailableFilesDiscovery { | |
| return _discoverAvailableFilesTask?.hasPendingWork ?? false; | |
| } | |
| /// Add the file with the given [path] to the set of files that are explicitly | |
| /// being analyzed. | |
| /// | |
| /// The [path] must be absolute and normalized. | |
| /// | |
| /// The results of analysis are eventually produced by the `events` stream. | |
| void addFile(String path) { | |
| _throwIfNotAbsolutePath(path); | |
| if (!_fsState.hasUri(path)) { | |
| return; | |
| } | |
| if (file_paths.isDart(resourceProvider.pathContext, path)) { | |
| _priorityResults.clear(); | |
| _resolvedLibraryCache.clear(); | |
| _pendingFileChanges.add(FileChange(path, FileChangeKind.add)); | |
| _scheduler.notify(); | |
| } | |
| } | |
| /// See [addFile]. | |
| void addFile2(File file) { | |
| addFile(file.path); | |
| } | |
| void afterPerformWork() {} | |
| @visibleForTesting | |
| ResolvedLibraryResultImpl? analyzeFileForTesting(String path) { | |
| return scheduler.accumulatedPerformance.run('analyzeFileForTesting', ( | |
| performance, | |
| ) { | |
| return _analyzeFileImpl(path: path, performance: performance); | |
| }); | |
| } | |
| /// Return a [Future] that completes after pending file changes are applied, | |
| /// so that [currentSession] can be used to compute results. | |
| /// | |
| /// The value is the set of all files that are potentially affected by | |
| /// the pending changes. This set can be both wider than the set of analyzed | |
| /// files (because it may include files imported from other packages, and | |
| /// which are on the import path from a changed file to an analyze file), | |
| /// and narrower than the set of analyzed files (because only files that | |
| /// were previously accessed are considered to be known and affected). | |
| Future<List<String>> applyPendingFileChanges() { | |
| if (_pendingFileChanges.isNotEmpty) { | |
| if (_disposed) throw DisposedAnalysisContextResult(); | |
| var completer = Completer<List<String>>(); | |
| _pendingFileChangesCompleters.add(completer); | |
| return completer.future; | |
| } else { | |
| return Future.value([]); | |
| } | |
| } | |
| /// Builds elements for library files from [uriList], and packs them into | |
| /// a bundle suitable for [PackageBundleReader]. | |
| /// | |
| /// Disconnected non-library files are ignored. | |
| Future<Uint8List> buildPackageBundle({ | |
| required List<Uri> uriList, | |
| PackageBundleSdk? packageBundleSdk, | |
| }) async { | |
| var bundleWriter = BundleWriter(); | |
| var packageBundleBuilder = PackageBundleBuilder(); | |
| for (var uri in uriList) { | |
| var uriStr = uri.toString(); | |
| var libraryResult = await getLibraryByUri(uriStr); | |
| if (libraryResult is LibraryElementResult) { | |
| var libraryElement = libraryResult.element as LibraryElementImpl; | |
| bundleWriter.writeLibraryElement(libraryElement); | |
| var unitUris = libraryElement.internal.allUnitSourceUris(); | |
| packageBundleBuilder.addLibrary( | |
| uriStr, | |
| unitUris.map((uri) => uri.toString()).toList(), | |
| ); | |
| } | |
| } | |
| var writeWriterResult = bundleWriter.finish(); | |
| return packageBundleBuilder.finish( | |
| resolutionBytes: writeWriterResult.resolutionBytes, | |
| sdk: packageBundleSdk, | |
| ); | |
| } | |
| /// The file with the given [path] might have changed - updated, added or | |
| /// removed. Or not, we don't know. Or it might have, but then changed back. | |
| /// | |
| /// The [path] must be absolute and normalized. | |
| /// | |
| /// The [path] can be any file - explicitly or implicitly analyzed, or neither. | |
| /// | |
| /// Causes the analysis state to transition to "working" (if it is not in | |
| /// that state already). Schedules the file contents for [path] to be read | |
| /// into the current file state prior to the next time the analysis state | |
| /// transitions to "idle". | |
| /// | |
| /// Invocation of this method will not prevent a [Future] returned from | |
| /// [getResolvedUnit] from completing with a result, but the result is not | |
| /// guaranteed to be consistent with the new current file state after this | |
| /// [changeFile] invocation. | |
| void changeFile(String path) { | |
| _throwIfNotAbsolutePath(path); | |
| if (!_fsState.hasUri(path)) { | |
| return; | |
| } | |
| if (file_paths.isDart(resourceProvider.pathContext, path)) { | |
| var file = resourceProvider.getFile(path); | |
| _lastProducedSignatures.remove(path); | |
| _lastProducedDiagnosticIds.remove(file); | |
| _priorityResults.clear(); | |
| _resolvedLibraryCache.clear(); | |
| _pendingFileChanges.add(FileChange(path, FileChangeKind.change)); | |
| _scheduler._fileUpdatesStatistics.changedFiles.add(path); | |
| _scheduler.notify(); | |
| } | |
| } | |
| /// See [changeFile]. | |
| void changeFile2(File file) { | |
| changeFile(file.path); | |
| } | |
| /// Clear the library context and any related data structures. Mostly we do | |
| /// this to reduce memory consumption. The library context holds to every | |
| /// library that was resynthesized, but after some initial analysis we might | |
| /// not get again to many of these libraries. So, we should clear the context | |
| /// periodically. | |
| void clearLibraryContext() { | |
| _libraryContext?.dispose(); | |
| _libraryContext = null; | |
| } | |
| void discardMemoryExpensiveData() { | |
| if (_hasDiscardedMemoryExpensiveData) { | |
| return; | |
| } | |
| _hasDiscardedMemoryExpensiveData = true; | |
| currentSession.clearHierarchies(); | |
| libraryContext.elementFactory.discardLibraryManifestInstances(); | |
| } | |
| /// Completes after all potentially available files have been added to | |
| /// [knownFiles]. Discovery is performed incrementally by the driver | |
| /// scheduler so that large or slow package trees do not monopolize the | |
| /// isolate. | |
| Future<void> discoverAvailableFiles() { | |
| var task = _discoverAvailableFilesTask ??= _DiscoverAvailableFilesTask( | |
| this, | |
| ); | |
| var result = task.discoverAvailableFiles(); | |
| if (task.hasPendingWork) { | |
| _scheduler.notify(); | |
| } | |
| return result; | |
| } | |
| /// Completes after files that can be accessed by the [targetFile] have been | |
| /// added to [knownFiles]. | |
| /// | |
| /// For a file in a Pub package, this discovers files from the same package, | |
| /// public files from direct dependencies, and SDK libraries. Other workspace | |
| /// kinds fall back to discovering all available files. | |
| Future<void> discoverAvailableFilesFor(FileState targetFile) { | |
| var task = _discoverAvailableFilesTask ??= _DiscoverAvailableFilesTask( | |
| this, | |
| ); | |
| var result = task.discoverAvailableFilesFor(targetFile); | |
| if (task.hasPendingWork) { | |
| _scheduler.notify(); | |
| } | |
| return result; | |
| } | |
| /// Notify the driver that the client is going to stop using it. | |
| Future<void> dispose2() { | |
| var completer = Completer<void>(); | |
| _disposed = true; | |
| _disposeRequests.add(completer); | |
| // Complete all waiting completers. | |
| for (var completerList in _requestedLibraries.values) { | |
| for (var completer in completerList) { | |
| completer.complete(DisposedAnalysisContextResult()); | |
| } | |
| } | |
| _requestedLibraries.clear(); | |
| for (var completerList in _requestedFiles.values) { | |
| for (var completer in completerList) { | |
| completer.complete(DisposedAnalysisContextResult()); | |
| } | |
| } | |
| _requestedFiles.clear(); | |
| for (var completerList in _requestedFilesNonInteractive.values) { | |
| for (var completer in completerList) { | |
| completer.complete(DisposedAnalysisContextResult()); | |
| } | |
| } | |
| _requestedFilesNonInteractive.clear(); | |
| for (var completerList in _unitElementRequestedFiles.values) { | |
| for (var completer in completerList) { | |
| completer.complete(DisposedAnalysisContextResult()); | |
| } | |
| } | |
| _unitElementRequestedFiles.clear(); | |
| for (var completerList in _errorsRequestedFiles.values) { | |
| for (var completer in completerList) { | |
| completer.complete(DisposedAnalysisContextResult()); | |
| } | |
| } | |
| _errorsRequestedFiles.clear(); | |
| for (var completer in _pendingFileChangesCompleters) { | |
| completer.completeError(DisposedAnalysisContextResult()); | |
| } | |
| _pendingFileChangesCompleters.clear(); | |
| _discoverAvailableFilesTask?.complete(); | |
| _scheduler.notify(); | |
| return completer.future; | |
| } | |
| AnalysisOptionsImpl getAnalysisOptionsForFile(File file) => | |
| analysisOptionsMap[file]; | |
| /// Return the cached [ResolvedUnitResult] for the Dart file with the given | |
| /// [path]. If there is no cached result, return `null`. Usually only results | |
| /// of priority files are cached. | |
| /// | |
| /// The [path] must be absolute and normalized. | |
| /// | |
| /// The [path] can be any file - explicitly or implicitly analyzed, or neither. | |
| ResolvedUnitResult? getCachedResolvedUnit(String path) { | |
| _throwIfNotAbsolutePath(path); | |
| return _priorityResults[path]; | |
| } | |
| /// See [getCachedResolvedUnit]. | |
| ResolvedUnitResult? getCachedResolvedUnit2(File file) { | |
| return _priorityResults[file.path]; | |
| } | |
| /// Return a [Future] that completes with the [ErrorsResult] for the Dart | |
| /// file with the given [path]. | |
| /// | |
| /// The [path] must be absolute and normalized. | |
| /// | |
| /// This method does not use analysis priorities, and must not be used in | |
| /// interactive analysis, such as Analysis Server or its plugins. | |
| Future<SomeErrorsResult> getErrors(String path) { | |
| if (!_isAbsolutePath(path)) { | |
| return Future.value(InvalidPathResult()); | |
| } | |
| if (!_fsState.hasUri(path)) { | |
| return Future.value(NotPathOfUriResult()); | |
| } | |
| if (_disposed) { | |
| return Future.value(DisposedAnalysisContextResult()); | |
| } | |
| // Schedule analysis. | |
| var completer = Completer<SomeErrorsResult>(); | |
| _errorsRequestedFiles.add(path, completer); | |
| _scheduler.notify(); | |
| return completer.future; | |
| } | |
| /// Completes with files that define a class member with the [name]. | |
| Future<List<FileState>> getFilesDefiningClassMemberName(String name) async { | |
| await discoverAvailableFiles(); | |
| var request = _GetFilesDefiningClassMemberNameRequest(name); | |
| _definingClassMemberNameRequests.add(request); | |
| _scheduler.notify(); | |
| return request.completer.future; | |
| } | |
| /// Completes with files that reference any of the given external [names]. | |
| Future<List<FileState>> getFilesReferencingNames(Set<String> names) async { | |
| if (names.isEmpty) { | |
| return const []; | |
| } | |
| await discoverAvailableFiles(); | |
| var request = _GetFilesReferencingNamesRequest(names); | |
| _referencingNameRequests.add(request); | |
| _scheduler.notify(); | |
| return request.completer.future; | |
| } | |
| /// Return the [FileResult] for the Dart file with the given [path]. | |
| /// | |
| /// The [path] must be absolute and normalized. | |
| SomeFileResult getFileSync(String path) { | |
| if (!_isAbsolutePath(path)) { | |
| return InvalidPathResult(); | |
| } | |
| FileState file = _fsState.getFileForPath(path); | |
| return FileResultImpl(session: currentSession, fileState: file); | |
| } | |
| /// See [getFileSync]. | |
| SomeFileResult getFileSync2(File file) { | |
| return getFileSync(file.path); | |
| } | |
| /// Return a [Future] that completes with the [AnalysisDriverUnitIndex] for | |
| /// the file with the given [path], or with `null` if the file cannot be | |
| /// analyzed. | |
| Future<AnalysisDriverUnitIndex?> getIndex(String path) { | |
| _throwIfNotAbsolutePath(path); | |
| if (!enableIndex) { | |
| throw ArgumentError('Indexing is not enabled.'); | |
| } | |
| if (!_fsState.hasUri(path)) { | |
| return Future.value(); | |
| } | |
| // Schedule analysis. | |
| var completer = Completer<AnalysisDriverUnitIndex?>(); | |
| _indexRequestedFiles.add(path, completer); | |
| _scheduler.notify(); | |
| return completer.future; | |
| } | |
| /// See [getIndex]. | |
| Future<AnalysisDriverUnitIndex?> getIndex2(File file) { | |
| return getIndex(file.path); | |
| } | |
| /// Return a [Future] that completes with [LibraryElementResult] for the given | |
| /// [uri], which is either resynthesized from the provided external summary | |
| /// store, or built for a file to which the given [uri] is resolved. | |
| Future<SomeLibraryElementResult> getLibraryByUri(String uri) async { | |
| var uriObj = uriCache.parse(uri); | |
| // Check if the element is already computed. | |
| if (_pendingFileChanges.isEmpty) { | |
| var rootReference = libraryContext.elementFactory.rootReference; | |
| if (rootReference.libraryIfExists(uriObj) case var reference?) { | |
| var element = reference.element; | |
| if (element is LibraryElementImpl) { | |
| return LibraryElementResultImpl(element); | |
| } | |
| } | |
| } | |
| var fileOr = _fsState.getFileForUri(uriObj); | |
| switch (fileOr) { | |
| case null: | |
| return CannotResolveUriResult(); | |
| case UriResolutionFile(:var file): | |
| var kind = file.kind; | |
| if (kind is LibraryFileKind) { | |
| } else if (kind is PartFileKind) { | |
| return NotLibraryButPartResult(); | |
| } else { | |
| throw UnimplementedError('(${kind.runtimeType}) $kind'); | |
| } | |
| var unitResult = await getUnitElement(file.path); | |
| if (unitResult is UnitElementResultImpl) { | |
| return LibraryElementResultImpl(unitResult.fragment.element); | |
| } | |
| // Some invalid results are invalid results for this request. | |
| // Note that up-down promotion does not work. | |
| if (unitResult is InvalidResult && | |
| unitResult is SomeLibraryElementResult) { | |
| return unitResult as SomeLibraryElementResult; | |
| } | |
| // Should not happen. | |
| return UnspecifiedInvalidResult(); | |
| case UriResolutionExternalLibrary(:var source): | |
| var uri = source.uri; | |
| // TODO(scheglov): Check if the source is not for library. | |
| var element = libraryContext.getLibraryElement(uri); | |
| return LibraryElementResultImpl(element); | |
| } | |
| } | |
| /// Return a [ParsedLibraryResult] for the library with the given [path]. | |
| /// | |
| /// The [path] must be absolute and normalized. | |
| SomeParsedLibraryResult getParsedLibrary(String path) { | |
| if (!_isAbsolutePath(path)) { | |
| return InvalidPathResult(); | |
| } | |
| if (!_fsState.hasUri(path)) { | |
| return NotPathOfUriResult(); | |
| } | |
| var file = _fsState.getFileForPath(path); | |
| var kind = file.kind; | |
| if (kind is LibraryFileKind) { | |
| } else if (kind is PartFileKind) { | |
| return NotLibraryButPartResult(); | |
| } else { | |
| throw UnimplementedError('(${kind.runtimeType}) $kind'); | |
| } | |
| var units = <ParsedUnitResult>[]; | |
| for (var unitFile in kind.files) { | |
| var unitPath = unitFile.path; | |
| var unitResult = parseFileSync(unitPath); | |
| if (unitResult is! ParsedUnitResult) { | |
| return UnspecifiedInvalidResult(); | |
| } | |
| units.add(unitResult); | |
| } | |
| return ParsedLibraryResultImpl(session: currentSession, units: units); | |
| } | |
| /// See [getParsedLibrary]. | |
| SomeParsedLibraryResult getParsedLibrary2(File file) { | |
| return getParsedLibrary(file.path); | |
| } | |
| /// Return a [ParsedLibraryResult] for the library with the given [uri]. | |
| SomeParsedLibraryResult getParsedLibraryByUri(Uri uri) { | |
| var fileOr = _fsState.getFileForUri(uri); | |
| return switch (fileOr) { | |
| null => CannotResolveUriResult(), | |
| UriResolutionFile(:var file) => getParsedLibrary(file.path), | |
| UriResolutionExternalLibrary() => UriOfExternalLibraryResult(), | |
| }; | |
| } | |
| /// Return a [Future] that completes with a [ResolvedLibraryResult] for the | |
| /// Dart library file with the given [path]. If the file cannot be analyzed, | |
| /// the [Future] completes with an [InvalidResult]. | |
| /// | |
| /// The [path] must be absolute and normalized. | |
| /// | |
| /// The [path] can be any file - explicitly or implicitly analyzed, or neither. | |
| /// | |
| /// Invocation of this method causes the analysis state to transition to | |
| /// "working" (if it is not in that state already), the driver will produce | |
| /// the resolution result for it, which is consistent with the current file | |
| /// state (including new states of the files previously reported using | |
| /// [changeFile]), prior to the next time the analysis state transitions | |
| /// to "idle". | |
| Future<SomeResolvedLibraryResult> getResolvedLibrary(String path) async { | |
| if (!_isAbsolutePath(path)) { | |
| return InvalidPathResult(); | |
| } | |
| if (!_fsState.hasUri(path)) { | |
| return NotPathOfUriResult(); | |
| } | |
| if (_disposed) { | |
| return Future.value(DisposedAnalysisContextResult()); | |
| } | |
| if (_resolvedLibraryCache[path] case var cached?) { | |
| return cached; | |
| } | |
| var completer = Completer<SomeResolvedLibraryResult>(); | |
| _requestedLibraries.add(path, completer); | |
| _scheduler.notify(); | |
| return completer.future; | |
| } | |
| /// Return a [Future] that completes with a [ResolvedLibraryResult] for the | |
| /// Dart library file with the given [uri]. If the file cannot be analyzed, | |
| /// the [Future] completes with an [InvalidResult]. | |
| /// | |
| /// Invocation of this method causes the analysis state to transition to | |
| /// "working" (if it is not in that state already), the driver will produce | |
| /// the resolution result for it, which is consistent with the current file | |
| /// state (including new states of the files previously reported using | |
| /// [changeFile]), prior to the next time the analysis state transitions | |
| /// to "idle". | |
| Future<SomeResolvedLibraryResult> getResolvedLibraryByUri(Uri uri) async { | |
| var fileOr = _fsState.getFileForUri(uri); | |
| switch (fileOr) { | |
| case null: | |
| return CannotResolveUriResult(); | |
| case UriResolutionFile(:var file): | |
| return getResolvedLibrary(file.path); | |
| case UriResolutionExternalLibrary(): | |
| return UriOfExternalLibraryResult(); | |
| } | |
| } | |
| /// Return a [Future] that completes with a [SomeResolvedUnitResult] for the | |
| /// Dart file with the given [path]. If the file cannot be analyzed, | |
| /// the [Future] completes with an [InvalidResult]. | |
| /// | |
| /// The [path] must be absolute and normalized. | |
| /// | |
| /// The [path] can be any file - explicitly or implicitly analyzed, or neither. | |
| /// | |
| /// If the driver has the cached analysis result for the file, it is returned. | |
| /// If [sendCachedToStream] is `true`, then the result is also reported into | |
| /// the `events` stream, just as if it were freshly computed. | |
| /// | |
| /// Otherwise causes the analysis state to transition to "working" (if it is | |
| /// not in that state already), the driver will produce the analysis result for | |
| /// it, which is consistent with the current file state (including new states | |
| /// of the files previously reported using [changeFile]), prior to the next | |
| /// time the analysis state transitions to "idle". | |
| Future<SomeResolvedUnitResult> getResolvedUnit( | |
| String path, { | |
| bool sendCachedToStream = false, | |
| bool interactive = true, | |
| }) { | |
| if (!_isAbsolutePath(path)) { | |
| return Future.value(InvalidPathResult()); | |
| } | |
| if (!_fsState.hasUri(path)) { | |
| return Future.value(NotPathOfUriResult()); | |
| } | |
| // Return the cached result. | |
| { | |
| ResolvedUnitResult? result = getCachedResolvedUnit(path); | |
| if (result != null) { | |
| if (sendCachedToStream) { | |
| _scheduler.eventsController.add(result); | |
| } | |
| return Future.value(result); | |
| } | |
| } | |
| if (_disposed) { | |
| return Future.value(DisposedAnalysisContextResult()); | |
| } | |
| // Schedule analysis. | |
| var completer = Completer<SomeResolvedUnitResult>(); | |
| if (interactive) { | |
| _requestedFiles.add(path, completer); | |
| } else { | |
| _requestedFilesNonInteractive.add(path, completer); | |
| } | |
| _scheduler.notify(); | |
| return completer.future; | |
| } | |
| /// See [getResolvedUnit]. | |
| Future<SomeResolvedUnitResult> getResolvedUnit2( | |
| File file, { | |
| bool sendCachedToStream = false, | |
| }) { | |
| return getResolvedUnit(file.path, sendCachedToStream: sendCachedToStream); | |
| } | |
| /// Return a [Future] that completes with the [SomeUnitElementResult] | |
| /// for the file with the given [path]. | |
| Future<SomeUnitElementResult> getUnitElement(String path) { | |
| if (!_isAbsolutePath(path)) { | |
| return Future.value(InvalidPathResult()); | |
| } | |
| if (!_fsState.hasUri(path)) { | |
| return Future.value(NotPathOfUriResult()); | |
| } | |
| if (_disposed) { | |
| return Future.value(DisposedAnalysisContextResult()); | |
| } | |
| // Schedule analysis. | |
| var completer = Completer<SomeUnitElementResult>(); | |
| _unitElementRequestedFiles.add(path, completer); | |
| _scheduler.notify(); | |
| return completer.future; | |
| } | |
| /// See [getUnitElement]. | |
| Future<SomeUnitElementResult> getUnitElement2(File file) { | |
| return getUnitElement(file.path); | |
| } | |
| /// Return a [ParsedUnitResult] for the file with the given [path]. | |
| /// | |
| /// The [path] must be absolute and normalized. | |
| /// | |
| /// The [path] can be any file - explicitly or implicitly analyzed, or neither. | |
| /// | |
| /// The parsing is performed in the method itself, and the result is not | |
| /// produced through the `events` stream (just because it is not a fully | |
| /// resolved unit). | |
| SomeParsedUnitResult parseFileSync(String path) { | |
| if (!_isAbsolutePath(path)) { | |
| return InvalidPathResult(); | |
| } | |
| _applyPendingFileChanges(); | |
| var file = _fsState.getFileForPath(path); | |
| RecordingDiagnosticListener listener = RecordingDiagnosticListener(); | |
| CompilationUnit unit = file.parse( | |
| diagnosticListener: listener, | |
| performance: OperationPerformanceImpl('<root>'), | |
| ); | |
| return ParsedUnitResultImpl( | |
| session: currentSession, | |
| fileState: file, | |
| unit: unit, | |
| diagnostics: listener.diagnostics, | |
| ); | |
| } | |
| /// See [parseFileSync]. | |
| SomeParsedUnitResult parseFileSync2(File file) { | |
| return parseFileSync(file.path); | |
| } | |
| /// Perform a single chunk of work and produce `events`. | |
| Future<void> performWork() async { | |
| _discoverDartCore(); | |
| _discoverLibraries(); | |
| if (_resolveForCompletionRequests.removeLastOrNull() case var request?) { | |
| try { | |
| var result = _resolveForCompletion(request); | |
| request.completer.complete(result); | |
| } catch (exception, stackTrace) { | |
| _reportException(request.path, exception, stackTrace, null); | |
| request.completer.completeError(exception, stackTrace); | |
| _clearLibraryContextAfterException(); | |
| } | |
| return; | |
| } | |
| // Analyze a requested file. | |
| if (_requestedFiles.firstKey case var path?) { | |
| _analyzeFile(path); | |
| return; | |
| } | |
| // Analyze a requested library. | |
| if (_requestedLibraries.firstKey case var path?) { | |
| _getResolvedLibrary(path); | |
| return; | |
| } | |
| // Process an error request. | |
| if (_errorsRequestedFiles.firstKey case var path?) { | |
| _getErrors(path); | |
| return; | |
| } | |
| // Process an index request. | |
| if (_indexRequestedFiles.firstKey case var path?) { | |
| _getIndex(path); | |
| return; | |
| } | |
| // Process a unit element request. | |
| if (_unitElementRequestedFiles.firstKey case var path?) { | |
| _getUnitElement(path); | |
| return; | |
| } | |
| // Explicit requests above take precedence over discovery. | |
| if (_discoverAvailableFilesTask case var task?) { | |
| if (task.hasPendingWork) { | |
| task.perform(); | |
| return; | |
| } | |
| } | |
| // Compute files defining a class member. | |
| if (_definingClassMemberNameRequests.removeLastOrNull() case var request?) { | |
| await _getFilesDefiningClassMemberName(request); | |
| return; | |
| } | |
| // Compute files referencing any of a set of names. | |
| if (_referencingNameRequests.removeLastOrNull() case var request?) { | |
| await _getFilesReferencingNames(request); | |
| return; | |
| } | |
| // Analyze a priority file. | |
| for (var path in _priorityFiles) { | |
| if (_fileTracker.isFilePending(path)) { | |
| _analyzeFile(path); | |
| return; | |
| } | |
| } | |
| // Analyze a general file. | |
| if (_fileTracker.anyPendingFile case var path?) { | |
| _produceErrors(path); | |
| return; | |
| } | |
| // Analyze a requested - but not interactivly requested - file. | |
| if (_requestedFilesNonInteractive.firstKey case var path?) { | |
| _analyzeFile(path); | |
| return; | |
| } | |
| } | |
| /// Remove the file with the given [path] from the list of files to analyze. | |
| /// | |
| /// The [path] must be absolute and normalized. | |
| /// | |
| /// The results of analysis of the file might still be produced by the | |
| /// `events` stream. The driver will try to stop producing these results, | |
| /// but does not guarantee this. | |
| void removeFile(String path) { | |
| _throwIfNotAbsolutePath(path); | |
| if (!_fsState.hasUri(path)) { | |
| return; | |
| } | |
| if (file_paths.isDart(resourceProvider.pathContext, path)) { | |
| var file = resourceProvider.getFile(path); | |
| _lastProducedSignatures.remove(path); | |
| _lastProducedDiagnosticIds.remove(file); | |
| _priorityResults.clear(); | |
| _resolvedLibraryCache.clear(); | |
| _pendingFileChanges.add(FileChange(path, FileChangeKind.remove)); | |
| _scheduler._fileUpdatesStatistics.removedFiles.add(path); | |
| _scheduler.notify(); | |
| } | |
| } | |
| /// See [removeFile]. | |
| void removeFile2(File file) { | |
| removeFile(file.path); | |
| } | |
| Future<ResolvedForCompletionResultImpl?> resolveForCompletion({ | |
| required String path, | |
| required int offset, | |
| required OperationPerformanceImpl performance, | |
| }) { | |
| var request = _ResolveForCompletionRequest( | |
| path: path, | |
| offset: offset, | |
| line: null, | |
| column: null, | |
| performance: performance, | |
| ); | |
| _resolveForCompletionRequests.add(request); | |
| _scheduler.notify(); | |
| return request.completer.future; | |
| } | |
| Future<ResolvedForCompletionResultImpl?> resolveForCompletionWithLineColumn({ | |
| required String path, | |
| required int line, | |
| required int column, | |
| required OperationPerformanceImpl performance, | |
| }) { | |
| var request = _ResolveForCompletionRequest( | |
| path: path, | |
| offset: null, | |
| line: line, | |
| column: column, | |
| performance: performance, | |
| ); | |
| _resolveForCompletionRequests.add(request); | |
| _scheduler.notify(); | |
| return request.completer.future; | |
| } | |
| void _analyzeFile(String path) { | |
| scheduler.accumulatedPerformance.run('analyzeFile', (performance) { | |
| _analyzeFileImpl(path: path, performance: performance); | |
| }); | |
| } | |
| ResolvedLibraryResultImpl? _analyzeFileImpl({ | |
| required String path, | |
| required OperationPerformanceImpl performance, | |
| }) { | |
| // We will produce the result for this file, at least. | |
| // And for any other files of the same library. | |
| _fileTracker.fileWasAnalyzed(path); | |
| var file = _fsState.getFileForPath(path); | |
| // Prepare the library - the file itself, or the known library. | |
| var kind = file.kind; | |
| var library = kind.library ?? kind.asLibrary; | |
| // We need the fully resolved unit, or the result is not cached. | |
| return _logger.run('Compute analysis result for $path', () { | |
| _logger.writeln('Work in $name'); | |
| try { | |
| testView?.numOfAnalyzedLibraries++; | |
| _scheduler.eventsController.add( | |
| events.AnalyzeFile(file: file, library: library), | |
| ); | |
| if (_completeRequestsIfMissingSdkLibrary(library)) { | |
| return null; | |
| } | |
| performance.run('libraryContext', (performance) { | |
| libraryContext.loadForResolution( | |
| targetLibrary: library, | |
| performance: performance, | |
| ); | |
| }); | |
| var libraryElement = libraryContext.elementFactory.libraryOfUri2( | |
| library.file.uri, | |
| ); | |
| RequirementsManifest? requirements; | |
| if (withFineDependencies) { | |
| requirements = RequirementsManifest(); | |
| requirements.addExcludedLibraries([library.file.uri]); | |
| globalResultRequirements = requirements; | |
| } | |
| var results = performance.run('LibraryAnalyzer', (performance) { | |
| return LibraryAnalyzer( | |
| library.file.analysisOptions, | |
| declaredVariables, | |
| libraryElement, | |
| libraryContext.elementFactory.analysisSession.inheritanceManager, | |
| library, | |
| performance: performance, | |
| testingData: testingData, | |
| enableLintRuleTiming: _enableLintRuleTiming, | |
| ).analyze(); | |
| }); | |
| // Invoke the test only hook to trigger additional requirements. | |
| if (testFineAfterLibraryAnalyzerHook case var hook?) { | |
| var units = results.map((result) => result.unit).toList(); | |
| hook(units); | |
| } | |
| if (withFineDependencies) { | |
| assert(requirements!.assertSerialization()); | |
| globalResultRequirements?.stopRecording(); | |
| globalResultRequirements = null; | |
| } | |
| var isLibraryWithPriorityFile = _isLibraryWithPriorityFile(library); | |
| var serializedFileResults = <Uri, Uint8List>{}; | |
| var resolvedUnits = <ResolvedUnitResultImpl>[]; | |
| for (var unitResult in results) { | |
| var unitFile = unitResult.file; | |
| var index = enableIndex | |
| ? indexUnit(unitResult.unit) | |
| : AnalysisDriverUnitIndexBuilder(); | |
| var resolvedUnit = _createResolvedUnitImpl( | |
| file: unitFile, | |
| unitResult: unitResult, | |
| ); | |
| resolvedUnits.add(resolvedUnit); | |
| // getResolvedUnit() | |
| _requestedFiles.completeAll(unitFile.path, resolvedUnit); | |
| _requestedFilesNonInteractive.completeAll( | |
| unitFile.path, | |
| resolvedUnit, | |
| ); | |
| // getErrors() | |
| _errorsRequestedFiles.completeAll( | |
| unitFile.path, | |
| _createErrorsResultImpl( | |
| file: unitFile, | |
| diagnostics: unitResult.diagnostics, | |
| ), | |
| ); | |
| // getIndex() | |
| _indexRequestedFiles.completeAll(unitFile.path, index); | |
| var unitSignature = _getResolvedUnitSignature(library, unitFile); | |
| { | |
| var unitKey = _getResolvedUnitKey(unitSignature); | |
| var unitBytes = AnalysisDriverResolvedUnitBuilder( | |
| errors: unitResult.diagnostics | |
| .map((d) => ErrorEncoding.encode(d)) | |
| .toList(), | |
| index: index, | |
| ).toBuffer(); | |
| _byteStore.putGet(unitKey, unitBytes); | |
| serializedFileResults[unitFile.uri] = unitBytes; | |
| } | |
| _fileTracker.fileWasAnalyzed(unitFile.path); | |
| _lastProducedSignatures[path] = unitSignature; | |
| _scheduler.eventsController.add(resolvedUnit); | |
| if (isLibraryWithPriorityFile) { | |
| _priorityResults[unitFile.path] = resolvedUnit; | |
| } | |
| _updateHasErrorOrWarningFlag(unitFile, resolvedUnit.diagnostics); | |
| } | |
| if (withFineDependencies && requirements != null) { | |
| requirements.removeReqForLibs({library.file.uri}); | |
| performance.run('writeLibraryDiagnostics', (performance) { | |
| var key = _getLibraryDiagnosticsKey(library); | |
| var id = ManifestItemId.generate(); | |
| LibraryDiagnosticsBundle.write( | |
| byteStore: _byteStore, | |
| key: key, | |
| id: id, | |
| requirements: requirements!, | |
| serializedFileResults: serializedFileResults, | |
| performance: performance, | |
| ); | |
| LibraryDiagnosticsBundle.writeDigest( | |
| byteStore: _byteStore, | |
| elementFactory: libraryContext.elementFactory, | |
| bundleKey: key, | |
| bundleId: id, | |
| requirements: requirements, | |
| ); | |
| for (var unitResult in results) { | |
| var unitResource = unitResult.file.resource; | |
| _lastProducedDiagnosticIds[unitResource] = id; | |
| } | |
| }); | |
| _scheduler.eventsController.add( | |
| events.AnalyzedLibrary( | |
| library: library, | |
| requirements: requirements, | |
| ), | |
| ); | |
| } | |
| var libraryResult = ResolvedLibraryResultImpl( | |
| session: currentSession, | |
| element: resolvedUnits.first.libraryElement, | |
| units: resolvedUnits, | |
| ); | |
| if (isLibraryWithPriorityFile) { | |
| _resolvedLibraryCache[library.file.path] = libraryResult; | |
| } | |
| // getResolvedLibrary() | |
| _requestedLibraries.completeAll(library.file.path, libraryResult); | |
| // Return the result, full or partial. | |
| _logger.writeln('Computed new analysis result.'); | |
| return libraryResult; | |
| } catch (exception, stackTrace) { | |
| var contextKey = _storeExceptionContext( | |
| path, | |
| library, | |
| exception, | |
| stackTrace, | |
| ); | |
| _reportException(path, exception, stackTrace, contextKey); | |
| // Complete all related requests with an error. | |
| void completeWithError<T>(List<Completer<T>>? completers) { | |
| if (completers != null) { | |
| for (var completer in completers) { | |
| completer.completeError(exception, stackTrace); | |
| } | |
| } | |
| } | |
| // TODO(scheglov): write tests | |
| for (var file in library.files) { | |
| // getResolvedUnit() | |
| completeWithError(_requestedFiles.remove(file.path)); | |
| completeWithError(_requestedFilesNonInteractive.remove(file.path)); | |
| // getErrors() | |
| completeWithError(_errorsRequestedFiles.remove(file.path)); | |
| // getIndex() | |
| completeWithError(_indexRequestedFiles.remove(file.path)); | |
| } | |
| // getResolvedLibrary() | |
| completeWithError(_requestedLibraries.remove(library.file.path)); | |
| _clearLibraryContextAfterException(); | |
| } | |
| return null; | |
| }); | |
| } | |
| void _applyPendingFileChanges() { | |
| var accumulatedAffected = <String>{}; | |
| for (var fileChange in _pendingFileChanges) { | |
| var path = fileChange.path; | |
| _removePotentiallyAffectedLibraries(accumulatedAffected, path); | |
| switch (fileChange.kind) { | |
| case FileChangeKind.add: | |
| _fileTracker.addFile(path); | |
| case FileChangeKind.change: | |
| _fileTracker.changeFile(path); | |
| case FileChangeKind.remove: | |
| _fileTracker.removeFile(path); | |
| _resetAvailableFilesDiscovery(); | |
| // TODO(scheglov): We have to do this because we discard files. | |
| // But this is not right, we need to handle removing better. | |
| clearLibraryContext(); | |
| } | |
| } | |
| _pendingFileChanges.clear(); | |
| // Read files, so that synchronous methods also see new content. | |
| while (_fileTracker.verifyChangedFilesIfNeeded()) {} | |
| if (_pendingFileChangesCompleters.isNotEmpty) { | |
| var completers = _pendingFileChangesCompleters.toList(); | |
| _pendingFileChangesCompleters.clear(); | |
| for (var completer in completers) { | |
| completer.complete(accumulatedAffected.toList()); | |
| } | |
| } | |
| } | |
| /// There was an exception during a file analysis, we don't know why. | |
| /// But it might have been caused by an inconsistency of files state, and | |
| /// the library context state. Reset the library context, and hope that | |
| /// we will solve the inconsistency while loading / building summaries. | |
| void _clearLibraryContextAfterException() { | |
| clearLibraryContext(); | |
| _priorityResults.clear(); | |
| _resolvedLibraryCache.clear(); | |
| } | |
| /// Completes pending requests if a required SDK library is missing. | |
| /// | |
| /// If a required SDK library is missing, this completes all pending requests | |
| /// for the [library] and returns `true`. | |
| bool _completeRequestsIfMissingSdkLibrary(LibraryFileKind library) { | |
| bool hasLibraryByUri(Uri uri) { | |
| var fileOr = _fsState.getFileForUri(uri); | |
| return switch (fileOr) { | |
| null => false, | |
| UriResolutionFile(:var file) => file.exists, | |
| UriResolutionExternalLibrary() => true, | |
| }; | |
| } | |
| void emitMissingSdkLibraryResult(Uri missingUri) { | |
| var result = MissingSdkLibraryResultImpl(missingUri: missingUri); | |
| _requestedLibraries.completeAll(library.file.path, result); | |
| for (var file in library.files) { | |
| _fileTracker.fileWasAnalyzed(file.path); | |
| _errorsRequestedFiles.completeAll(file.path, result); | |
| _requestedFiles.completeAll(file.path, result); | |
| _requestedFilesNonInteractive.completeAll(file.path, result); | |
| _unitElementRequestedFiles.completeAll(file.path, result); | |
| _indexRequestedFiles.completeAll(file.path, null); | |
| } | |
| } | |
| var dartCore = uriCache.parse('dart:core'); | |
| if (!hasLibraryByUri(dartCore)) { | |
| emitMissingSdkLibraryResult(dartCore); | |
| return true; | |
| } | |
| var dartAsync = uriCache.parse('dart:async'); | |
| if (!hasLibraryByUri(dartAsync)) { | |
| emitMissingSdkLibraryResult(dartAsync); | |
| return true; | |
| } | |
| return false; | |
| } | |
| ErrorsResultImpl _createErrorsResultFromBytes( | |
| FileState file, | |
| LibraryFileKind library, | |
| Uint8List bytes, | |
| ) { | |
| _scheduler.eventsController.add( | |
| events.GetErrorsFromBytes(file: file, library: library), | |
| ); | |
| var unit = AnalysisDriverResolvedUnit.fromBuffer(bytes); | |
| var diagnostics = _getDiagnosticsFromSerialized(file, unit.errors); | |
| _updateHasErrorOrWarningFlag(file, diagnostics); | |
| var result = _createErrorsResultImpl(file: file, diagnostics: diagnostics); | |
| return result; | |
| } | |
| ErrorsResultImpl _createErrorsResultImpl({ | |
| required FileState file, | |
| required List<Diagnostic> diagnostics, | |
| }) { | |
| return ErrorsResultImpl( | |
| session: currentSession, | |
| file: file.resource, | |
| content: file.content, | |
| lineInfo: file.lineInfo, | |
| uri: file.uri, | |
| isLibrary: file.kind is LibraryFileKind, | |
| isPart: file.kind is PartFileKind, | |
| diagnostics: diagnostics, | |
| analysisOptions: file.analysisOptions, | |
| ); | |
| } | |
| /// Creates new [FileSystemState] and [FileTracker] objects. | |
| /// | |
| /// This is used on initial construction. | |
| void _createFileTracker() { | |
| var featureSetProvider = FeatureSetProvider.build( | |
| sourceFactory: sourceFactory, | |
| resourceProvider: _resourceProvider, | |
| packages: _packages, | |
| ); | |
| _fsState = FileSystemState( | |
| _byteStore, | |
| _resourceProvider, | |
| name, | |
| sourceFactory, | |
| analysisContext?.contextRoot.workspace, | |
| declaredVariables, | |
| _saltForUnlinked, | |
| _saltForElements, | |
| featureSetProvider, | |
| analysisOptionsMap, | |
| languageVersionOverride: _languageVersionOverride, | |
| fileContentStrategy: _fileContentStrategy, | |
| unlinkedUnitStore: _unlinkedUnitStore, | |
| prefetchFiles: null, | |
| isGenerated: (_) => false, | |
| onNewFile: _onNewFile, | |
| testData: testView?.fileSystem, | |
| withFineDependencies: withFineDependencies, | |
| ); | |
| _fileTracker = FileTracker(_logger, _fsState, _fileContentStrategy); | |
| } | |
| ResolvedUnitResultImpl _createResolvedUnitImpl({ | |
| required FileState file, | |
| required UnitAnalysisResult unitResult, | |
| }) { | |
| return ResolvedUnitResultImpl( | |
| session: currentSession, | |
| fileState: file, | |
| unit: unitResult.unit, | |
| diagnostics: unitResult.diagnostics, | |
| ); | |
| } | |
| /// When we look at a part that has a `part of name;` directive, we | |
| /// usually don't know the library (in contrast to `part of uri;`). | |
| /// So, we have no choice than to resolve this part as its own library. | |
| /// | |
| /// But parts of `dart:xyz` libraries are special. The reason is that | |
| /// `dart:core` is always implicitly imported. So, when we start building | |
| /// the library cycle of such "part as a library", we discover `dart:core`, | |
| /// and see that it contains our part. So, we don't add it as a library on | |
| /// its own. But have already committed that it is a library. This causes | |
| /// an exception in `LinkedElementFactory`. | |
| /// | |
| /// The current workaround for this is to discover `dart:core` before any | |
| /// analysis. | |
| void _discoverDartCore() { | |
| if (_hasDartCoreDiscovered) { | |
| return; | |
| } | |
| _hasDartCoreDiscovered = true; | |
| var dartCoreUri = uriCache.parse('dart:core'); | |
| var dartCoreResolution = _fsState.getFileForUri(dartCoreUri); | |
| if (dartCoreResolution is UriResolutionFile) { | |
| var dartCoreFile = dartCoreResolution.file; | |
| dartCoreFile.kind.discoverReferencedFiles(); | |
| } | |
| } | |
| void _discoverLibraries() { | |
| if (_hasLibrariesDiscovered) { | |
| return; | |
| } | |
| _hasLibrariesDiscovered = true; | |
| for (var path in _fileTracker.addedFiles) { | |
| _fsState.getFileForPath(path); | |
| } | |
| } | |
| /// Return [Diagnostic]s for the given [serialized] diagnostics. | |
| List<Diagnostic> _getDiagnosticsFromSerialized( | |
| FileState file, | |
| List<AnalysisDriverUnitError> serialized, | |
| ) { | |
| List<Diagnostic> diagnostics = <Diagnostic>[]; | |
| for (AnalysisDriverUnitError error in serialized) { | |
| var diagnostic = ErrorEncoding.decode(file.source, error); | |
| if (diagnostic != null) { | |
| diagnostics.add(diagnostic); | |
| } | |
| } | |
| return diagnostics; | |
| } | |
| void _getErrors(String path) { | |
| scheduler.accumulatedPerformance.run('getErrors', (performance) { | |
| var file = _fsState.getFileForPath(path); | |
| // Prepare the library - the file itself, or the known library. | |
| var kind = file.kind; | |
| var library = kind.library ?? kind.asLibrary; | |
| if (_completeRequestsIfMissingSdkLibrary(library)) { | |
| return; | |
| } | |
| // Errors are based on elements, so load them. | |
| performance.run('libraryContext', (performance) { | |
| libraryContext.loadForResolution( | |
| targetLibrary: library, | |
| performance: performance, | |
| ); | |
| }); | |
| if (withFineDependencies) { | |
| var bundle = _getLibraryDiagnosticsBundle( | |
| library: library, | |
| performance: performance, | |
| ); | |
| if (bundle != null) { | |
| var bytes = bundle.serializedFileResults[file.uri]!; | |
| var result = _createErrorsResultFromBytes(file, library, bytes); | |
| _errorsRequestedFiles.completeAll(path, result); | |
| return; | |
| } | |
| } | |
| // Prepare the signature and key. | |
| var signature = _getResolvedUnitSignature(library, file); | |
| var key = _getResolvedUnitKey(signature); | |
| var bytes = _byteStore.get(key); | |
| if (bytes != null) { | |
| var result = _createErrorsResultFromBytes(file, library, bytes); | |
| _errorsRequestedFiles.completeAll(path, result); | |
| return; | |
| } | |
| performance.run('analyzeFile', (performance) { | |
| _analyzeFileImpl(path: path, performance: performance); | |
| }); | |
| }); | |
| } | |
| Future<void> _getFilesDefiningClassMemberName( | |
| _GetFilesDefiningClassMemberNameRequest request, | |
| ) async { | |
| var result = <FileState>[]; | |
| for (var file in knownFiles) { | |
| if (file.definedClassMemberNames.contains(request.name)) { | |
| result.add(file); | |
| } | |
| } | |
| request.completer.complete(result); | |
| } | |
| Future<void> _getFilesReferencingNames( | |
| _GetFilesReferencingNamesRequest request, | |
| ) async { | |
| var result = <FileState>[]; | |
| for (var file in knownFiles) { | |
| if (request.names.any(file.referencedNames.contains)) { | |
| result.add(file); | |
| } | |
| } | |
| request.completer.complete(result); | |
| } | |
| void _getIndex(String path) { | |
| scheduler.accumulatedPerformance.run('getIndex', (performance) { | |
| var file = _fsState.getFileForPath(path); | |
| // Prepare the library - the file itself, or the known library. | |
| var kind = file.kind; | |
| var library = kind.library ?? kind.asLibrary; | |
| if (_completeRequestsIfMissingSdkLibrary(library)) { | |
| return; | |
| } | |
| // Index is based on elements, so load them. | |
| performance.run('libraryContext', (performance) { | |
| libraryContext.load(targetLibrary: library, performance: performance); | |
| }); | |
| if (withFineDependencies) { | |
| var bundle = _getLibraryDiagnosticsBundle( | |
| library: library, | |
| performance: performance, | |
| ); | |
| if (bundle != null) { | |
| var bytes = bundle.serializedFileResults[file.uri]!; | |
| var unit = AnalysisDriverResolvedUnit.fromBuffer(bytes); | |
| _indexRequestedFiles.completeAll(path, unit.index!); | |
| return; | |
| } | |
| } | |
| // Prepare the signature and key. | |
| var signature = _getResolvedUnitSignature(library, file); | |
| var key = _getResolvedUnitKey(signature); | |
| var bytes = _byteStore.get(key); | |
| if (bytes != null) { | |
| var unit = AnalysisDriverResolvedUnit.fromBuffer(bytes); | |
| _indexRequestedFiles.completeAll(path, unit.index!); | |
| return; | |
| } | |
| performance.run('analyzeFile', (performance) { | |
| _analyzeFileImpl(path: path, performance: performance); | |
| }); | |
| }); | |
| } | |
| /// Returns the [LibraryDiagnosticsBundle] for the given [library]. | |
| /// | |
| /// The bundle is returned if it is present in the cache, and its | |
| /// requirements are satisfied. The cache here is in-memory, or the | |
| /// byte store. | |
| /// | |
| /// Returns `null` if the bundle is not available, or its requirements | |
| /// are not satisfied. In this case the client should re-analyze the | |
| /// library to produce a new bundle. | |
| LibraryDiagnosticsBundle? _getLibraryDiagnosticsBundle({ | |
| required LibraryFileKind library, | |
| required OperationPerformanceImpl performance, | |
| }) { | |
| return performance.run('getLibraryDiagnosticsBundle', (performance) { | |
| var bundleKey = _getLibraryDiagnosticsKey(library); | |
| var bundle = performance.run('readFromBytes', (performance) { | |
| var bytes = _byteStore.get(bundleKey); | |
| if (bytes != null) { | |
| performance.getDataInt('hasBytes').increment(); | |
| performance.getDataInt('bytes').add(bytes.length); | |
| return LibraryDiagnosticsBundle.fromBytes(bytes); | |
| } else { | |
| performance.getDataInt('noBytes').increment(); | |
| } | |
| return null; | |
| }); | |
| if (bundle == null) { | |
| return null; | |
| } | |
| var elementFactory = libraryContext.elementFactory; | |
| if (performance.run('checkDigest', (performance) { | |
| return bundle.isDigestSatisfied( | |
| byteStore: _byteStore, | |
| elementFactory: elementFactory, | |
| bundleKey: bundleKey, | |
| ); | |
| })) { | |
| return bundle; | |
| } | |
| var failure = performance.run('checkRequirements', (performance) { | |
| return bundle.requirements.isSatisfied( | |
| elementFactory: elementFactory, | |
| performance: performance, | |
| ); | |
| }); | |
| scheduler.eventsController.add( | |
| events.CheckLibraryDiagnosticsRequirements( | |
| library: library, | |
| failure: failure, | |
| ), | |
| ); | |
| if (failure != null) { | |
| performance.getDataInt(failure.statisticKey).increment(); | |
| _scheduler | |
| ._workingStatistics | |
| ?.libraryDiagnosticsBundleRequirementsFailures | |
| .update(failure.kindId, (value) => value + 1, ifAbsent: () => 1); | |
| return null; | |
| } | |
| // Requirements satisfied; refresh the fast-path digest entry. | |
| LibraryDiagnosticsBundle.writeDigest( | |
| byteStore: _byteStore, | |
| elementFactory: elementFactory, | |
| bundleKey: bundleKey, | |
| bundleId: bundle.id, | |
| requirements: bundle.requirements, | |
| ); | |
| return bundle; | |
| }); | |
| } | |
| /// Returns the key for the [LibraryDiagnosticsBundle] in the byte store. | |
| /// | |
| /// The key is a signature that incorporates the analysis options, the content | |
| /// of the `pubspec.yaml` file, and the paths, URIs, and content hashes of all | |
| /// files that are part of the [library]. This ensures that the cached bundle | |
| /// is invalidated if any of these inputs change. | |
| String _getLibraryDiagnosticsKey(LibraryFileKind library) { | |
| if (library.diagnosticsBundleKey case var key?) { | |
| return key; | |
| } | |
| var keyBuilder = ApiSignature(); | |
| keyBuilder.addInt(AnalysisDriver.DATA_VERSION); | |
| keyBuilder.addUint32List(_saltForResolution); | |
| keyBuilder.addUint32List(library.file.analysisOptions.signature); | |
| if (library.file.workspacePackage case PubPackage pubPackage) { | |
| keyBuilder.addString(pubPackage.pubspecContent ?? ''); | |
| } | |
| var sortedFiles = library.files.sortedBy((f) => f.path); | |
| for (var file in sortedFiles) { | |
| keyBuilder.addString(file.path); | |
| keyBuilder.addString(file.uriStr); | |
| keyBuilder.addString(file.contentHash); | |
| keyBuilder.addLanguageVersion(file.packageLanguageVersion); | |
| file.kind.addDirectivesSignature(keyBuilder); | |
| } | |
| var key = '${keyBuilder.toHex()}.resolved2'; | |
| return library.diagnosticsBundleKey = key; | |
| } | |
| /// Completes the [getResolvedLibrary] request. | |
| void _getResolvedLibrary(String path) { | |
| var file = _fsState.getFileForPath(path); | |
| var kind = file.kind; | |
| switch (kind) { | |
| case LibraryFileKind(): | |
| break; | |
| case PartFileKind(): | |
| _requestedLibraries.completeAll(path, NotLibraryButPartResult()); | |
| return; | |
| default: | |
| throw UnimplementedError('(${kind.runtimeType}) $kind'); | |
| } | |
| if (_resolvedLibraryCache[path] case var cached?) { | |
| _requestedLibraries.completeAll(path, cached); | |
| return; | |
| } | |
| _analyzeFile(path); | |
| } | |
| /// Return the key to store fully resolved results for the [signature]. | |
| String _getResolvedUnitKey(String signature) { | |
| return '$signature.resolved'; | |
| } | |
| /// Return the signature that identifies fully resolved results for the [file] | |
| /// in the [library], e.g. element model, errors, index, etc. | |
| String _getResolvedUnitSignature(LibraryFileKind library, FileState file) { | |
| ApiSignature signature = ApiSignature(); | |
| signature.addUint32List(_saltForResolution); | |
| if (file.workspacePackage case PubPackage pubPackage) { | |
| signature.addString(pubPackage.pubspecContent ?? ''); | |
| } | |
| signature.addString(library.file.uriStr); | |
| signature.addString(library.libraryCycle.apiSignature); | |
| // Doc imports are not dependencies of the library cycle, but documentation | |
| // comments are resolved against the doc-imported libraries. | |
| for (var importedLibrary in library.docImportedLibraries) { | |
| signature.addString(importedLibrary.libraryCycle.apiSignature); | |
| } | |
| signature.addUint32List(library.file.analysisOptions.signature); | |
| signature.addString(file.uriStr); | |
| signature.addString(file.contentHash); | |
| return signature.toHex(); | |
| } | |
| void _getUnitElement(String path) { | |
| scheduler.accumulatedPerformance.run('getUnitElement', (performance) { | |
| var file = _fsState.getFileForPath(path); | |
| // Prepare the library - the file itself, or the known library. | |
| var kind = file.kind; | |
| var library = kind.library ?? kind.asLibrary; | |
| if (_completeRequestsIfMissingSdkLibrary(library)) { | |
| return; | |
| } | |
| performance.run('libraryContext', (performance) { | |
| libraryContext.load(targetLibrary: library, performance: performance); | |
| }); | |
| var fragment = libraryContext.computeUnitElement(library, file); | |
| var result = UnitElementResultImpl( | |
| session: currentSession, | |
| fileState: file, | |
| fragment: fragment, | |
| ); | |
| _unitElementRequestedFiles.completeAll(path, result); | |
| }); | |
| } | |
| bool _isAbsolutePath(String path) { | |
| return _resourceProvider.pathContext.isAbsolute(path); | |
| } | |
| bool _isLibraryWithPriorityFile(LibraryFileKind library) { | |
| for (var file in library.files) { | |
| if (_priorityFiles.contains(file.path)) { | |
| return true; | |
| } | |
| } | |
| return false; | |
| } | |
| Future<void> _maybeDispose() async { | |
| if (_disposeRequests.isNotEmpty) { | |
| _scheduler.remove(this); | |
| clearLibraryContext(); | |
| for (var completer in _disposeRequests.toList()) { | |
| completer.complete(); | |
| } | |
| } | |
| } | |
| void _onNewFile(FileState file) { | |
| var ownedFiles = this.ownedFiles; | |
| if (ownedFiles != null) { | |
| if (addedFiles.contains(file.path)) { | |
| ownedFiles.addAdded(file.resource, this); | |
| } else { | |
| ownedFiles.addKnown(file.resource, this); | |
| } | |
| } | |
| } | |
| void _produceErrors(String path) { | |
| var workingStatistics = _scheduler._workingStatistics; | |
| workingStatistics?.produceErrorsTimer.start(); | |
| try { | |
| scheduler.accumulatedPerformance.run('produceErrors', (performance) { | |
| var file = _fsState.getFileForPath(path); | |
| // Prepare the library - the file itself, or the known library. | |
| var kind = file.kind; | |
| var library = kind.library ?? kind.asLibrary; | |
| if (workingStatistics case var workingStatistics?) { | |
| for (var file in library.files) { | |
| workingStatistics.produceErrorsPotentialFileCount++; | |
| workingStatistics.produceErrorsPotentialFileLineCount += | |
| file.lineInfo.lineCount; | |
| } | |
| } | |
| if (_completeRequestsIfMissingSdkLibrary(library)) { | |
| return; | |
| } | |
| // Errors are based on elements, so load them. | |
| workingStatistics?.produceErrorsElementsTimer.start(); | |
| try { | |
| performance.run('libraryContext', (performance) { | |
| libraryContext.loadForResolution( | |
| targetLibrary: library, | |
| performance: performance, | |
| ); | |
| }); | |
| } finally { | |
| workingStatistics?.produceErrorsElementsTimer.stop(); | |
| } | |
| if (withFineDependencies) { | |
| var bundle = _getLibraryDiagnosticsBundle( | |
| library: library, | |
| performance: performance, | |
| ); | |
| if (bundle != null) { | |
| for (var fileEntry in bundle.serializedFileResults.entries) { | |
| var file = library.files | |
| .where((file) => file.uri == fileEntry.key) | |
| .single; | |
| _fileTracker.fileWasAnalyzed(file.path); | |
| if (_lastProducedDiagnosticIds[file.resource] == bundle.id) { | |
| // Don't produce diagnostics if the same as the last time. | |
| } else { | |
| var result = _createErrorsResultFromBytes( | |
| file, | |
| library, | |
| fileEntry.value, | |
| ); | |
| _errorsRequestedFiles.completeAll(file.path, result); | |
| _scheduler.eventsController.add(result); | |
| } | |
| } | |
| return; | |
| } | |
| } else { | |
| // Check if we have cached errors for all library files. | |
| List<(FileState, String, Uint8List)>? forAllFiles = []; | |
| for (var file in library.files) { | |
| // If the file is priority, we need the resolved unit. | |
| // So, the cached errors is not enough. | |
| if (priorityFiles.contains(file.path)) { | |
| forAllFiles = null; | |
| break; | |
| } | |
| var signature = _getResolvedUnitSignature(library, file); | |
| var key = _getResolvedUnitKey(signature); | |
| var bytes = _byteStore.get(key); | |
| if (bytes == null) { | |
| forAllFiles = null; | |
| break; | |
| } | |
| // Will not be `null` here. | |
| forAllFiles?.add((file, signature, bytes)); | |
| } | |
| // If we have results for all library files, produce them. | |
| if (forAllFiles != null) { | |
| for (var (file, signature, bytes) in forAllFiles) { | |
| // We have the result for this file. | |
| _fileTracker.fileWasAnalyzed(file.path); | |
| // Don't produce the result if the signature is the same. | |
| if (_lastProducedSignatures[file.path] == signature) { | |
| continue; | |
| } | |
| // Produce the result from bytes. | |
| var result = _createErrorsResultFromBytes(file, library, bytes); | |
| _lastProducedSignatures[file.path] = signature; | |
| _errorsRequestedFiles.completeAll(file.path, result); | |
| _scheduler.eventsController.add(result); | |
| } | |
| // We produced all results for the library. | |
| return; | |
| } | |
| } | |
| if (workingStatistics case var workingStatistics?) { | |
| for (var file in library.files) { | |
| workingStatistics.produceErrorsActualFileCount++; | |
| workingStatistics.produceErrorsActualFileLineCount += | |
| file.lineInfo.lineCount; | |
| } | |
| } | |
| performance.run('analyzeFile', (performance) { | |
| _analyzeFileImpl(path: path, performance: performance); | |
| }); | |
| }); | |
| } finally { | |
| workingStatistics?.produceErrorsTimer.stop(); | |
| } | |
| } | |
| void _removePotentiallyAffectedLibraries( | |
| Set<String> accumulatedAffected, | |
| String path, | |
| ) { | |
| var affected = <FileState>{}; | |
| _fsState.collectAffected(path, affected); | |
| var removedKeys = <String>{}; | |
| _libraryContext?.remove(affected, removedKeys); | |
| // TODO(scheglov): Eventually list of `LibraryOrAugmentationFileKind`. | |
| for (var file in affected) { | |
| var kind = file.kind; | |
| if (kind is LibraryFileKind) { | |
| kind.disposeLibraryCycle(); | |
| } | |
| accumulatedAffected.add(file.path); | |
| } | |
| if (_fsState.getExistingFromPath(path) case var changedFile?) { | |
| var changedLibrary = changedFile.kind.library; | |
| changedLibrary?.invalidateDiagnosticsBundleKey(); | |
| } | |
| _libraryContext?.elementFactory.replaceAnalysisSession( | |
| AnalysisSessionImpl(this), | |
| ); | |
| } | |
| void _reportException( | |
| String path, | |
| Object exception, | |
| StackTrace stackTrace, | |
| String? contextKey, | |
| ) { | |
| CaughtException caught = CaughtException(exception, stackTrace); | |
| var fileContentMap = <String, String>{}; | |
| try { | |
| var file = _fsState.getFileForPath(path); | |
| var fileKind = file.kind; | |
| var libraryKind = fileKind.library; | |
| if (libraryKind != null) { | |
| for (var file in libraryKind.files) { | |
| fileContentMap[file.path] = file.content; | |
| } | |
| } else { | |
| var file = fileKind.file; | |
| fileContentMap[file.path] = file.content; | |
| } | |
| } catch (_) { | |
| // We might get an exception while parsing to access parts. | |
| // Ignore, continue with the exception that we are reporting now. | |
| } | |
| if (exception is CaughtExceptionWithFiles) { | |
| for (var nested in exception.fileContentMap.entries) { | |
| fileContentMap['nested-${nested.key}'] = nested.value; | |
| } | |
| } | |
| _exceptionController.add( | |
| ExceptionResult( | |
| filePath: path, | |
| fileContentMap: fileContentMap, | |
| exception: caught, | |
| contextKey: contextKey, | |
| ), | |
| ); | |
| } | |
| void _resetAvailableFilesDiscovery() { | |
| var task = _discoverAvailableFilesTask; | |
| if (task == null) { | |
| return; | |
| } | |
| if (task.hasPendingWork) { | |
| task.reset(); | |
| } else { | |
| _discoverAvailableFilesTask = null; | |
| } | |
| } | |
| ResolvedForCompletionResultImpl? _resolveForCompletion( | |
| _ResolveForCompletionRequest request, | |
| ) { | |
| return request.performance.run('body', (performance) { | |
| var path = request.path; | |
| if (!_isAbsolutePath(path)) { | |
| return null; | |
| } | |
| if (!_fsState.hasUri(path)) { | |
| return null; | |
| } | |
| var file = _fsState.getFileForPath(path); | |
| int? getOffsetFromLineColumn() { | |
| var line = request.line; | |
| var column = request.column; | |
| if (line == null || column == null) return null; | |
| // line is zero-based so cannot equal lineCount | |
| if (line >= file.lineInfo.lineCount) { | |
| return null; | |
| } | |
| return file.lineInfo.getOffsetOfLine(line) + column; | |
| } | |
| int? offset = request.offset ?? getOffsetFromLineColumn(); | |
| if (offset == null) return null; | |
| // Prepare the library - the file itself, or the known library. | |
| var kind = file.kind; | |
| var library = kind.library ?? kind.asLibrary; | |
| performance.run('libraryContext', (performance) { | |
| libraryContext.load(targetLibrary: library, performance: performance); | |
| }); | |
| var libraryFragment = libraryContext.computeUnitElement(library, file); | |
| var libraryElement = libraryContext.elementFactory.libraryOfUri2( | |
| library.file.uri, | |
| ); | |
| var analysisResult = performance.run('LibraryAnalyzer', (performance) { | |
| return LibraryAnalyzer( | |
| library.file.analysisOptions, | |
| declaredVariables, | |
| libraryElement, | |
| libraryContext.elementFactory.analysisSession.inheritanceManager, | |
| library, | |
| performance: OperationPerformanceImpl('<root>'), | |
| testingData: testingData, | |
| ).analyzeForCompletion( | |
| file: file, | |
| offset: offset, | |
| libraryFragment: libraryFragment, | |
| performance: performance, | |
| ); | |
| }); | |
| return ResolvedForCompletionResultImpl( | |
| analysisSession: currentSession, | |
| fileState: file, | |
| path: path, | |
| uri: file.uri, | |
| exists: file.exists, | |
| content: file.content, | |
| lineInfo: file.lineInfo, | |
| parsedUnit: analysisResult.parsedUnit, | |
| libraryFragment: libraryFragment, | |
| resolvedNodes: analysisResult.resolvedNodes, | |
| ); | |
| }); | |
| } | |
| String? _storeExceptionContext( | |
| String path, | |
| LibraryFileKind library, | |
| Object exception, | |
| StackTrace stackTrace, | |
| ) { | |
| if (allowedNumberOfContextsToWrite <= 0) { | |
| return null; | |
| } else { | |
| allowedNumberOfContextsToWrite--; | |
| } | |
| try { | |
| var contextFiles = library.files | |
| .map( | |
| (file) => AnalysisDriverExceptionFileBuilder( | |
| path: file.path, | |
| content: file.content, | |
| ), | |
| ) | |
| .toList(); | |
| contextFiles.sort((a, b) => a.path.compareTo(b.path)); | |
| AnalysisDriverExceptionContextBuilder contextBuilder = | |
| AnalysisDriverExceptionContextBuilder( | |
| path: path, | |
| exception: exception.toString(), | |
| stackTrace: stackTrace.toString(), | |
| files: contextFiles, | |
| ); | |
| var bytes = contextBuilder.toBuffer(); | |
| String twoDigits(int n) { | |
| if (n >= 10) return '$n'; | |
| return '0$n'; | |
| } | |
| String threeDigits(int n) { | |
| if (n >= 100) return '$n'; | |
| if (n >= 10) return '0$n'; | |
| return '00$n'; | |
| } | |
| DateTime time = DateTime.now(); | |
| String m = twoDigits(time.month); | |
| String d = twoDigits(time.day); | |
| String h = twoDigits(time.hour); | |
| String min = twoDigits(time.minute); | |
| String sec = twoDigits(time.second); | |
| String ms = threeDigits(time.millisecond); | |
| String key = 'exception_${time.year}$m${d}_$h$min${sec}_$ms'; | |
| _byteStore.putGet(key, bytes); | |
| return key; | |
| } catch (_) { | |
| return null; | |
| } | |
| } | |
| /// The driver supports only absolute paths, this method is used to validate | |
| /// any input paths to prevent errors later. | |
| void _throwIfNotAbsolutePath(String path) { | |
| if (!_isAbsolutePath(path)) { | |
| throw ArgumentError('Only absolute paths are supported: $path'); | |
| } | |
| } | |
| /// Given the list of [diagnostics] for the [file], update the [file]'s | |
| /// [FileState.hasErrorOrWarning] flag. | |
| void _updateHasErrorOrWarningFlag( | |
| FileState file, | |
| List<Diagnostic> diagnostics, | |
| ) { | |
| for (var diagnostic in diagnostics) { | |
| var severity = diagnostic.diagnosticCode.severity; | |
| if (severity == DiagnosticSeverity.ERROR) { | |
| file.hasErrorOrWarning = true; | |
| return; | |
| } | |
| } | |
| file.hasErrorOrWarning = false; | |
| } | |
| static void _addDeclaredVariablesToSignature( | |
| ApiSignature buffer, | |
| DeclaredVariables declaredVariables, | |
| ) { | |
| buffer.addInt(declaredVariables.variableNames.length); | |
| for (var name in declaredVariables.variableNames) { | |
| var value = declaredVariables.get(name); | |
| buffer.addString(name); | |
| buffer.addString(value!); | |
| } | |
| } | |
| static Uint32List _calculateSaltForElements( | |
| DeclaredVariables declaredVariables, | |
| ) { | |
| var buffer = ApiSignature(); | |
| buffer.addInt(DATA_VERSION); | |
| _addDeclaredVariablesToSignature(buffer, declaredVariables); | |
| return buffer.toUint32List(); | |
| } | |
| static Uint32List _calculateSaltForResolution({ | |
| required bool enableIndex, | |
| required DriverBasedAnalysisContext? analysisContext, | |
| required DeclaredVariables declaredVariables, | |
| }) { | |
| var buffer = ApiSignature() | |
| ..addInt(DATA_VERSION) | |
| ..addBool(enableIndex); | |
| _addDeclaredVariablesToSignature(buffer, declaredVariables); | |
| var workspace = analysisContext?.contextRoot.workspace; | |
| workspace?.contributeToResolutionSalt(buffer); | |
| return buffer.toUint32List(); | |
| } | |
| static Uint32List _calculateSaltForUnlinked({required bool enableIndex}) { | |
| var buffer = ApiSignature() | |
| ..addInt(DATA_VERSION) | |
| ..addBool(enableIndex); | |
| return buffer.toUint32List(); | |
| } | |
| } | |
| /// Priorities of [AnalysisDriver] work. The farther a priority to the beginning | |
| /// of the list, the earlier the corresponding [AnalysisDriver] should be asked | |
| /// to perform work. | |
| enum AnalysisDriverPriority { | |
| nothing, | |
| general, | |
| generalWithErrors, | |
| generalImportChanged, | |
| generalChanged, | |
| changedFiles, | |
| priority, | |
| interactive, | |
| completion, | |
| } | |
| /// Instances of this class schedule work in multiple [AnalysisDriver]s so that | |
| /// work with the highest priority is performed first. | |
| class AnalysisDriverScheduler { | |
| /// The number of microseconds of work to do per call to | |
| /// `await Future.delayed(Duration.zero)` that will allow other async work to | |
| /// complete. | |
| static const int _microsecondsWorkPerWait = 1000000 ~/ 500; | |
| /// The maximum number of calls to `await Future.delayed(Duration.zero)` to | |
| /// do in a single batch. | |
| static const int _maxWaits = 128; | |
| final PerformanceLog _logger; | |
| /// The object used to watch as analysis drivers are created and deleted. | |
| final DriverWatcher? driverWatcher; | |
| /// When Analysis Server is idle, we want to reduce memory usage, so we | |
| /// discard a few expensive data structures once [AnalysisDriver] has no | |
| /// more work to do. | |
| /// | |
| /// This does not work well for `build_runner`, which makes requests one | |
| /// by one, without big chunks of work in form of added files. | |
| final bool shouldDiscardMemoryExpensiveDataOnIdle; | |
| /// The controller for [events] stream. | |
| final StreamController<Object> eventsController = StreamController<Object>(); | |
| /// The cached instance of [events] stream. | |
| late final Stream<Object> _events = eventsController.stream; | |
| /// The broadcast version of [events] stream. | |
| @visibleForTesting | |
| late final Stream<Object> eventsBroadcast = _events.asBroadcastStream(); | |
| final List<AnalysisDriver> _drivers = []; | |
| final Monitor _hasWork = Monitor(); | |
| late final StatusSupport _statusSupport = StatusSupport( | |
| eventsController: eventsController, | |
| ); | |
| bool _started = false; | |
| /// The operations performance accumulated so far. | |
| /// | |
| /// It is expected that the consumer of this performance operation will | |
| /// do analysis operations, take the instance to print and otherwise | |
| /// process, and reset this field with a new instance. | |
| OperationPerformanceImpl accumulatedPerformance = OperationPerformanceImpl( | |
| '<scheduler>', | |
| ); | |
| /// The last known file counts statistics. | |
| FileCountsStatistics? _fileCountsStatistics; | |
| /// File updates since last transition to working status. | |
| FileUpdatesStatistics _fileUpdatesStatistics = FileUpdatesStatistics(); | |
| AnalysisDriverScheduler( | |
| this._logger, { | |
| this.driverWatcher, | |
| this.shouldDiscardMemoryExpensiveDataOnIdle = true, | |
| }); | |
| /// The [Stream] that produces analysis results for all drivers, and status | |
| /// events. | |
| /// | |
| /// Note that the stream supports only one single subscriber. | |
| /// | |
| /// Analysis starts when the [AnalysisDriverScheduler] is started and the | |
| /// driver is added to it. The analysis state transitions to "working" and | |
| /// an analysis result is produced for every added file prior to the next time | |
| /// the analysis state transitions to "idle". | |
| /// | |
| /// [AnalysisStatusWorking] is produced every time when the current status | |
| /// is [AnalysisStatusIdle], and there is any work to do in any | |
| /// [AnalysisDriver]. This includes analysis of files passed to | |
| /// [AnalysisDriver.addFile], any asynchronous `getXyz()` requests, and | |
| /// [AnalysisDriver.changeFile]. | |
| /// | |
| /// [AnalysisStatusIdle] is produced every time when there is no more work | |
| /// to do after [AnalysisStatusWorking]. | |
| /// | |
| /// [ErrorsResult]s are produced for files passed to [AnalysisDriver.addFile] | |
| /// which are not in [AnalysisDriver.priorityFiles]. We can avoid analyzing | |
| /// a file, if there is already result for it in the [ByteStore]. | |
| /// | |
| /// [ResolvedUnitResult]s are produced for every analyzed file. Currently | |
| /// to analyze a file of a library, the whole library is analyzed, all its | |
| /// files - the defining unit, augmentations, and parts. | |
| /// | |
| /// A file requires analysis if: | |
| /// 1. It was requested by [AnalysisDriver.getResolvedUnit] or | |
| /// [AnalysisDriver.getResolvedLibrary], and not cached. | |
| /// 2. It was [AnalysisDriver.addFile], and either there is no result for it | |
| /// in the [ByteStore], or it is in [AnalysisDriver.priorityFiles]. | |
| Stream<Object> get events => _events; | |
| bool get isStarted => _started; | |
| /// Returns `true` if we are currently working on requests. | |
| bool get isWorking { | |
| return _statusSupport.currentStatus.isWorking; | |
| } | |
| /// Return `true` if there is a driver with a file to analyze. | |
| bool get _hasFilesToAnalyze { | |
| for (var driver in _drivers) { | |
| if (driver._hasFilesToAnalyze) { | |
| return true; | |
| } | |
| } | |
| return false; | |
| } | |
| AnalysisStatusWorkingStatistics? get _workingStatistics { | |
| return _statusSupport.currentStatus | |
| .tryCast<AnalysisStatusWorking>() | |
| ?.workingStatistics; | |
| } | |
| /// Add the given [driver] and schedule it to perform its work. | |
| void add(AnalysisDriver driver) { | |
| _drivers.add(driver); | |
| _hasWork.notify(); | |
| if (driver.analysisContext != null) { | |
| driverWatcher?.addedDriver(driver); | |
| } | |
| } | |
| /// Notifies the scheduler that there might be work to do. | |
| void notify() { | |
| _hasWork.notify(); | |
| _transitionToWorking(); | |
| } | |
| /// Remove the given [driver] from the scheduler, so that it will not be | |
| /// asked to perform any new work. | |
| void remove(AnalysisDriver driver) { | |
| driverWatcher?.removedDriver(driver); | |
| _drivers.remove(driver); | |
| _hasWork.notify(); | |
| } | |
| /// Start the scheduler, so that any [AnalysisDriver] created before or | |
| /// after will be asked to perform work. | |
| void start() { | |
| if (_started) { | |
| throw StateError('The scheduler has already been started.'); | |
| } | |
| _started = true; | |
| _run(); | |
| } | |
| /// Return a future that will be completed the next time the status is idle. | |
| /// | |
| /// If the status is currently idle, the returned future will be signaled | |
| /// immediately. | |
| Future<void> waitForIdle() => _statusSupport.waitForIdle(); | |
| /// Run infinitely analysis cycle, selecting the drivers with the highest | |
| /// priority first. | |
| void _run() async { | |
| // Give other microtasks the time to run before doing the analysis cycle. | |
| await null; | |
| // The amount of time spent working without corresponding calls to | |
| // `_pumpEventQueue`. | |
| var workDuration = Duration.zero; | |
| PerformanceLogSection? analysisSection; | |
| while (true) { | |
| // Wait for other async work if needed to satisfy `_microscondsWorkPerWait`. | |
| if (workDuration.inMicroseconds > _microsecondsWorkPerWait) { | |
| var waits = workDuration.inMicroseconds ~/ _microsecondsWorkPerWait; | |
| await _pumpEventQueue(min(waits, _maxWaits)); | |
| workDuration -= Duration( | |
| microseconds: waits * _microsecondsWorkPerWait, | |
| ); | |
| } | |
| await _hasWork.signal; | |
| for (var driver in _drivers.toList()) { | |
| await driver._maybeDispose(); | |
| } | |
| for (var driver in _drivers) { | |
| driver._applyPendingFileChanges(); | |
| } | |
| // Transition to working if there are files to analyze. | |
| if (_hasFilesToAnalyze) { | |
| _transitionToWorking(); | |
| analysisSection ??= _logger.enter('Working'); | |
| } | |
| // Find the driver with the highest priority. | |
| late AnalysisDriver bestDriver; | |
| AnalysisDriverPriority bestPriority = AnalysisDriverPriority.nothing; | |
| for (var driver in _drivers) { | |
| AnalysisDriverPriority priority = driver.workPriority; | |
| if (priority.index > bestPriority.index) { | |
| bestDriver = driver; | |
| bestPriority = priority; | |
| } | |
| if (priority == AnalysisDriverPriority.nothing) { | |
| if (driver.withFineDependencies && | |
| shouldDiscardMemoryExpensiveDataOnIdle) { | |
| driver.discardMemoryExpensiveData(); | |
| } | |
| } else { | |
| driver._hasDiscardedMemoryExpensiveData = false; | |
| } | |
| } | |
| // Transition to idle if no files to analyze. | |
| if (!_hasFilesToAnalyze) { | |
| _statusSupport.transitionToIdle(); | |
| analysisSection?.exit(); | |
| analysisSection = null; | |
| } | |
| // Continue to sleep if no work to do. | |
| if (bestPriority == AnalysisDriverPriority.nothing) { | |
| continue; | |
| } | |
| var workTimer = Stopwatch()..start(); | |
| // Ask the driver to perform a chunk of work. | |
| await bestDriver.performWork(); | |
| bestDriver.afterPerformWork(); | |
| workDuration += workTimer.elapsed; | |
| // Schedule one more cycle. | |
| _hasWork.notify(); | |
| } | |
| } | |
| void _transitionToWorking() { | |
| _statusSupport.transitionToWorking( | |
| buildStatistics: () { | |
| var withFineDependencies = _drivers.any( | |
| (driver) => driver.withFineDependencies, | |
| ); | |
| // Recompute file statistics once per: 60 * 1000 ms. | |
| var fileCountsStatistics = _fileCountsStatistics; | |
| if (fileCountsStatistics == null || | |
| fileCountsStatistics.age.elapsedMilliseconds > 60 * 1000) { | |
| fileCountsStatistics = _fileCountsStatistics = FileCountsStatistics(); | |
| for (var driver in _drivers) { | |
| for (var fileState in driver.knownFiles) { | |
| if (driver.addedFiles.contains(fileState.path)) { | |
| fileCountsStatistics.immediateFileCount++; | |
| fileCountsStatistics.immediateFileLineCount += | |
| fileState.lineInfo.lineCount; | |
| } | |
| fileCountsStatistics.transitiveFileCount++; | |
| fileCountsStatistics.transitiveFileLineCount += | |
| fileState.lineInfo.lineCount; | |
| } | |
| } | |
| } | |
| var fileUpdatesStatistics = _fileUpdatesStatistics; | |
| _fileUpdatesStatistics = FileUpdatesStatistics(); | |
| return AnalysisStatusWorkingStatistics( | |
| withFineDependencies: withFineDependencies, | |
| changedFiles: fileUpdatesStatistics.changedFiles, | |
| removedFiles: fileUpdatesStatistics.removedFiles, | |
| fileCounts: fileCountsStatistics, | |
| ); | |
| }, | |
| ); | |
| } | |
| /// Returns a [Future] that completes after performing [times] pumpings of | |
| /// the event queue. | |
| static Future _pumpEventQueue(int times) { | |
| if (times == 0) { | |
| return Future.value(); | |
| } | |
| return Future.delayed(Duration.zero, () => _pumpEventQueue(times - 1)); | |
| } | |
| } | |
| class AnalysisDriverTestView { | |
| final fileSystem = FileSystemTestData(); | |
| late final libraryContext = LibraryContextTestData( | |
| fileSystemTestData: fileSystem, | |
| ); | |
| late final AnalysisDriver driver; | |
| int numOfAnalyzedLibraries = 0; | |
| FileTracker get fileTracker => driver._fileTracker; | |
| Set<String> get loadedLibraryUriSet { | |
| var elementFactory = driver.libraryContext.elementFactory; | |
| var libraryReferences = elementFactory.rootReference.children; | |
| return libraryReferences.map((e) => e.uriString).toSet(); | |
| } | |
| int get numberOfFilesToAnalyze => driver.numberOfFilesToAnalyze; | |
| List<FileChange> get pendingFileChanges { | |
| return driver._pendingFileChanges.toList(); | |
| } | |
| Set<String> get priorityFiles => driver._priorityFiles; | |
| Map<String, ResolvedUnitResult> get priorityResults { | |
| return driver._priorityResults; | |
| } | |
| AnalysisDriverPriority get workPriority => driver.workPriority; | |
| } | |
| /// An object that watches for the creation and removal of analysis drivers. | |
| /// | |
| /// Clients may not extend, implement or mix-in this class. | |
| abstract class DriverWatcher { | |
| /// The context manager has just added the given analysis [driver]. This method | |
| /// must be called before the driver has been allowed to perform any analysis. | |
| void addedDriver(AnalysisDriver driver); | |
| /// The context manager has just removed the given analysis [driver]. | |
| void removedDriver(AnalysisDriver driver); | |
| } | |
| class ErrorEncoding { | |
| static Diagnostic? decode(Source source, AnalysisDriverUnitError error) { | |
| String errorName = error.uniqueName; | |
| DiagnosticCode? diagnosticCode = | |
| errorCodeByUniqueName(errorName) ?? _lintCodeByUniqueName(errorName); | |
| if (diagnosticCode == null) { | |
| // This could fail because the error code is no longer defined, or, in | |
| // the case of a lint rule, if the lint rule has been disabled since the | |
| // errors were written. | |
| AnalysisEngine.instance.instrumentationService.logError( | |
| 'No error code for "$error" in "$source"', | |
| ); | |
| return null; | |
| } | |
| var contextMessages = <DiagnosticMessageImpl>[]; | |
| for (var message in error.contextMessages) { | |
| var url = message.url; | |
| contextMessages.add( | |
| DiagnosticMessageImpl( | |
| filePath: message.filePath, | |
| length: message.length, | |
| message: message.message, | |
| offset: message.offset, | |
| url: url.isEmpty ? null : url, | |
| ), | |
| ); | |
| } | |
| return Diagnostic.forValues( | |
| source: source, | |
| offset: error.offset, | |
| length: error.length, | |
| diagnosticCode: diagnosticCode, | |
| message: error.message, | |
| correctionMessage: error.correction.isEmpty ? null : error.correction, | |
| contextMessages: contextMessages, | |
| ); | |
| } | |
| static AnalysisDriverUnitErrorBuilder encode(Diagnostic diagnostic) { | |
| var contextMessages = <DiagnosticMessageBuilder>[]; | |
| for (var message in diagnostic.contextMessages) { | |
| contextMessages.add( | |
| DiagnosticMessageBuilder( | |
| filePath: message.filePath, | |
| length: message.length, | |
| message: message.messageText(includeUrl: false), | |
| offset: message.offset, | |
| url: message.url, | |
| ), | |
| ); | |
| } | |
| return AnalysisDriverUnitErrorBuilder( | |
| offset: diagnostic.offset, | |
| length: diagnostic.length, | |
| uniqueName: diagnostic.diagnosticCode.lowerCaseUniqueName, | |
| message: diagnostic.message, | |
| correction: diagnostic.correctionMessage ?? '', | |
| contextMessages: contextMessages, | |
| ); | |
| } | |
| /// Returns the lint code with the given [diagnosticName], or `null` if there | |
| /// is no lint registered with that name. | |
| static DiagnosticCode? _lintCodeByUniqueName(String diagnosticName) { | |
| return linter.Registry.ruleRegistry.codeForUniqueName(diagnosticName); | |
| } | |
| } | |
| /// Exception that happened during analysis. | |
| class ExceptionResult { | |
| /// The path of the library being analyzed when the [exception] happened. | |
| /// | |
| /// Absolute and normalized. | |
| final String filePath; | |
| /// The content of the library and its parts. | |
| final Map<String, String> fileContentMap; | |
| /// The exception during analysis of the file with the [filePath]. | |
| final CaughtException exception; | |
| /// If the exception happened during a file analysis, and the context in which | |
| /// the exception happened was stored, this field is the key of the context | |
| /// in the byte store. May be `null` if the context is unknown, the maximum | |
| /// number of context to store was reached, etc. | |
| final String? contextKey; | |
| ExceptionResult({ | |
| required this.filePath, | |
| required this.fileContentMap, | |
| required this.exception, | |
| required this.contextKey, | |
| }); | |
| } | |
| @visibleForTesting | |
| class FileChange { | |
| final String path; | |
| final FileChangeKind kind; | |
| FileChange(this.path, this.kind); | |
| @override | |
| String toString() { | |
| return '[path: $path][kind: $kind]'; | |
| } | |
| } | |
| @visibleForTesting | |
| enum FileChangeKind { add, change, remove } | |
| /// Container that keeps track of file owners. | |
| class OwnedFiles { | |
| /// Key: the file from the collection's resource provider. | |
| /// Value: the driver to which the file is added. | |
| final Map<File, AnalysisDriver> addedFiles = {}; | |
| /// Key: the file from the collection's resource provider. | |
| /// Value: a driver in which this file is available via dependencies. | |
| /// This map does not contain any files that are in [addedFiles]. | |
| final Map<File, AnalysisDriver> knownFiles = {}; | |
| /// Key: the folder from the collection's resource provider. | |
| /// Value: the driver discovering this package folder. | |
| final Map<Folder, AnalysisDriver> knownFolders = {}; | |
| void addAdded(File file, AnalysisDriver analysisDriver) { | |
| addedFiles[file] ??= analysisDriver; | |
| knownFiles.remove(file); | |
| } | |
| void addKnown(File file, AnalysisDriver analysisDriver) { | |
| if (!addedFiles.containsKey(file)) { | |
| knownFiles[file] ??= analysisDriver; | |
| } | |
| } | |
| /// Return the files owned by [analysisDriver]. | |
| /// | |
| /// The maps are intentionally append-only hints; each yielded result is | |
| /// resolved through the current owner driver so stale entries are ignored. | |
| List<FileState> filesFor(AnalysisDriver analysisDriver) { | |
| return [ | |
| for (var map in [addedFiles, knownFiles]) | |
| for (var entry in map.entries) | |
| if (identical(entry.value, analysisDriver)) | |
| ?analysisDriver.fsState.getExisting(entry.key), | |
| ]; | |
| } | |
| AnalysisDriver? ownerOf(File file) { | |
| return addedFiles[file] ?? knownFiles[file]; | |
| } | |
| } | |
| /// Worker in [AnalysisDriverScheduler]. | |
| abstract class SchedulerWorker { | |
| /// Return the priority of work that this worker needs to perform. | |
| AnalysisDriverPriority get workPriority; | |
| /// Perform a single chunk of work. | |
| Future<void> performWork(); | |
| } | |
| /// Incrementally adds files selected by [filter] to the file-system state. | |
| /// | |
| /// Directory listing and file-state creation use synchronous APIs. Keeping | |
| /// each invocation bounded does not make an individual I/O operation | |
| /// interruptible, but it prevents a large package graph from monopolizing the | |
| /// isolate across thousands of such operations. | |
| class _DiscoverAvailableFilesJob { | |
| static const _workInterval = Duration(milliseconds: 5); | |
| final AnalysisDriver driver; | |
| final FileStateFilter? filter; | |
| final Completer<void> completer = Completer<void>(); | |
| final Queue<File> _files = Queue<File>(); | |
| final Queue<({Folder folder, bool excludeSrc})> _folders = Queue(); | |
| final Set<File> _seenFiles = <File>{}; | |
| final Set<({Folder folder, bool excludeSrc})> _seenFolders = {}; | |
| bool _isInitialized = false; | |
| _DiscoverAvailableFilesJob(this.driver, this.filter); | |
| bool get isCompleted => completer.isCompleted; | |
| void complete() { | |
| if (!isCompleted) { | |
| _clearPending(); | |
| completer.complete(); | |
| } | |
| } | |
| void perform() { | |
| if (isCompleted) { | |
| return; | |
| } | |
| _initialize(); | |
| var timer = Stopwatch()..start(); | |
| while (true) { | |
| if (_files.isNotEmpty) { | |
| driver._fsState.getFileForPath(_files.removeFirst().path); | |
| } else if (_folders.isNotEmpty) { | |
| _discoverChildren(_folders.removeFirst()); | |
| } else { | |
| complete(); | |
| return; | |
| } | |
| if (timer.elapsed >= _workInterval) { | |
| return; | |
| } | |
| } | |
| } | |
| /// Restarts an in-progress job after the file-system state was cleared. | |
| /// The existing completer deliberately remains pending so callers never | |
| /// observe a "complete" discovery whose results have just been discarded. | |
| void reset() { | |
| if (isCompleted) { | |
| throw StateError('Cannot reset completed discovery.'); | |
| } | |
| _clearPending(); | |
| _isInitialized = false; | |
| } | |
| void _addFile(File file) { | |
| if (filter == null) { | |
| if (driver.ownedFiles case var ownedFiles?) { | |
| var owner = ownedFiles.ownerOf(file); | |
| if (owner != null && !identical(owner, driver)) { | |
| return; | |
| } | |
| } | |
| } | |
| if (_seenFiles.add(file)) { | |
| _files.addLast(file); | |
| } | |
| } | |
| void _addFolder(Folder folder, {required bool excludeSrc}) { | |
| var request = (folder: folder, excludeSrc: excludeSrc); | |
| if (_seenFolders.add(request)) { | |
| _folders.addLast(request); | |
| } | |
| } | |
| void _clearPending() { | |
| _folders.clear(); | |
| _files.clear(); | |
| _seenFolders.clear(); | |
| _seenFiles.clear(); | |
| } | |
| void _discoverChildren(({Folder folder, bool excludeSrc}) request) { | |
| try { | |
| var pathContext = driver.resourceProvider.pathContext; | |
| for (var child in request.folder.getChildren()) { | |
| if (child is File) { | |
| if (file_paths.isDart(pathContext, child.path)) { | |
| _addFile(child); | |
| } | |
| } else if (child is Folder) { | |
| if (!request.excludeSrc || child.shortName != 'src') { | |
| _addFolder(child, excludeSrc: false); | |
| } | |
| } | |
| } | |
| } catch (_) {} | |
| } | |
| void _initialize() { | |
| if (_isInitialized) { | |
| return; | |
| } | |
| _isInitialized = true; | |
| for (var path in driver.addedFiles) { | |
| var file = driver.resourceProvider.getFile(path); | |
| _addFile(file); | |
| } | |
| if (driver._sourceFactory.dartSdk case var dartSdk?) { | |
| for (var sdkLibrary in dartSdk.sdkLibraries) { | |
| var source = dartSdk.mapDartUri(sdkLibrary.shortName); | |
| if (source is FileSource) { | |
| _addFile(source.file); | |
| } | |
| } | |
| } | |
| if (driver._sourceFactory.packageMap case var packageMap?) { | |
| for (var MapEntry(key: packageName, value: folder) | |
| in packageMap.entries) { | |
| if (filter case var filter?) { | |
| if (filter.shouldIncludePackage(packageName)) { | |
| _addFolder( | |
| folder, | |
| excludeSrc: !filter.shouldIncludePackageSrc(packageName), | |
| ); | |
| } | |
| } else { | |
| var owner = driver.ownedFiles?.knownFolders[folder]; | |
| if (owner != null && !identical(owner, driver)) { | |
| continue; | |
| } | |
| driver.ownedFiles?.knownFolders[folder] ??= driver; | |
| _addFolder(folder, excludeSrc: false); | |
| } | |
| } | |
| } | |
| } | |
| } | |
| /// Schedules incremental discovery of files available to this driver. | |
| /// | |
| /// Requests scoped to a target file take precedence over complete discovery. | |
| /// This lets interactive operations avoid waiting for transitive dependencies, | |
| /// while searches can still request every available file. | |
| class _DiscoverAvailableFilesTask { | |
| final AnalysisDriver driver; | |
| final Queue<_DiscoverAvailableFilesJob> _pendingScopedJobs = Queue(); | |
| final Map<FileStateFilter, _DiscoverAvailableFilesJob> _scopedJobs = {}; | |
| _DiscoverAvailableFilesJob? _allFilesJob; | |
| bool _isStopped = false; | |
| _DiscoverAvailableFilesTask(this.driver); | |
| bool get hasPendingWork { | |
| return _pendingScopedJobs.any((job) => !job.isCompleted) || | |
| (_allFilesJob?.isCompleted == false); | |
| } | |
| void complete() { | |
| if (_isStopped) { | |
| return; | |
| } | |
| _isStopped = true; | |
| _allFilesJob?.complete(); | |
| for (var job in _scopedJobs.values) { | |
| job.complete(); | |
| } | |
| _pendingScopedJobs.clear(); | |
| } | |
| Future<void> discoverAvailableFiles() { | |
| if (_isStopped) { | |
| return Future.value(); | |
| } | |
| var job = _allFilesJob ??= _DiscoverAvailableFilesJob(driver, null); | |
| return job.completer.future; | |
| } | |
| Future<void> discoverAvailableFilesFor(FileState targetFile) { | |
| if (_isStopped) { | |
| return Future.value(); | |
| } | |
| if (_allFilesJob case var allFilesJob?) { | |
| if (allFilesJob.isCompleted) { | |
| return allFilesJob.completer.future; | |
| } | |
| } | |
| var filter = FileStateFilter(targetFile); | |
| if (filter.includesAllPackages) { | |
| return discoverAvailableFiles(); | |
| } | |
| var job = _scopedJobs[filter]; | |
| if (job == null) { | |
| job = _DiscoverAvailableFilesJob(driver, filter); | |
| _scopedJobs[filter] = job; | |
| _pendingScopedJobs.addLast(job); | |
| } | |
| return job.completer.future; | |
| } | |
| void perform() { | |
| while (_pendingScopedJobs.isNotEmpty && | |
| _pendingScopedJobs.first.isCompleted) { | |
| _pendingScopedJobs.removeFirst(); | |
| } | |
| if (_pendingScopedJobs.firstOrNull case var job?) { | |
| job.perform(); | |
| return; | |
| } | |
| _allFilesJob?.perform(); | |
| } | |
| void reset() { | |
| if (_isStopped) { | |
| throw StateError('Cannot reset stopped discovery.'); | |
| } | |
| _scopedJobs.removeWhere((_, job) => job.isCompleted); | |
| _pendingScopedJobs.removeWhere((job) => job.isCompleted); | |
| for (var job in _pendingScopedJobs) { | |
| job.reset(); | |
| } | |
| if (_allFilesJob case var job?) { | |
| if (job.isCompleted) { | |
| _allFilesJob = null; | |
| } else { | |
| job.reset(); | |
| } | |
| } | |
| } | |
| } | |
| class _GetFilesDefiningClassMemberNameRequest { | |
| final String name; | |
| final completer = Completer<List<FileState>>(); | |
| _GetFilesDefiningClassMemberNameRequest(this.name); | |
| } | |
| class _GetFilesReferencingNamesRequest { | |
| final Set<String> names; | |
| final completer = Completer<List<FileState>>(); | |
| _GetFilesReferencingNamesRequest(this.names); | |
| } | |
| class _ResolveForCompletionRequest { | |
| final String path; | |
| final int? offset; | |
| final int? line; | |
| final int? column; | |
| final OperationPerformanceImpl performance; | |
| final Completer<ResolvedForCompletionResultImpl?> completer = Completer(); | |
| _ResolveForCompletionRequest({ | |
| required this.path, | |
| required this.offset, | |
| required this.line, | |
| required this.column, | |
| required this.performance, | |
| }); | |
| } | |
| extension<K, V> on Map<K, List<Completer<V>>> { | |
| void completeAll(K key, V value) { | |
| remove(key)?.completeAll(value); | |
| } | |
| } |