| // Copyright (c) 2014, 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:core'; |
| |
| import 'package:analysis_server/src/services/correction/fix/data_driven/transform_set_parser.dart'; |
| import 'package:analyzer/error/listener.dart'; |
| import 'package:analyzer/file_system/file_system.dart'; |
| import 'package:analyzer/instrumentation/instrumentation.dart'; |
| import 'package:analyzer/src/dart/analysis/analysis_context_collection.dart'; |
| import 'package:analyzer/src/dart/analysis/byte_store.dart'; |
| import 'package:analyzer/src/dart/analysis/driver.dart'; |
| import 'package:analyzer/src/dart/analysis/driver_based_analysis_context.dart'; |
| import 'package:analyzer/src/dart/analysis/performance_logger.dart'; |
| import 'package:analyzer/src/generated/engine.dart'; |
| import 'package:analyzer/src/generated/java_engine.dart'; |
| import 'package:analyzer/src/generated/sdk.dart'; |
| import 'package:analyzer/src/generated/source.dart'; |
| import 'package:analyzer/src/generated/source_io.dart'; |
| import 'package:analyzer/src/lint/linter.dart'; |
| import 'package:analyzer/src/lint/pub.dart'; |
| import 'package:analyzer/src/manifest/manifest_validator.dart'; |
| import 'package:analyzer/src/pubspec/pubspec_validator.dart'; |
| import 'package:analyzer/src/task/options.dart'; |
| import 'package:analyzer/src/util/file_paths.dart' as file_paths; |
| import 'package:analyzer/src/workspace/bazel.dart'; |
| import 'package:analyzer_plugin/protocol/protocol_common.dart' as protocol; |
| import 'package:analyzer_plugin/utilities/analyzer_converter.dart'; |
| import 'package:path/path.dart' as pathos; |
| import 'package:watcher/watcher.dart'; |
| import 'package:yaml/yaml.dart'; |
| |
| /// Enables watching of files generated by Bazel. |
| /// |
| /// TODO(michalt): This is a temporary flag that we use to disable this |
| /// functionality due its performance issues. We plan to benchmark and optimize |
| /// it and re-enable it everywhere. |
| /// Not private to enable testing. |
| var experimentalEnableBazelWatching = false; |
| |
| /// Class that maintains a mapping from included/excluded paths to a set of |
| /// folders that should correspond to analysis contexts. |
| abstract class ContextManager { |
| // TODO(brianwilkerson) Support: |
| // setting the default analysis options |
| // setting the default content cache |
| // setting the default SDK |
| // telling server when a context has been added or removed |
| // (see onContextsChanged) |
| // telling server when a context needs to be re-analyzed |
| // notifying the client when results should be flushed |
| // using analyzeFileFunctions to determine which files to analyze |
| // |
| // TODO(brianwilkerson) Move this class to a public library. |
| |
| /// Get the callback interface used to create, destroy, and update contexts. |
| ContextManagerCallbacks get callbacks; |
| |
| /// Set the callback interface used to create, destroy, and update contexts. |
| set callbacks(ContextManagerCallbacks value); |
| |
| /// A table mapping [Folder]s to the [AnalysisDriver]s associated with them. |
| Map<Folder, AnalysisDriver> get driverMap; |
| |
| /// Return the list of excluded paths (folders and files) most recently passed |
| /// to [setRoots]. |
| List<String> get excludedPaths; |
| |
| /// Return the list of included paths (folders and files) most recently passed |
| /// to [setRoots]. |
| List<String> get includedPaths; |
| |
| /// Return the existing analysis context that should be used to analyze the |
| /// given [path], or `null` if the [path] is not analyzed in any of the |
| /// created analysis contexts. |
| DriverBasedAnalysisContext getContextFor(String path); |
| |
| /// Return the [AnalysisDriver] for the "innermost" context whose associated |
| /// folder is or contains the given path. ("innermost" refers to the nesting |
| /// of contexts, so if there is a context for path /foo and a context for |
| /// path /foo/bar, then the innermost context containing /foo/bar/baz.dart is |
| /// the context for /foo/bar.) |
| /// |
| /// If no driver contains the given path, `null` is returned. |
| AnalysisDriver getDriverFor(String path); |
| |
| /// Determine whether the given [path], when interpreted relative to innermost |
| /// context root, contains a folder whose name starts with '.'. |
| /// |
| /// TODO(scheglov) Remove it, just [isInAnalysisRoot] should be enough. |
| bool isContainedInDotFolder(String path); |
| |
| /// Return `true` if the given absolute [path] is in one of the current |
| /// root folders and is not excluded. |
| bool isInAnalysisRoot(String path); |
| |
| /// Rebuild the set of contexts from scratch based on the data last sent to |
| /// [setRoots]. |
| void refresh(); |
| |
| /// Change the set of paths which should be used as starting points to |
| /// determine the context directories. |
| void setRoots(List<String> includedPaths, List<String> excludedPaths); |
| } |
| |
| /// Callback interface used by [ContextManager] to (a) request that contexts be |
| /// created, destroyed or updated, (b) inform the client when "pub list" |
| /// operations are in progress, and (c) determine which files should be |
| /// analyzed. |
| /// |
| /// TODO(paulberry): eliminate this interface, and instead have [ContextManager] |
| /// operations return data structures describing how context state should be |
| /// modified. |
| abstract class ContextManagerCallbacks { |
| /// Called after analysis contexts are created, usually when new analysis |
| /// roots are set, or after detecting a change that required rebuilding |
| /// the set of analysis contexts. |
| void afterContextsCreated(); |
| |
| /// Called after analysis contexts are destroyed. |
| void afterContextsDestroyed(); |
| |
| /// An [event] was processed, so analysis state might be different now. |
| void afterWatchEvent(WatchEvent event); |
| |
| /// The given [file] was removed. |
| void applyFileRemoved(String file); |
| |
| /// Sent the given watch [event] to any interested plugins. |
| void broadcastWatchEvent(WatchEvent event); |
| |
| /// Add listeners to the [driver]. This must be the only listener. |
| /// |
| /// TODO(scheglov) Just pass results in here? |
| void listenAnalysisDriver(AnalysisDriver driver); |
| |
| /// Record error information for the file with the given [path]. |
| void recordAnalysisErrors(String path, List<protocol.AnalysisError> errors); |
| } |
| |
| /// Class that maintains a mapping from included/excluded paths to a set of |
| /// folders that should correspond to analysis contexts. |
| class ContextManagerImpl implements ContextManager { |
| /// The [ResourceProvider] using which paths are converted into [Resource]s. |
| final ResourceProvider resourceProvider; |
| |
| /// The manager used to access the SDK that should be associated with a |
| /// particular context. |
| final DartSdkManager sdkManager; |
| |
| /// The storage for cached results. |
| final ByteStore _byteStore; |
| |
| /// The logger used to create analysis contexts. |
| final PerformanceLog _performanceLog; |
| |
| /// The scheduler used to create analysis contexts, and report status. |
| final AnalysisDriverScheduler _scheduler; |
| |
| /// The current set of analysis contexts. |
| AnalysisContextCollectionImpl _collection; |
| |
| /// The context used to work with file system paths. |
| pathos.Context pathContext; |
| |
| /// The list of excluded paths (folders and files) most recently passed to |
| /// [setRoots]. |
| @override |
| List<String> excludedPaths = <String>[]; |
| |
| /// The list of included paths (folders and files) most recently passed to |
| /// [setRoots]. |
| @override |
| List<String> includedPaths = <String>[]; |
| |
| /// The instrumentation service used to report instrumentation data. |
| final InstrumentationService _instrumentationService; |
| |
| @override |
| ContextManagerCallbacks callbacks; |
| |
| @override |
| final Map<Folder, AnalysisDriver> driverMap = |
| HashMap<Folder, AnalysisDriver>(); |
| |
| /// Stream subscription we are using to watch each analysis root directory for |
| /// changes. |
| final Map<Folder, StreamSubscription<WatchEvent>> changeSubscriptions = |
| <Folder, StreamSubscription<WatchEvent>>{}; |
| |
| /// For each root directory stores subscriptions and watchers that we |
| /// established to detect changes to Bazel generated files. |
| final Map<Folder, _BazelWorkspaceSubscription> bazelSubscriptions = |
| <Folder, _BazelWorkspaceSubscription>{}; |
| |
| ContextManagerImpl( |
| this.resourceProvider, |
| this.sdkManager, |
| this._byteStore, |
| this._performanceLog, |
| this._scheduler, |
| this._instrumentationService, |
| ) { |
| pathContext = resourceProvider.pathContext; |
| } |
| |
| @override |
| DriverBasedAnalysisContext getContextFor(String path) { |
| try { |
| return _collection?.contextFor(path); |
| } on StateError { |
| return null; |
| } |
| } |
| |
| @override |
| AnalysisDriver getDriverFor(String path) { |
| return getContextFor(path)?.driver; |
| } |
| |
| /// Determine whether the given [path], when interpreted relative to innermost |
| /// context root, contains a folder whose name starts with '.'. |
| @override |
| bool isContainedInDotFolder(String path) { |
| for (var analysisContext in _collection.contexts) { |
| var contextImpl = analysisContext as DriverBasedAnalysisContext; |
| if (_isContainedInDotFolder(contextImpl.contextRoot.root.path, path)) { |
| return true; |
| } |
| } |
| return false; |
| } |
| |
| @override |
| bool isInAnalysisRoot(String path) { |
| var collection = _collection; |
| if (collection == null) { |
| return false; |
| } |
| |
| return collection.contexts.any( |
| (context) => context.contextRoot.isAnalyzed(path), |
| ); |
| } |
| |
| @override |
| void refresh() { |
| _createAnalysisContexts(); |
| } |
| |
| @override |
| void setRoots(List<String> includedPaths, List<String> excludedPaths) { |
| this.includedPaths = includedPaths; |
| this.excludedPaths = excludedPaths; |
| |
| _createAnalysisContexts(); |
| } |
| |
| /// Use the given analysis [driver] to analyze the content of the analysis |
| /// options file at the given [path]. |
| void _analyzeAnalysisOptionsYaml(AnalysisDriver driver, String path) { |
| var convertedErrors = const <protocol.AnalysisError>[]; |
| try { |
| var content = _readFile(path); |
| var lineInfo = _computeLineInfo(content); |
| var errors = analyzeAnalysisOptions( |
| resourceProvider.getFile(path).createSource(), |
| content, |
| driver.sourceFactory); |
| var converter = AnalyzerConverter(); |
| convertedErrors = converter.convertAnalysisErrors(errors, |
| lineInfo: lineInfo, options: driver.analysisOptions); |
| } catch (exception) { |
| // If the file cannot be analyzed, fall through to clear any previous |
| // errors. |
| } |
| callbacks.recordAnalysisErrors(path, convertedErrors); |
| } |
| |
| /// Use the given analysis [driver] to analyze the content of the |
| /// AndroidManifest file at the given [path]. |
| void _analyzeAndroidManifestXml(AnalysisDriver driver, String path) { |
| var convertedErrors = const <protocol.AnalysisError>[]; |
| try { |
| var content = _readFile(path); |
| var validator = |
| ManifestValidator(resourceProvider.getFile(path).createSource()); |
| var lineInfo = _computeLineInfo(content); |
| var errors = validator.validate( |
| content, driver.analysisOptions.chromeOsManifestChecks); |
| var converter = AnalyzerConverter(); |
| convertedErrors = converter.convertAnalysisErrors(errors, |
| lineInfo: lineInfo, options: driver.analysisOptions); |
| } catch (exception) { |
| // If the file cannot be analyzed, fall through to clear any previous |
| // errors. |
| } |
| callbacks.recordAnalysisErrors(path, convertedErrors); |
| } |
| |
| /// Use the given analysis [driver] to analyze the content of the |
| /// data file at the given [path]. |
| void _analyzeFixDataYaml(AnalysisDriver driver, String path) { |
| var convertedErrors = const <protocol.AnalysisError>[]; |
| try { |
| var file = resourceProvider.getFile(path); |
| var packageName = file.parent2.parent2.shortName; |
| var content = _readFile(path); |
| var errorListener = RecordingErrorListener(); |
| var errorReporter = ErrorReporter(errorListener, file.createSource()); |
| var parser = TransformSetParser(errorReporter, packageName); |
| parser.parse(content); |
| var converter = AnalyzerConverter(); |
| convertedErrors = converter.convertAnalysisErrors(errorListener.errors, |
| lineInfo: _computeLineInfo(content), options: driver.analysisOptions); |
| } catch (exception) { |
| // If the file cannot be analyzed, fall through to clear any previous |
| // errors. |
| } |
| callbacks.recordAnalysisErrors(path, convertedErrors); |
| } |
| |
| /// Use the given analysis [driver] to analyze the content of the pubspec file |
| /// at the given [path]. |
| void _analyzePubspecYaml(AnalysisDriver driver, String path) { |
| var convertedErrors = const <protocol.AnalysisError>[]; |
| try { |
| var content = _readFile(path); |
| var node = loadYamlNode(content); |
| if (node is YamlMap) { |
| var validator = PubspecValidator( |
| resourceProvider, resourceProvider.getFile(path).createSource()); |
| var lineInfo = _computeLineInfo(content); |
| var errors = validator.validate(node.nodes); |
| var converter = AnalyzerConverter(); |
| convertedErrors = converter.convertAnalysisErrors(errors, |
| lineInfo: lineInfo, options: driver.analysisOptions); |
| |
| if (driver.analysisOptions.lint) { |
| var visitors = <LintRule, PubspecVisitor>{}; |
| for (var linter in driver.analysisOptions.lintRules) { |
| if (linter is LintRule) { |
| var visitor = linter.getPubspecVisitor(); |
| if (visitor != null) { |
| visitors[linter] = visitor; |
| } |
| } |
| } |
| |
| if (visitors.isNotEmpty) { |
| var sourceUri = resourceProvider.pathContext.toUri(path); |
| var pubspecAst = Pubspec.parse(content, |
| sourceUrl: sourceUri, resourceProvider: resourceProvider); |
| var listener = RecordingErrorListener(); |
| var reporter = ErrorReporter(listener, |
| resourceProvider.getFile(path).createSource(sourceUri), |
| isNonNullableByDefault: false); |
| for (var entry in visitors.entries) { |
| entry.key.reporter = reporter; |
| pubspecAst.accept(entry.value); |
| } |
| if (listener.errors.isNotEmpty) { |
| convertedErrors ??= <protocol.AnalysisError>[]; |
| convertedErrors.addAll(converter.convertAnalysisErrors( |
| listener.errors, |
| lineInfo: lineInfo, |
| options: driver.analysisOptions)); |
| } |
| } |
| } |
| } |
| } catch (exception) { |
| // If the file cannot be analyzed, fall through to clear any previous |
| // errors. |
| } |
| callbacks.recordAnalysisErrors(path, convertedErrors); |
| } |
| |
| void _checkForAndroidManifestXmlUpdate(String path) { |
| if (file_paths.isAndroidManifestXml(pathContext, path)) { |
| var context = getContextFor(path); |
| var driver = context.driver; |
| _analyzeAndroidManifestXml(driver, path); |
| } |
| } |
| |
| void _checkForFixDataYamlUpdate(String path) { |
| if (file_paths.isFixDataYaml(pathContext, path)) { |
| var context = getContextFor(path); |
| var driver = context.driver; |
| _analyzeFixDataYaml(driver, path); |
| } |
| } |
| |
| /// Compute line information for the given [content]. |
| LineInfo _computeLineInfo(String content) { |
| var lineStarts = StringUtilities.computeLineStarts(content); |
| return LineInfo(lineStarts); |
| } |
| |
| void _createAnalysisContexts() { |
| _destroyAnalysisContexts(); |
| |
| _collection = AnalysisContextCollectionImpl( |
| includedPaths: includedPaths, |
| excludedPaths: excludedPaths, |
| byteStore: _byteStore, |
| drainStreams: false, |
| enableIndex: true, |
| performanceLog: _performanceLog, |
| resourceProvider: resourceProvider, |
| scheduler: _scheduler, |
| sdkPath: sdkManager.defaultSdkDirectory, |
| ); |
| |
| for (var context in _collection.contexts) { |
| var contextImpl = context as DriverBasedAnalysisContext; |
| var driver = contextImpl.driver; |
| |
| callbacks.listenAnalysisDriver(driver); |
| |
| var rootFolder = contextImpl.contextRoot.root; |
| driverMap[rootFolder] = driver; |
| |
| changeSubscriptions[rootFolder] = rootFolder.changes |
| .listen(_handleWatchEvent, onError: _handleWatchInterruption); |
| |
| _watchBazelFilesIfNeeded(rootFolder, driver); |
| |
| for (var file in contextImpl.contextRoot.analyzedFiles()) { |
| if (_isContainedInDotFolder(contextImpl.contextRoot.root.path, file)) { |
| continue; |
| } |
| if (file_paths.isAndroidManifestXml(pathContext, file)) { |
| _analyzeAndroidManifestXml(driver, file); |
| } else if (file_paths.isDart(pathContext, file)) { |
| driver.addFile(file); |
| } |
| } |
| |
| var optionsFile = context.contextRoot.optionsFile; |
| if (optionsFile != null) { |
| _analyzeAnalysisOptionsYaml(driver, optionsFile.path); |
| } |
| |
| var fixDataYamlFile = rootFolder |
| .getChildAssumingFolder('lib') |
| .getChildAssumingFile(file_paths.fixDataYaml); |
| if (fixDataYamlFile.exists) { |
| _analyzeFixDataYaml(driver, fixDataYamlFile.path); |
| } |
| |
| var pubspecFile = rootFolder.getChildAssumingFile(file_paths.pubspecYaml); |
| if (pubspecFile.exists) { |
| _analyzePubspecYaml(driver, pubspecFile.path); |
| } |
| } |
| |
| callbacks.afterContextsCreated(); |
| } |
| |
| /// Clean up and destroy the context associated with the given folder. |
| void _destroyAnalysisContext(DriverBasedAnalysisContext context) { |
| context.driver.dispose(); |
| |
| var rootFolder = context.contextRoot.root; |
| changeSubscriptions.remove(rootFolder)?.cancel(); |
| bazelSubscriptions.remove(rootFolder)?.cancel(); |
| driverMap.remove(rootFolder); |
| } |
| |
| void _destroyAnalysisContexts() { |
| if (_collection != null) { |
| for (var analysisContext in _collection.contexts) { |
| var contextImpl = analysisContext as DriverBasedAnalysisContext; |
| _destroyAnalysisContext(contextImpl); |
| } |
| callbacks.afterContextsDestroyed(); |
| } |
| } |
| |
| /// Establishes watch(es) for the Bazel generated files provided in |
| /// [notification]. |
| /// |
| /// Whenever the files change, we trigger re-analysis. This allows us to react |
| /// to creation/modification of files that were generated by Bazel. |
| void _handleBazelFileNotification( |
| Folder folder, BazelFileNotification notification) { |
| var fileSubscriptions = bazelSubscriptions[folder].fileSubscriptions; |
| if (fileSubscriptions.containsKey(notification.requested)) { |
| // We have already established a Watcher for this particular path. |
| return; |
| } |
| var watcher = notification.watcher( |
| pollingDelayShort: Duration(seconds: 10), |
| pollingDelayLong: Duration(seconds: 30)); |
| var subscription = watcher.events.listen(_handleBazelWatchEvent); |
| fileSubscriptions[notification.requested] = |
| _BazelFilesSubscription(watcher, subscription); |
| watcher.start(); |
| } |
| |
| /// Notifies the drivers that a generated Bazel file has changed. |
| void _handleBazelWatchEvent(WatchEvent event) { |
| if (event.type == ChangeType.ADD) { |
| for (var driver in driverMap.values) { |
| driver.addFile(event.path); |
| // Since the file has been created after we've searched for it, the |
| // URI resolution is likely wrong, so we need to reset it. |
| driver.resetUriResolution(); |
| } |
| } else if (event.type == ChangeType.MODIFY) { |
| for (var driver in driverMap.values) { |
| driver.changeFile(event.path); |
| } |
| } else if (event.type == ChangeType.REMOVE) { |
| for (var driver in driverMap.values) { |
| driver.removeFile(event.path); |
| } |
| } |
| } |
| |
| void _handleWatchEvent(WatchEvent event) { |
| callbacks.broadcastWatchEvent(event); |
| _handleWatchEventImpl(event); |
| callbacks.afterWatchEvent(event); |
| } |
| |
| void _handleWatchEventImpl(WatchEvent event) { |
| // Figure out which context this event applies to. |
| // TODO(brianwilkerson) If a file is explicitly included in one context |
| // but implicitly referenced in another context, we will only send a |
| // changeSet to the context that explicitly includes the file (because |
| // that's the only context that's watching the file). |
| var path = event.path; |
| var type = event.type; |
| |
| _instrumentationService.logWatchEvent('<unknown>', path, type.toString()); |
| |
| if (file_paths.isAnalysisOptionsYaml(pathContext, path) || |
| file_paths.isDotPackages(pathContext, path) || |
| file_paths.isPackageConfigJson(pathContext, path) || |
| file_paths.isPubspecYaml(pathContext, path) || |
| false) { |
| _createAnalysisContexts(); |
| return; |
| } |
| |
| if (file_paths.isDart(pathContext, path)) { |
| for (var analysisContext_ in _collection.contexts) { |
| var analysisContext = analysisContext_ as DriverBasedAnalysisContext; |
| switch (type) { |
| case ChangeType.ADD: |
| // TODO(scheglov) Why not `isInAnalysisRoot()`? |
| if (_isContainedInDotFolder( |
| analysisContext.contextRoot.root.path, path)) { |
| return; |
| } |
| analysisContext.driver.addFile(path); |
| break; |
| case ChangeType.MODIFY: |
| analysisContext.driver.changeFile(path); |
| break; |
| case ChangeType.REMOVE: |
| analysisContext.driver.removeFile(path); |
| break; |
| } |
| } |
| } |
| |
| switch (type) { |
| case ChangeType.ADD: |
| case ChangeType.MODIFY: |
| _checkForAndroidManifestXmlUpdate(path); |
| _checkForFixDataYamlUpdate(path); |
| break; |
| case ChangeType.REMOVE: |
| callbacks.applyFileRemoved(path); |
| break; |
| } |
| } |
| |
| /// On windows, the directory watcher may overflow, and we must recover. |
| void _handleWatchInterruption(dynamic error, StackTrace stackTrace) { |
| // We've handled the error, so we only have to log it. |
| _instrumentationService |
| .logError('Watcher error; refreshing contexts.\n$error\n$stackTrace'); |
| // TODO(mfairhurst): Optimize this, or perhaps be less complete. |
| refresh(); |
| } |
| |
| /// Determine whether the given [path], when interpreted relative to the |
| /// context root [root], contains a folder whose name starts with '.' but is |
| /// not included in [exclude]. |
| bool _isContainedInDotFolder(String root, String path, |
| {Set<String> exclude}) { |
| var pathDir = pathContext.dirname(path); |
| var rootPrefix = root + pathContext.separator; |
| if (pathDir.startsWith(rootPrefix)) { |
| var suffixPath = pathDir.substring(rootPrefix.length); |
| for (var pathComponent in pathContext.split(suffixPath)) { |
| if (pathComponent.startsWith('.') && |
| !(exclude?.contains(pathComponent) ?? false)) { |
| return true; |
| } |
| } |
| } |
| return false; |
| } |
| |
| /// Read the contents of the file at the given [path], or throw an exception |
| /// if the contents cannot be read. |
| String _readFile(String path) { |
| return resourceProvider.getFile(path).readAsStringSync(); |
| } |
| |
| /// Listens to files generated by Bazel that were found or searched for. |
| /// |
| /// This is handled specially because the files are outside the package |
| /// folder, but we still want to watch for changes to them. |
| /// |
| /// Does nothing if the [driver] is not in a Bazel workspace. |
| void _watchBazelFilesIfNeeded(Folder folder, AnalysisDriver analysisDriver) { |
| if (!experimentalEnableBazelWatching) return; |
| var workspace = analysisDriver.analysisContext.contextRoot.workspace; |
| if (workspace is BazelWorkspace && |
| !bazelSubscriptions.containsKey(folder)) { |
| var subscription = workspace.bazelCandidateFiles.listen( |
| (notification) => _handleBazelFileNotification(folder, notification)); |
| bazelSubscriptions[folder] = _BazelWorkspaceSubscription(subscription); |
| } |
| } |
| } |
| |
| /// A watcher with subscription used to detect changes to some file. |
| class _BazelFilesSubscription { |
| final BazelFileWatcher watcher; |
| final StreamSubscription<WatchEvent> subscription; |
| |
| _BazelFilesSubscription(this.watcher, this.subscription); |
| |
| void cancel() { |
| subscription.cancel(); |
| watcher.stop(); |
| } |
| } |
| |
| /// A subscription to notifications from a Bazel workspace. |
| class _BazelWorkspaceSubscription { |
| final StreamSubscription<BazelFileNotification> workspaceSubscription; |
| |
| /// For each absolute path that we searched for, provides the subscriptions |
| /// that we established to watch for changes. |
| /// |
| /// Note that the absolute path used when searching for a file is not |
| /// necessarily the actual path of the file (see [BazelWorkspace.findFile] for |
| /// details on how the files are searched). |
| final fileSubscriptions = <String, _BazelFilesSubscription>{}; |
| |
| _BazelWorkspaceSubscription(this.workspaceSubscription); |
| |
| void cancel() { |
| workspaceSubscription.cancel(); |
| fileSubscriptions.values.forEach((sub) => sub.cancel()); |
| } |
| } |