// 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. 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: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/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/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/file_content_cache.dart'; import 'package:analyzer/src/dart/element/type_system.dart'; import 'package:analyzer/src/ignore_comments/ignore_info.dart'; import 'package:analyzer/src/lint/linter.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'; typedef _ErrorAndProtocolError = ({ Diagnostic diagnostic, protocol.AnalysisError protocolError, }); typedef _PluginState = ({ AnalysisContext analysisContext, List<_ErrorAndProtocolError> 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; final _registry = PluginRegistryImpl(); /// The recent state of analysis reults, to be cleared on file changes. final _recentState = {}; /// The next modification stamp for a changed file in the [resourceProvider]. int _overlayModificationStamp = 0; PluginServer({ required ResourceProvider resourceProvider, required List plugins, }) : _resourceProvider = OverlayResourceProvider(resourceProvider), _plugins = plugins { for (var plugin in plugins) { plugin.register(_registry); } _registry.registerIgnoreProducerGenerators(); } /// 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(); 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.getResolvedLibrary(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.getResolvedLibrary(path); if (libraryResult is! ResolvedLibraryResult) { return protocol.EditGetFixesResult(const []); } var unitResult = libraryResult.unitWithPath(path); if (unitResult is! ResolvedUnitResult) { return protocol.EditGetFixesResult(const []); } var lintAtOffset = errors.where((error) => error.diagnostic.offset == offset); if (lintAtOffset.isEmpty) return protocol.EditGetFixesResult(const []); var errorFixesList = []; var workspace = DartChangeWorkspace([analysisContext.currentSession]); for (var (:diagnostic, :protocolError) 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 { 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(protocolError); 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: () {}); } /// 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 AnalysisContextCollection contextCollection, }) async { _channel.sendNotification( protocol.PluginStatusParams(analysis: protocol.AnalysisStatus(true)) .toNotification()); await _forAnalysisContexts(contextCollection, (analysisContext) async { 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(); await _analyzeFiles( analysisContext: analysisContext, paths: paths, ); }); _channel.sendNotification( protocol.PluginStatusParams(analysis: protocol.AnalysisStatus(false)) .toNotification()); } Future _analyzeFile({ required AnalysisContext analysisContext, required String path, }) async { var file = _resourceProvider.getFile(path); var analysisOptions = analysisContext.getAnalysisOptionsForFile(file); var diagnostics = await _computeDiagnostics( analysisContext, path, analysisOptions: analysisOptions as AnalysisOptionsImpl, ); _channel.sendNotification( protocol.AnalysisErrorsParams(path, diagnostics).toNotification()); } /// Analyzes the files at the given [paths]. Future _analyzeFiles({ required AnalysisContext analysisContext, required Set paths, }) async { // First analyze priority files. for (var path in _priorityPaths) { if (paths.remove(path)) { await _analyzeFile(analysisContext: analysisContext, path: path); } } // Then analyze the remaining files. for (var path in paths) { await _analyzeFile(analysisContext: analysisContext, path: path); } } Future> _computeDiagnostics( AnalysisContext analysisContext, String path, { required AnalysisOptionsImpl analysisOptions, }) async { var libraryResult = await analysisContext.currentSession.getResolvedLibrary(path); if (libraryResult is! ResolvedLibraryResult) { return const []; } var unitResult = await analysisContext.currentSession.getResolvedUnit(path); if (unitResult is! ResolvedUnitResult) { return const []; } var listener = RecordingDiagnosticListener(); var diagnosticReporter = DiagnosticReporter( listener, unitResult.libraryElement2.firstFragment.source); var currentUnit = RuleContextUnit( file: unitResult.file, content: unitResult.content, diagnosticReporter: diagnosticReporter, unit: unitResult.unit, ); var allUnits = [ for (var unitResult in libraryResult.units) RuleContextUnit( file: unitResult.file, content: unitResult.content, diagnosticReporter: diagnosticReporter, unit: unitResult.unit, ), ]; // TODO(srawlins): Enable timing similar to what the linter package's // `benchhmark.dart` script does. var nodeRegistry = RuleVisitorRegistryImpl(enableTiming: false); var context = RuleContextWithResolvedResults( allUnits, currentUnit, libraryResult.element2.typeProvider, libraryResult.element2.typeSystem as TypeSystemImpl, // TODO(srawlins): Support 'package' parameter. null, ); // A mapping from each lint and warning code to its corresponding plugin. var pluginCodeMapping = {}; for (var configuration in analysisOptions.pluginConfigurations) { if (!configuration.isEnabled) continue; // TODO(srawlins): Namespace rules by their plugin, to avoid collisions. var rules = Registry.ruleRegistry.enabled(configuration.diagnosticConfigs); for (var rule in rules) { rule.reporter = diagnosticReporter; // TODO(srawlins): Enable timing similar to what the linter package's // `benchhmark.dart` script does. rule.registerNodeProcessors(nodeRegistry, context); } for (var code in rules.expand((r) => r.diagnosticCodes)) { var existingPlugin = pluginCodeMapping[code.name]; if (existingPlugin == null) { pluginCodeMapping[code.name] = configuration.name; } } } context.currentUnit = currentUnit; currentUnit.unit.accept( AnalysisRuleVisitor(nodeRegistry, shouldPropagateExceptions: true)); var ignoreInfo = IgnoreInfo.forDart(unitResult.unit, unitResult.content); var diagnostics = listener.diagnostics.where((e) { var pluginName = pluginCodeMapping[e.diagnosticCode.name]; 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 diagnosticsAndProtocolErrors = [ for (var diagnostic in diagnostics) ( diagnostic: diagnostic, protocolError: protocol.AnalysisError( _severityOf(diagnostic), protocol.AnalysisErrorType.STATIC_WARNING, _locationFor(currentUnit.unit, path, diagnostic), diagnostic.message, diagnostic.diagnosticCode.name, correction: diagnostic.correctionMessage, // TODO(srawlins): Use a valid value here. hasFix: true, ) ), ]; _recentState[path] = ( analysisContext: analysisContext, errors: [...diagnosticsAndProtocolErrors], ); return diagnosticsAndProtocolErrors.map((e) => e.protocolError).toList(); } /// Converts the severity of [diagnostic] into a /// [protocol.AnalysisErrorSeverity]. protocol.AnalysisErrorSeverity _severityOf(Diagnostic diagnostic) { try { return protocol.AnalysisErrorSeverity.values .byName(diagnostic.severity.name.toUpperCase()); } catch (_) { assert(false, 'Invalid severity: ${diagnostic.severity}'); // Return the default severity of `LintCode`. return protocol.AnalysisErrorSeverity.INFO; } } /// Invokes [fn] first for priority analysis contexts, then for the rest. Future _forAnalysisContexts( AnalysisContextCollection contextCollection, Future Function(AnalysisContext 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_GET_NAVIGATION: case protocol.ANALYSIS_REQUEST_HANDLE_WATCH_EVENTS: result = null; case protocol.ANALYSIS_REQUEST_SET_CONTEXT_ROOTS: var params = protocol.AnalysisSetContextRootsParams.fromRequest(request); result = await _handleAnalysisSetContextRoots(params); case protocol.ANALYSIS_REQUEST_SET_PRIORITY_FILES: case protocol.ANALYSIS_REQUEST_SET_SUBSCRIPTIONS: case protocol.ANALYSIS_REQUEST_UPDATE_CONTENT: var params = protocol.AnalysisUpdateContentParams.fromRequest(request); result = await _handleAnalysisUpdateContent(params); case protocol.COMPLETION_REQUEST_GET_SUGGESTIONS: result = null; case protocol.EDIT_REQUEST_GET_ASSISTS: var params = protocol.EditGetAssistsParams.fromRequest(request); result = await handleEditGetAssists(params); case protocol.EDIT_REQUEST_GET_AVAILABLE_REFACTORINGS: result = null; case protocol.EDIT_REQUEST_GET_FIXES: var params = protocol.EditGetFixesParams.fromRequest(request); result = await handleEditGetFixes(params); case protocol.EDIT_REQUEST_GET_REFACTORING: result = null; case protocol.PLUGIN_REQUEST_SHUTDOWN: _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); } if (result == null) { return Response(request.id, requestTime, error: RequestErrorFactory.unknownRequest(request.method)); } return result.toResponse(request.id, requestTime); } /// Handles files that might have been affected by a content change of /// one or more files. The implementation may check if these files should /// be analyzed, do such analysis, and send diagnostics. /// /// By default invokes [_analyzeFiles] only for files that are analyzed in /// this [analysisContext]. Future _handleAffectedFiles({ required AnalysisContext analysisContext, required List paths, }) async { var analyzedPaths = paths.where(analysisContext.contextRoot.isAnalyzed).toSet(); await _analyzeFiles( analysisContext: analysisContext, paths: analyzedPaths, ); } /// Handles an 'analysis.setContextRoots' request. Future _handleAnalysisSetContextRoots( protocol.AnalysisSetContextRootsParams parameters) async { var currentContextCollection = _contextCollection; if (currentContextCollection != null) { _contextCollection = null; await currentContextCollection.dispose(); } var includedPaths = parameters.roots.map((e) => e.root).toList(); var contextCollection = AnalysisContextCollectionImpl( resourceProvider: _resourceProvider, includedPaths: includedPaths, byteStore: _byteStore, sdkPath: _sdkPath, fileContentCache: FileContentCache(_resourceProvider), ); _contextCollection = contextCollection; await _analyzeAllFilesInContextCollection( contextCollection: contextCollection); return protocol.AnalysisSetContextRootsResult(); } /// 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 { if (_resourceProvider.hasOverlay(path)) { 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) { _resourceProvider.setOverlay( path, content: newContent, modificationStamp: _overlayModificationStamp++, ); } else { _resourceProvider.removeOverlay(path); } changedPaths.add(path); }); await _handleContentChanged(changedPaths.toList()); return protocol.AnalysisUpdateContentResult(); } /// Handles the fact that files with [paths] were changed. Future _handleContentChanged(List paths) async { if (_contextCollection case var contextCollection?) { _channel.sendNotification( protocol.PluginStatusParams(analysis: protocol.AnalysisStatus(true)) .toNotification()); await _forAnalysisContexts(contextCollection, (analysisContext) async { for (var path in paths) { analysisContext.changeFile(path); } var affected = await analysisContext.applyPendingFileChanges(); await _handleAffectedFiles( analysisContext: analysisContext, paths: affected); }); _channel.sendNotification( protocol.PluginStatusParams(analysis: protocol.AnalysisStatus(false)) .toNotification()); } } 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), (error, stackTrace) { _channel.sendNotification(protocol.PluginErrorParams( false /* isFatal */, error.toString(), stackTrace.toString()) .toNotification()); }, ); } bool _isPriorityAnalysisContext(AnalysisContext analysisContext) => _priorityPaths.any(analysisContext.contextRoot.isAnalyzed); 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, ); } }