efa1c497d6
In testing, I saw that _added_ files were not being analyzed at all by plugins. It turned out that `PluginManager.broadcastWatchEvent` uses that glob to decide if watch events should be broadcast to a given plugin isolate. It uses this Glob to determine that answer: ```dart Glob( _resourceProvider.pathContext.separator, pattern, ).matches(filePath) ``` and using `'*.dart'` for that pattern, always answers "no, this file does not match '*.dart'"; I believe `**.dart` is the correct glob pattern we should use. Change-Id: Id99d758151629c485884ba20e4d1f49f574f5889 Reviewed-on: https://dart-review.googlesource.com/c/sdk/+/452160 Commit-Queue: Samuel Rawlins <srawlins@google.com> Reviewed-by: Brian Wilkerson <brianwilkerson@google.com>
893 lines
31 KiB
Dart
893 lines
31 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: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,
|
|
);
|
|
}
|
|
}
|