// Copyright (c) 2024, 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. /// @docImport 'package:analyzer/src/dart/analysis/driver.dart'; library; import 'dart:async'; import 'package:analysis_server_plugin/edit/assist/assist.dart'; import 'package:analysis_server_plugin/edit/assist/dart_assist_context.dart'; import 'package:analysis_server_plugin/edit/fix/dart_fix_context.dart'; import 'package:analysis_server_plugin/edit/fix/fix.dart'; import 'package:analysis_server_plugin/plugin.dart'; import 'package:analysis_server_plugin/src/correction/assist_processor.dart'; import 'package:analysis_server_plugin/src/correction/dart_change_workspace.dart'; import 'package:analysis_server_plugin/src/correction/fix_processor.dart'; import 'package:analysis_server_plugin/src/registry.dart'; import 'package:analysis_server_plugin/src/utilities/diagnostic_messages.dart'; import 'package:analyzer/analysis_rule/rule_context.dart'; import 'package:analyzer/dart/analysis/analysis_context.dart'; import 'package:analyzer/dart/analysis/analysis_context_collection.dart'; import 'package:analyzer/dart/analysis/results.dart'; import 'package:analyzer/dart/analysis/session.dart'; import 'package:analyzer/dart/ast/ast.dart'; import 'package:analyzer/diagnostic/diagnostic.dart'; import 'package:analyzer/error/error.dart'; import 'package:analyzer/error/listener.dart'; import 'package:analyzer/file_system/file_system.dart'; import 'package:analyzer/file_system/overlay_file_system.dart'; import 'package:analyzer/instrumentation/instrumentation.dart'; import 'package:analyzer/src/analysis_rule/rule_context.dart'; import 'package:analyzer/src/dart/analysis/analysis_context_collection.dart'; import 'package:analyzer/src/dart/analysis/analysis_options.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/file_content_cache.dart'; import 'package:analyzer/src/dart/analysis/status.dart'; import 'package:analyzer/src/dart/element/element.dart'; import 'package:analyzer/src/dart/element/type_system.dart'; import 'package:analyzer/src/ignore_comments/ignore_info.dart'; import 'package:analyzer/src/lint/config.dart'; import 'package:analyzer/src/lint/linter_visitor.dart'; import 'package:analyzer/src/lint/registry.dart'; import 'package:analyzer/src/util/file_paths.dart' as file_paths; import 'package:analyzer_plugin/channel/channel.dart'; import 'package:analyzer_plugin/protocol/protocol.dart'; import 'package:analyzer_plugin/protocol/protocol_common.dart' as protocol; import 'package:analyzer_plugin/protocol/protocol_constants.dart' as protocol; import 'package:analyzer_plugin/protocol/protocol_generated.dart' as protocol; import 'package:analyzer_plugin/src/protocol/protocol_internal.dart'; import 'package:meta/meta.dart'; /// A pair, matching a [Diagnostic] with it's equivalent /// [protocol.AnalysisError]. typedef _DiagnosticAndAnalysisError = ({ Diagnostic diagnostic, protocol.AnalysisError analysisError, }); typedef _PluginState = ({ AnalysisContext analysisContext, List<_DiagnosticAndAnalysisError> errors, }); /// The server that communicates with the analysis server, passing requests and /// responses between the analysis server and individual plugins. class PluginServer { /// The communication channel being used to communicate with the analysis /// server. late PluginCommunicationChannel _channel; final OverlayResourceProvider _resourceProvider; late final ByteStore _byteStore = MemoryCachingByteStore( NullByteStore(), 1024 * 1024 * 256, ); AnalysisContextCollectionImpl? _contextCollection; String? _sdkPath; /// Paths of priority files. Set _priorityPaths = {}; final List _plugins; /// The recent state of analysis reults, to be cleared on file changes. final _recentState = {}; /// The map of files currently being analyzed, mapped to their active session. final _filesBeingAnalyzed = {}; /// The map of files currently being resolved, mapped to their active session. final _filesBeingResolved = {}; /// The next modification stamp for a changed file in the [_resourceProvider]. int _overlayModificationStamp = 0; /// The map of registered features for each plugin, for namespacing purposes. static final registries = {}; /// The current subscription of [_contextCollection]'s /// [AnalysisDriverScheduler.events] Stream. This must be cancelled when /// [_contextCollection] is disposed. StreamSubscription? _eventsSubscription; PluginServer({ required ResourceProvider resourceProvider, required List plugins, }) : _resourceProvider = OverlayResourceProvider(resourceProvider), _plugins = plugins { int i = 0; for (var plugin in plugins) { var registry = PluginRegistryImpl(plugin.name); // For this unnamed (old) constructor, we were not tracking them by name // so we do this (use '$i') simply to store the registries uniquely. // // `registries` was previously only used for the `PLUGIN_REQUEST_DETAILS` // request handling. // // Now, we plan on deprecating this constructor and move to `new2`, since // that allows us to track plugins by name and fix issues with ignores and // avoiding diagnostic collisions. // // When initializing with this constructor we can't use `registries` to // find them by name, so we use the main `Registry.ruleRegistry` to find // the rules instead. registries['$i'] = registry; plugin.register(registry); i++; } PluginRegistryImpl.registerIgnoreProducerGenerators(); } PluginServer.new2({ required ResourceProvider resourceProvider, required Map plugins, }) : _resourceProvider = OverlayResourceProvider(resourceProvider), _plugins = plugins.values.toList() { for (var MapEntry(key: name, value: plugin) in plugins.entries) { var registry = PluginRegistryImpl(plugin.name); registries[name.toLowerCase()] = registry; plugin.register(registry); } PluginRegistryImpl.registerIgnoreProducerGenerators(); } @visibleForTesting Set get priorityPaths => {..._priorityPaths}; /// Handles an 'analysis.setPriorityFiles' request. /// /// Throws a [RequestFailure] if the request could not be handled. Future handleAnalysisSetPriorityFiles( protocol.AnalysisSetPriorityFilesParams parameters, ) async { _priorityPaths = parameters.files.toSet(); _updatePriorityFiles(); return protocol.AnalysisSetPriorityFilesResult(); } /// Handles an 'edit.getAssists' request. /// /// Throws a [RequestFailure] if the request could not be handled. Future handleEditGetAssists( protocol.EditGetAssistsParams parameters, ) async { var path = parameters.file; var recentState = _recentState[path]; if (recentState == null) { return protocol.EditGetAssistsResult(const []); } var (:analysisContext, :errors) = recentState; var libraryResult = await analysisContext.currentSession .getResolvedLibraryContaining(path); if (libraryResult is! ResolvedLibraryResult) { return protocol.EditGetAssistsResult(const []); } var unitResult = libraryResult.unitWithPath(path); if (unitResult is! ResolvedUnitResult) { return protocol.EditGetAssistsResult(const []); } var context = DartAssistContext( // TODO(srawlins): Use a real instrumentation service. Other // implementations get InstrumentationService from AnalysisServer. InstrumentationService.NULL_SERVICE, DartChangeWorkspace([analysisContext.currentSession]), libraryResult, unitResult, parameters.offset, parameters.length, ); List assists; try { assists = await computeAssists(context); } on InconsistentAnalysisException { // TODO(srawlins): Is it important to at least log this? Or does it // happen on the regular? assists = []; } if (assists.isEmpty) { return protocol.EditGetAssistsResult(const []); } var corrections = [ for (var assist in assists..sort(Assist.compareAssists)) protocol.PrioritizedSourceChange(assist.kind.priority, assist.change), ]; return protocol.EditGetAssistsResult(corrections); } /// Handles an 'edit.getFixes' request. /// /// Throws a [RequestFailure] if the request could not be handled. Future handleEditGetFixes( protocol.EditGetFixesParams parameters, ) async { var path = parameters.file; var offset = parameters.offset; var recentState = _recentState[path]; if (recentState == null) { return protocol.EditGetFixesResult(const []); } var (:analysisContext, :errors) = recentState; var libraryResult = await analysisContext.currentSession .getResolvedLibraryContaining(path); if (libraryResult is! ResolvedLibraryResult) { return protocol.EditGetFixesResult(const []); } var unitResult = libraryResult.unitWithPath(path); if (unitResult is! ResolvedUnitResult) { return protocol.EditGetFixesResult(const []); } var lineInfo = unitResult.lineInfo; var requestLine = lineInfo.getLocation(offset).lineNumber; var lintAtOffset = errors.where((error) { var errorLine = lineInfo.getLocation(error.diagnostic.offset).lineNumber; return errorLine == requestLine; }); if (lintAtOffset.isEmpty) return protocol.EditGetFixesResult(const []); var errorFixesList = []; var workspace = DartChangeWorkspace([analysisContext.currentSession]); for (var (:diagnostic, :analysisError) in lintAtOffset) { var context = DartFixContext( // TODO(srawlins): Use a real instrumentation service. Other // implementations get InstrumentationService from AnalysisServer. instrumentationService: InstrumentationService.NULL_SERVICE, workspace: workspace, libraryResult: libraryResult, unitResult: unitResult, error: diagnostic, ); List fixes; try { // TODO(srawlins): Somehow wrap each ProducerGenerator invocation in a // zone, to support `print` capturing per-plugin. fixes = await computeFixes(context); } on InconsistentAnalysisException { // TODO(srawlins): Is it important to at least log this? Or does it // happen on the regular? fixes = []; } if (fixes.isNotEmpty) { fixes.sort(Fix.compareFixes); var errorFixes = protocol.AnalysisErrorFixes(analysisError); errorFixesList.add(errorFixes); for (var fix in fixes) { errorFixes.fixes.add(protocol.PrioritizedSourceChange(1, fix.change)); } } } return protocol.EditGetFixesResult(errorFixesList); } /// Handles a 'plugin.versionCheck' request. Future handlePluginVersionCheck( protocol.PluginVersionCheckParams parameters, ) async { // TODO(srawlins): It seems improper for _this_ method to be the point where // the SDK path is configured... _sdkPath = parameters.sdkPath; return protocol.PluginVersionCheckResult(true, 'Plugin Server', '0.0.1', [ '*.dart', ]); } /// Initializes each of the registered plugins. Future initialize() async { await Future.wait( _plugins.map((p) => p.start()).whereType>(), ); } /// Starts this plugin by listening to the given communication [channel]. void start(PluginCommunicationChannel channel) { _channel = channel; _channel.listen( _handleRequestZoned, // TODO(srawlins): Implement. onDone: () {}, ); } /// Waits until all background drivers in the context collection are idle. @visibleForTesting Future waitForIdle() async { if (_contextCollection case var contextCollection?) { await Future.wait([ for (var analysisContext in contextCollection.contexts) analysisContext.driver.scheduler.waitForIdle(), ]); } } /// This method is invoked when a new instance of [AnalysisContextCollection] /// is created, so the plugin can perform initial analysis of analyzed files. Future _analyzeAllFilesInContextCollection({ required AnalysisContextCollectionImpl contextCollection, }) async { await _forAnalysisContexts(contextCollection, (analysisContext) async { final driver = analysisContext.driver; var paths = analysisContext.contextRoot .analyzedFiles() // TODO(srawlins): Enable analysis on other files, even if only // YAML files for analysis options and pubspec analysis and quick // fixes. .where((p) => file_paths.isDart(_resourceProvider.pathContext, p)) .toSet(); for (var path in paths) { driver.addFile(path); } }); } /// Computes and returns [protocol.AnalysisError]s for each of the parts in /// the library at [libraryPath]. Future>> _computeAnalysisErrors( AnalysisContext analysisContext, String libraryPath, { required AnalysisOptionsImpl analysisOptions, }) async { var libraryResult = await analysisContext.currentSession.getResolvedLibrary( libraryPath, ); if (libraryResult is! ResolvedLibraryResult) { // We only handle analyzing at the library-level. Below, we work through // each of the compilation units found in `libraryResult`. return const {}; } var diagnosticListeners = { for (var unitResult in libraryResult.units) unitResult: RecordingDiagnosticListener(), }; RuleContextUnit? definingContextUnit; var definingUnit = (libraryResult.element as LibraryElementImpl).firstFragment; var allUnits = []; for (var unitResult in libraryResult.units) { var contextUnit = RuleContextUnit( file: unitResult.file, content: unitResult.content, diagnosticReporter: DiagnosticReporter( diagnosticListeners[unitResult]!, unitResult.libraryElement.firstFragment.source, ), unit: unitResult.unit, ); allUnits.add(contextUnit); if (unitResult.unit.declaredFragment == definingUnit) { definingContextUnit = contextUnit; } } // Just a fallback value. We shouldn't get into this situation, but this is // a safe default. definingContextUnit ??= allUnits.first; // TODO(srawlins): Enable timing similar to what the linter package's // `benchmark.dart` script does. var ruleVisitorRegistry = RuleVisitorRegistryImpl(enableTiming: false); var package = analysisContext.contextRoot.workspace.findPackageFor( libraryPath, ); var context = RuleContextWithResolvedResults( allUnits, definingContextUnit, libraryResult.element.typeProvider, libraryResult.element.typeSystem as TypeSystemImpl, package, ); // A mapping from each diagnostic code to its corresponding plugin. var pluginCodeMapping = {}; // A mapping from each diagnostic code to its configured severity. var severityMapping = {}; for (var configuration in analysisOptions.pluginConfigurations) { runZonedGuarded( () => _computeDiagnosticsFromPlugin( configuration, analysisContext: analysisContext, allUnits: allUnits, ruleVisitorRegistry: ruleVisitorRegistry, context: context, pluginCodeMapping: pluginCodeMapping, severityMapping: severityMapping, ), _zoneOnErrorHandler, zoneSpecification: ZoneSpecification( print: (_, _, _, line) => _print(configuration.name, line), ), ); } // The list of the `AnalysisError`s and their associated // `protocol.AnalysisError`s. var diagnosticsAndAnalysisErrors = <({Diagnostic diagnostic, protocol.AnalysisError analysisError})>[]; diagnosticListeners.forEach((unitResult, listener) { var ignoreInfo = IgnoreInfo.forDart(unitResult.unit, unitResult.content); var diagnostics = listener.diagnostics.where((e) { var pluginName = pluginCodeMapping[e.diagnosticCode]; if (pluginName == null) { // If [e] is somehow not mapped, something is wrong; but don't mark it // as ignored. return true; } return !ignoreInfo.ignored(e, pluginName: pluginName); }); // The list of the `AnalysisError`s and their associated // `protocol.AnalysisError`s. var unitDiagnosticsAndAnalysisErrors = <({Diagnostic diagnostic, protocol.AnalysisError analysisError})>[]; for (var diagnostic in diagnostics) { unitDiagnosticsAndAnalysisErrors.add(( diagnostic: diagnostic, analysisError: protocol.AnalysisError( severityMapping[diagnostic.diagnosticCode] ?? protocol.AnalysisErrorSeverity.INFO, protocol.AnalysisErrorType.STATIC_WARNING, _locationFor(unitResult.unit, unitResult.path, diagnostic), diagnostic.message, diagnostic.diagnosticCode.lowerCaseName, correction: diagnostic.correctionMessage, contextMessages: diagnostic.contextMessages .map( (message) => newDiagnosticMessage( message, analysisContext.currentSession, lineInfo: message.filePath == unitResult.path ? unitResult.lineInfo : null, ), ) .nonNulls .toList(), // TODO(srawlins): Use a valid value here. hasFix: true, ), )); } _recentState[unitResult.path] = ( analysisContext: analysisContext, errors: [...unitDiagnosticsAndAnalysisErrors], ); diagnosticsAndAnalysisErrors.addAll(unitDiagnosticsAndAnalysisErrors); }); // A map that has a key for each unit's path. It is important to collect the // analysis errors for each unit, even if it has none. We must send a // notification for each unit, even if there are no analysis errors to // report. var analysisErrorsByPath = >{ for (var unitResult in libraryResult.units) unitResult.path: [], }; for (var (diagnostic: _, :analysisError) in diagnosticsAndAnalysisErrors) { analysisErrorsByPath[analysisError.location.file]!.add(analysisError); } return analysisErrorsByPath; } /// Computes diagnostics for the plugin configured with [configuration]. /// /// [pluginCodeMapping] and [severityMapping] are output parameters; this /// function adds mappings, which can be used at the call-site outside of the /// [Zone]. void _computeDiagnosticsFromPlugin( PluginConfiguration configuration, { required AnalysisContext analysisContext, required List allUnits, required RuleVisitorRegistryImpl ruleVisitorRegistry, required RuleContextWithResolvedResults context, required Map pluginCodeMapping, required Map severityMapping, }) { if (!configuration.isEnabled) return; var registry = registries[configuration.name] ?? Registry.ruleRegistry; var rules = registry.enabled({ for (var entry in configuration.diagnosticConfigs.entries) entry.key.toLowerCase(): entry.value, }); for (var code in rules.expand((r) => r.diagnosticCodes)) { pluginCodeMapping.putIfAbsent(code, () => configuration.name); severityMapping.putIfAbsent( code, () => _configuredSeverity(configuration, code), ); } for (var rule in rules) { // TODO(srawlins): Enable timing similar to what the linter package's // `benchmark.dart` script does. rule.registerNodeProcessors(ruleVisitorRegistry, context); } // Now to perform the actual analysis. for (var currentUnit in allUnits) { if (!analysisContext.contextRoot.isAnalyzed(currentUnit.file.path)) { // If the file isn't analyzed, then we shouldn't be running any rules // on it. This can happen when the library has parts that aren't // analyzed (e.g. because they are excluded via analysis options). continue; } for (var rule in rules) { rule.reporter = currentUnit.diagnosticReporter; } context.currentUnit = currentUnit; currentUnit.unit.accept( AnalysisRuleVisitor( ruleVisitorRegistry, shouldPropagateExceptions: true, ), ); } // Now that all lint rules have visited the code in each of the compilation // units, we can accept each lint rule's `afterLibrary` hook. AnalysisRuleVisitor(ruleVisitorRegistry).afterLibrary(); } /// Converts the severity of [code] into a [protocol.AnalysisErrorSeverity]. protocol.AnalysisErrorSeverity? _configuredSeverity( PluginConfiguration configuration, DiagnosticCode code, ) { var configuredSeverity = configuration.diagnosticConfigs[code.lowerCaseName]?.severity; if (configuredSeverity != null && configuredSeverity != ConfiguredSeverity.enable) { var severityName = configuredSeverity.name.toUpperCase(); var severity = protocol.AnalysisErrorSeverity.values .asNameMap()[severityName]; assert( severity != null, 'Invalid configured severity: ${configuredSeverity.name}', ); return severity; } // Fall back to the declared severity of [code]. var severityName = code.severity.name.toUpperCase(); var severity = protocol.AnalysisErrorSeverity.values .asNameMap()[code.severity.name]; assert(severity != null, 'Invalid severity: $severityName'); return severity; } /// Invokes [fn] first for priority analysis contexts, then for the rest. Future _forAnalysisContexts( AnalysisContextCollectionImpl contextCollection, Future Function(DriverBasedAnalysisContext analysisContext) fn, ) async { var nonPriorityAnalysisContexts = []; for (var analysisContext in contextCollection.contexts) { if (_isPriorityAnalysisContext(analysisContext)) { await fn(analysisContext); } else { nonPriorityAnalysisContexts.add(analysisContext); } } for (var analysisContext in nonPriorityAnalysisContexts) { await fn(analysisContext); } } /// Computes the response for the given [request]. Future _getResponse(Request request, int requestTime) async { ResponseResult? result; switch (request.method) { case protocol.ANALYSIS_REQUEST_HANDLE_WATCH_EVENTS: var params = protocol.AnalysisHandleWatchEventsParams.fromRequest( request, ); result = await _handleAnalysisWatchEvents(params); case protocol.ANALYSIS_REQUEST_SET_CONTEXT_ROOTS: // This request is for legacy plugins. New plugins use // `protocol.ANALYSIS_REQUEST_SET_ANALYSIS_ROOTS`. result = null; case protocol.ANALYSIS_REQUEST_SET_ANALYSIS_ROOTS: var params = protocol.AnalysisSetAnalysisRootsParams.fromRequest( request, ); result = await _handleAnalysisSetAnalysisRoots(params); case protocol.ANALYSIS_REQUEST_SET_PRIORITY_FILES: var params = protocol.AnalysisSetPriorityFilesParams.fromRequest( request, ); result = await handleAnalysisSetPriorityFiles(params); case protocol.ANALYSIS_REQUEST_UPDATE_CONTENT: var params = protocol.AnalysisUpdateContentParams.fromRequest(request); result = await _handleAnalysisUpdateContent(params); case protocol.EDIT_REQUEST_GET_ASSISTS: var params = protocol.EditGetAssistsParams.fromRequest(request); result = await handleEditGetAssists(params); case protocol.EDIT_REQUEST_GET_FIXES: var params = protocol.EditGetFixesParams.fromRequest(request); result = await handleEditGetFixes(params); case protocol.PLUGIN_REQUEST_DETAILS: var details = []; for (var pluginRegistry in registries.values) { var assists = [ for (var assistKind in pluginRegistry.assistKinds) protocol.AssistDescription(assistKind.id, assistKind.message), ]; var fixes = [ for (var MapEntry(key: fixKind, value: codes) in pluginRegistry.fixKinds.entries) protocol.FixDescription(fixKind.id, fixKind.message, codes), ]; details.add( protocol.PluginDetails( pluginRegistry.pluginName, pluginRegistry.lintRules.keys.toList(), pluginRegistry.warningRules.keys.toList(), assists, fixes, ), ); } result = protocol.PluginDetailsResult(details); case protocol.PLUGIN_REQUEST_SHUTDOWN: await Future.wait( _plugins.map((p) => p.shutDown()).whereType>(), ); _channel.sendResponse( protocol.PluginShutdownResult().toResponse(request.id, requestTime), ); _channel.close(); return null; case protocol.PLUGIN_REQUEST_VERSION_CHECK: var params = protocol.PluginVersionCheckParams.fromRequest(request); result = await handlePluginVersionCheck(params); default: // Anything else is unsupported. result = null; } if (result == null) { return Response( request.id, requestTime, error: RequestErrorFactory.unknownRequest(request.method), ); } return result.toResponse(request.id, requestTime); } /// Handles an 'analysis.setAnalysisRoots' request. Future _handleAnalysisSetAnalysisRoots( protocol.AnalysisSetAnalysisRootsParams parameters, ) async { if (_contextCollection case var contextCollection?) { _contextCollection = null; await contextCollection.dispose(); } if (_eventsSubscription case var eventsSubscription?) { _eventsSubscription = null; await eventsSubscription.cancel(); } var contextCollection = AnalysisContextCollectionImpl( resourceProvider: _resourceProvider, includedPaths: parameters.included, byteStore: _byteStore, sdkPath: _sdkPath, fileContentCache: FileContentCache(_resourceProvider), updateAnalysisOptions4: // Disable extra warning computation and lint computation, because // these are reported in the main analysis server isolate, not in the // plugins isolate. ({required AnalysisOptionsImpl analysisOptions}) => analysisOptions ..warning = false ..lint = false, withFineDependencies: true, drainStreams: false, ); _contextCollection = contextCollection; _updatePriorityFiles(); _eventsSubscription = contextCollection.scheduler.events.listen((event) { if (event is ResolvedUnitResult) { _handleResolvedUnit(event); } else if (event is AnalysisStatus) { _handleAnalysisStatus(event); } else if (event is ErrorsResult) { _handleErrorsResult(event); } }); await _analyzeAllFilesInContextCollection( contextCollection: contextCollection, ); return protocol.AnalysisSetAnalysisRootsResult(); } void _handleAnalysisStatus(AnalysisStatus status) { _channel.sendNotification( protocol.PluginStatusParams( analysis: protocol.AnalysisStatus(status.isWorking), ).toNotification(), ); } /// Handles an 'analysis.updateContent' request. /// /// Throws a [RequestFailure] if the request could not be handled. Future _handleAnalysisUpdateContent( protocol.AnalysisUpdateContentParams parameters, ) async { var changedPaths = {}; var paths = parameters.files; paths.forEach((String path, Object overlay) { // Prepare the old overlay contents. String? oldContent; try { oldContent = _resourceProvider.getFile(path).readAsStringSync(); } catch (_) { // Leave `oldContent` empty. } // Prepare the new contents. String? newContent; if (overlay is protocol.AddContentOverlay) { newContent = overlay.content; } else if (overlay is protocol.ChangeContentOverlay) { if (oldContent == null) { // The server should only send a ChangeContentOverlay if there is // already an existing overlay for the source. throw RequestFailure( RequestErrorFactory.invalidOverlayChangeNoContent(), ); } try { newContent = protocol.SourceEdit.applySequence( oldContent, overlay.edits, ); } on RangeError { throw RequestFailure( RequestErrorFactory.invalidOverlayChangeInvalidEdit(), ); } } else if (overlay is protocol.RemoveContentOverlay) { newContent = null; } if (newContent != null) { if (newContent != oldContent) { _resourceProvider.setOverlay( path, content: newContent, modificationStamp: _overlayModificationStamp++, ); changedPaths.add(path); } } else { if (_resourceProvider.hasOverlay(path)) { _resourceProvider.removeOverlay(path); changedPaths.add(path); } } }); await _handleContentChanged(modifiedPaths: changedPaths.toList()); return protocol.AnalysisUpdateContentResult(); } /// Handles an 'analysis.handleWatchEvents' request. Future _handleAnalysisWatchEvents( protocol.AnalysisHandleWatchEventsParams parameters, ) async { final addedPaths = parameters.events .where((e) => e.type == protocol.WatchEventType.ADD) .map((e) => e.path) .toList(); final modifiedPaths = parameters.events .where((e) => e.type == protocol.WatchEventType.MODIFY) .map((e) => e.path) .toList(); final removedPaths = parameters.events .where((e) => e.type == protocol.WatchEventType.REMOVE) .map((e) => e.path) .toList(); await _handleContentChanged( addedPaths: addedPaths, modifiedPaths: modifiedPaths, removedPaths: removedPaths, ); return protocol.AnalysisHandleWatchEventsResult(); } /// Handles added files, modified files, and removed files. Future _handleContentChanged({ List addedPaths = const [], List modifiedPaths = const [], List removedPaths = const [], }) async { if (_contextCollection case var contextCollection?) { _filesBeingAnalyzed.clear(); _filesBeingResolved.clear(); await _forAnalysisContexts(contextCollection, (analysisContext) async { final driver = analysisContext.driver; for (var path in modifiedPaths) { driver.changeFile(path); } for (var path in removedPaths) { driver.removeFile(path); } for (var path in addedPaths) { driver.addFile(path); } }); for (var path in removedPaths) { _channel.sendNotification( protocol.AnalysisErrorsParams(path, []).toNotification(), ); } } } void _handleErrorsResult(ErrorsResult event) { var path = event.path; var session = event.session; var analysisContext = session.analysisContext; if (analysisContext is DriverBasedAnalysisContext) { if (analysisContext.contextRoot.isAnalyzed(path)) { var activeSession = _filesBeingResolved[path]; if (activeSession != session) { _filesBeingResolved[path] = session; analysisContext.driver.getResolvedUnit(path); } } } } Future _handleRequest(Request request) async { var requestTime = DateTime.now().millisecondsSinceEpoch; var id = request.id; Response? response; try { response = await _getResponse(request, requestTime); } on RequestFailure catch (exception) { response = Response(id, requestTime, error: exception.error); } catch (exception, stackTrace) { response = Response( id, requestTime, error: protocol.RequestError( protocol.RequestErrorCode.PLUGIN_ERROR, exception.toString(), stackTrace: stackTrace.toString(), ), ); } if (response != null) { _channel.sendResponse(response); } } Future _handleRequestZoned(Request request) async { await runZonedGuarded( () => _handleRequest(request), _zoneOnErrorHandler, zoneSpecification: ZoneSpecification( print: (_, _, _, line) => _print('', line), ), ); } Future _handleResolvedUnit(ResolvedUnitResult unitResult) async { var analysisContext = unitResult.session.analysisContext; if (!analysisContext.contextRoot.isAnalyzed(unitResult.path)) { return; } if (unitResult.unit.declaredFragment != unitResult.libraryElement.firstFragment) { return; } var session = unitResult.session; var activeSession = _filesBeingAnalyzed[unitResult.path]; if (activeSession == session) { return; } _filesBeingAnalyzed[unitResult.path] = session; try { var file = _resourceProvider.getFile(unitResult.path); var analysisOptions = analysisContext.getAnalysisOptionsForFile(file); var analysisErrorsByPath = await _computeAnalysisErrors( analysisContext, unitResult.path, analysisOptions: analysisOptions as AnalysisOptionsImpl, ); for (var MapEntry(key: path, value: analysisErrors) in analysisErrorsByPath.entries) { _channel.sendNotification( protocol.AnalysisErrorsParams(path, analysisErrors).toNotification(), ); } } on InconsistentAnalysisException { // State changed during resolution; a subsequent pass will handle it. } } bool _isPriorityAnalysisContext(AnalysisContext analysisContext) => _priorityPaths.any(analysisContext.contextRoot.isAnalyzed); void _print(String pluginName, String message) { _channel.sendNotification( protocol.PluginPrintParams( protocol.PluginPrint( pluginName, message, DateTime.now().millisecondsSinceEpoch, ), ).toNotification(), ); } void _updatePriorityFiles() { if (_contextCollection case var contextCollection?) { for (var analysisContext in contextCollection.contexts) { analysisContext.driver.priorityFiles = _priorityPaths.toList(); } } } /// The `onError` handler for use in [runZonedGuarded]. void _zoneOnErrorHandler(Object error, StackTrace stackTrace) { _channel.sendNotification( protocol.PluginErrorParams( false /* isFatal */, error.toString(), stackTrace.toString(), ).toNotification(), ); } static protocol.Location _locationFor( CompilationUnit unit, String path, Diagnostic diagnostic, ) { var lineInfo = unit.lineInfo; var startLocation = lineInfo.getLocation(diagnostic.offset); var endLocation = lineInfo.getLocation( diagnostic.offset + diagnostic.length, ); return protocol.Location( path, diagnostic.offset, diagnostic.length, startLocation.lineNumber, startLocation.columnNumber, endLine: endLocation.lineNumber, endColumn: endLocation.columnNumber, ); } }