Files
sdk/pkg/analysis_server_plugin/lib/src/plugin_server.dart
T
Sam Rawlins 7d0a17196c DAS plugins: Add print-debugging support in Insights pages.
Work towards https://github.com/dart-lang/sdk/issues/61868

This adds a new notification type, 'PluginPrint'. There are several
fields and variables then named 'pluginPrint' or 'print', and I am
definitely open to changing these names, but this is the best one that
I thought of.

PluginPrint has three fields: The name of the plugin that printed, the
message that was printed, and the timestamp.

We wrap each plugin's AnalysisRule invocations with a zone, so that
the `print` handler can know the name of the plugin. The prints are
caught and sent to the server isolate as Notifications. The
PluginIsolate then stores the collected prints. The Plugins Insights
page can then retrieve them and display them.

Manual testing, with 1000 libraries that get new lint reported once per
file, I did not observe a negative performance impact.

Change-Id: Id800ae41781c15ce89d06b563878c2b0edae43d3
Reviewed-on: https://dart-review.googlesource.com/c/sdk/+/486827
Reviewed-by: Brian Wilkerson <brianwilkerson@google.com>
Commit-Queue: Samuel Rawlins <srawlins@google.com>
2026-03-16 14:56:59 -07:00

1006 lines
36 KiB
Dart

