blob: c476e2b8c9143e681ad5c059dc89ed8f9221ab6b [file] [log] [blame]
// 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/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 list of registered features for each plugin, for reporting purposes.
final _registries = <PluginRegistryImpl>[];
PluginServer({
required ResourceProvider resourceProvider,
required List<Plugin> plugins,
}) : _resourceProvider = OverlayResourceProvider(resourceProvider),
_plugins = plugins {
for (var plugin in plugins) {
var registry = PluginRegistryImpl(plugin.name);
_registries.add(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.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<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.getResolvedLibrary(
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 {
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 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(),
);
}
}
/// 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,
);
}
}
/// 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 nodeRegistry = 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) {
if (!configuration.isEnabled) continue;
// TODO(srawlins): Namespace rules by their plugin, to avoid collisions.
var rules = Registry.ruleRegistry.enabled(
configuration.diagnosticConfigs,
);
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(nodeRegistry, context);
}
// Now to perform the actual analysis.
for (var currentUnit in allUnits) {
for (var rule in rules) {
rule.reporter = currentUnit.diagnosticReporter;
}
context.currentUnit = currentUnit;
currentUnit.unit.accept(
AnalysisRuleVisitor(nodeRegistry, shouldPropagateExceptions: true),
);
}
}
// TODO(srawlins): Support `AnalysisRuleVisitor.afterLibrary`. See how it is
// used in `library_analyzer.dart`.
// 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);
});
for (var diagnostic in diagnostics) {
diagnosticsAndAnalysisErrors.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.name,
correction: diagnostic.correctionMessage,
// TODO(srawlins): Use a valid value here.
hasFix: true,
),
));
}
});
_recentState[libraryPath] = (
analysisContext: analysisContext,
errors: [...diagnosticsAndAnalysisErrors],
);
// 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;
}
/// Converts the severity of [code] into a [protocol.AnalysisErrorSeverity].
protocol.AnalysisErrorSeverity? _configuredSeverity(
PluginConfiguration configuration,
DiagnosticCode code,
) {
var configuredSeverity =
configuration.diagnosticConfigs[code.name]?.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) {
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,
pluginRegistry.warningRules,
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,
);
_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), (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,
);
}
}