// 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: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/file_content_cache.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<String> _priorityPaths = {};
final List<Plugin> _plugins;
/// The recent state of analysis reults, to be cleared on file changes.
final _recentState = <String, _PluginState>{};
/// 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 = <String, PluginRegistryImpl>{};
PluginServer({
required ResourceProvider resourceProvider,
required List<Plugin> 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<String, Plugin> 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<String> get priorityPaths => {..._priorityPaths};
/// Handles an 'analysis.setPriorityFiles' request.
///
/// Throws a [RequestFailure] if the request could not be handled.
Future<protocol.AnalysisSetPriorityFilesResult>
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<protocol.EditGetAssistsResult> 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<Assist> 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<protocol.EditGetFixesResult> 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 = <protocol.AnalysisErrorFixes>[];
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<Fix> 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<protocol.PluginVersionCheckResult> 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<void> initialize() async {
await Future.wait(
_plugins.map((p) => p.start()).whereType<Future<Object?>>(),
);
}
/// 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<void> _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 _analyzeLibraries(analysisContext: analysisContext, paths: paths);
});
_channel.sendNotification(
protocol.PluginStatusParams(
analysis: protocol.AnalysisStatus(false),
).toNotification(),
);
}
/// Analyzes the libraries at the given [paths].
// TODO(srawlins): Refactor how libraries are analyzed using AnalysisDriver,
// to be similar to what analysis server does:
//
// 1. When the analysis roots change it creates the AnalysisContextCollection
// and listens to the drivers' streams of results.
// 2. When a file changes, it lets the driver know about it.
// 3. As the driver analyzes the potentially impacted files it
// a. runs the lints that have been enabled and
// b. puts the result on the stream.
// 4. When a result is on the stream the server grabs the results and sends
// the diagnostics to the client.
//
// It doesn't ever explicitly ask the driver which files were impacted and it
// doesn't explicitly run the lints directly because the driver will do that
// implicitly. There would be benefits to plugins working the same way:
//
// * The driver can do a more efficient job of scheduling analysis than the
// server can (because of having a more complete picture).
// * It means there's only one way that we're trying to use the analyzer so it
// will be easier to make changes to the analyzer when we need to (smaller
// API exposure).
// * The logic for doing analysis is in one place so it's easier to reason
// about.
// * We don't need to be familiar with two different architectures.
Future<void> _analyzeLibraries({
required AnalysisContext analysisContext,
required Set<String> paths,
}) async {
// First analyze priority files.
for (var path in _priorityPaths) {
if (paths.remove(path)) {
await _analyzeLibrary(
analysisContext: analysisContext,
libraryPath: path,
);
}
}
// Then analyze the remaining files.
for (var path in paths) {
await _analyzeLibrary(
analysisContext: analysisContext,
libraryPath: path,
);
}
}
/// Analyzes the library at the given [libraryPath], sending an
/// 'analysis.errors' [Notification] for each compilation unit.
Future<void> _analyzeLibrary({
required AnalysisContext analysisContext,
required String libraryPath,
}) async {
var file = _resourceProvider.getFile(libraryPath);
var analysisOptions = analysisContext.getAnalysisOptionsForFile(file);
var analysisErrorsByPath = await _computeAnalysisErrors(
analysisContext,
libraryPath,
analysisOptions: analysisOptions as AnalysisOptionsImpl,
);
for (var MapEntry(key: path, value: analysisErrors)
in analysisErrorsByPath.entries) {
_channel.sendNotification(
protocol.AnalysisErrorsParams(path, analysisErrors).toNotification(),
);
}
}
/// Computes and returns [protocol.AnalysisError]s for each of the parts in
/// the library at [libraryPath].
Future<Map<String, List<protocol.AnalysisError>>> _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 = <RuleContextUnit>[];
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 = <DiagnosticCode, String>{};
// A mapping from each diagnostic code to its configured severity.
var severityMapping = <DiagnosticCode, protocol.AnalysisErrorSeverity?>{};
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 = <String, List<protocol.AnalysisError>>{
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<RuleContextUnit> allUnits,
required RuleVisitorRegistryImpl ruleVisitorRegistry,
required RuleContextWithResolvedResults context,
required Map<DiagnosticCode, String> pluginCodeMapping,
required Map<DiagnosticCode, protocol.AnalysisErrorSeverity?>
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<void> _forAnalysisContexts(
AnalysisContextCollection contextCollection,
Future<void> Function(AnalysisContext analysisContext) fn,
) async {
var nonPriorityAnalysisContexts = <AnalysisContext>[];
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<Response?> _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:
var params = protocol.AnalysisSetContextRootsParams.fromRequest(
request,
);
result = await _handleAnalysisSetContextRoots(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 = <protocol.PluginDetails>[];
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:
_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 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 [_analyzeLibraries] only for files that are analyzed in
/// this [analysisContext].
Future<void> _handleAffectedFiles({
required AnalysisContext analysisContext,
required List<String> paths,
}) async {
var analyzedPaths = paths
.where(analysisContext.contextRoot.isAnalyzed)
.toSet();
await _analyzeLibraries(
analysisContext: analysisContext,
paths: analyzedPaths,
);
}
/// Handles an 'analysis.setContextRoots' request.
Future<protocol.AnalysisSetContextRootsResult> _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),
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,
);
_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<protocol.AnalysisUpdateContentResult> _handleAnalysisUpdateContent(
protocol.AnalysisUpdateContentParams parameters,
) async {
var changedPaths = <String>{};
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(modifiedPaths: changedPaths.toList());
return protocol.AnalysisUpdateContentResult();
}
/// Handles an 'analysis.handleWatchEvents' request.
Future<protocol.AnalysisHandleWatchEventsResult> _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<void> _handleContentChanged({
List<String> addedPaths = const [],
List<String> modifiedPaths = const [],
List<String> removedPaths = const [],
}) async {
if (_contextCollection case var contextCollection?) {
_channel.sendNotification(
protocol.PluginStatusParams(
analysis: protocol.AnalysisStatus(true),
).toNotification(),
);
await _forAnalysisContexts(contextCollection, (analysisContext) async {
for (var path in modifiedPaths) {
analysisContext.changeFile(path);
}
for (var path in removedPaths) {
analysisContext.changeFile(path);
}
var affected = [
...await analysisContext.applyPendingFileChanges(),
...addedPaths,
];
await _handleAffectedFiles(
analysisContext: analysisContext,
paths: affected,
);
});
_channel.sendNotification(
protocol.PluginStatusParams(
analysis: protocol.AnalysisStatus(false),
).toNotification(),
);
}
}
Future<void> _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<void> _handleRequestZoned(Request request) async {
await runZonedGuarded(
() => _handleRequest(request),
_zoneOnErrorHandler,
zoneSpecification: ZoneSpecification(
print: (_, _, _, line) => _print('<shared plugin code>', line),
),
);
}
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(),
);
}
/// 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,
);
}
}