b8b1e01173
In the recent change, https://dart-review.googlesource.com/c/sdk/+/503040, the asynchronous nature of the change required adding _new_ hooks to track that the plugin isolate is analyzing or not. I missed removing the old hooks. The result is that an invocation of `dart analyze` will get a notification from the plugin isolate that it is analyzing, and then _immediately_ a notification indicating that it is not. So this change removes the old hooks. Change-Id: Idc871d25c043ee1be94030729a1fd8226bdc753f Reviewed-on: https://dart-review.googlesource.com/c/sdk/+/504140 Reviewed-by: Brian Wilkerson <brianwilkerson@google.com> Commit-Queue: Samuel Rawlins <srawlins@google.com>
1014 lines
36 KiB
Dart
1014 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/driver_based_analysis_context.dart';
|
|
import 'package:analyzer/src/dart/analysis/file_content_cache.dart';
|
|
import 'package:analyzer/src/dart/analysis/status.dart';
|
|
import 'package:analyzer/src/dart/element/element.dart';
|
|
import 'package:analyzer/src/dart/element/type_system.dart';
|
|
import 'package:analyzer/src/ignore_comments/ignore_info.dart';
|
|
import 'package:analyzer/src/lint/config.dart';
|
|
import 'package:analyzer/src/lint/linter_visitor.dart';
|
|
import 'package:analyzer/src/lint/registry.dart';
|
|
import 'package:analyzer/src/util/file_paths.dart' as file_paths;
|
|
import 'package:analyzer_plugin/channel/channel.dart';
|
|
import 'package:analyzer_plugin/protocol/protocol.dart';
|
|
import 'package:analyzer_plugin/protocol/protocol_common.dart' as protocol;
|
|
import 'package:analyzer_plugin/protocol/protocol_constants.dart' as protocol;
|
|
import 'package:analyzer_plugin/protocol/protocol_generated.dart' as protocol;
|
|
import 'package:analyzer_plugin/src/protocol/protocol_internal.dart';
|
|
import 'package:meta/meta.dart';
|
|
|
|
/// A pair, matching a [Diagnostic] with it's equivalent
|
|
/// [protocol.AnalysisError].
|
|
typedef _DiagnosticAndAnalysisError = ({
|
|
Diagnostic diagnostic,
|
|
protocol.AnalysisError analysisError,
|
|
});
|
|
|
|
typedef _PluginState = ({
|
|
AnalysisContext analysisContext,
|
|
List<_DiagnosticAndAnalysisError> errors,
|
|
});
|
|
|
|
/// The server that communicates with the analysis server, passing requests and
|
|
/// responses between the analysis server and individual plugins.
|
|
class PluginServer {
|
|
/// The communication channel being used to communicate with the analysis
|
|
/// server.
|
|
late PluginCommunicationChannel _channel;
|
|
|
|
final OverlayResourceProvider _resourceProvider;
|
|
|
|
late final ByteStore _byteStore = MemoryCachingByteStore(
|
|
NullByteStore(),
|
|
1024 * 1024 * 256,
|
|
);
|
|
|
|
AnalysisContextCollectionImpl? _contextCollection;
|
|
|
|
String? _sdkPath;
|
|
|
|
/// Paths of priority files.
|
|
Set<String> _priorityPaths = {};
|
|
|
|
final List<Plugin> _plugins;
|
|
|
|
/// The recent state of analysis reults, to be cleared on file changes.
|
|
final _recentState = <String, _PluginState>{};
|
|
|
|
/// The map of files currently being analyzed, mapped to their active session.
|
|
final _filesBeingAnalyzed = <String, AnalysisSession>{};
|
|
|
|
/// The map of files currently being resolved, mapped to their active session.
|
|
final _filesBeingResolved = <String, AnalysisSession>{};
|
|
|
|
/// 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();
|
|
_updatePriorityFiles();
|
|
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: () {},
|
|
);
|
|
}
|
|
|
|
/// Waits until all background drivers in the context collection are idle.
|
|
@visibleForTesting
|
|
Future<void> waitForIdle() async {
|
|
if (_contextCollection case var contextCollection?) {
|
|
await Future.wait([
|
|
for (var analysisContext in contextCollection.contexts)
|
|
analysisContext.driver.scheduler.waitForIdle(),
|
|
]);
|
|
}
|
|
}
|
|
|
|
/// This method is invoked when a new instance of [AnalysisContextCollection]
|
|
/// is created, so the plugin can perform initial analysis of analyzed files.
|
|
Future<void> _analyzeAllFilesInContextCollection({
|
|
required AnalysisContextCollectionImpl contextCollection,
|
|
}) async {
|
|
await _forAnalysisContexts(contextCollection, (analysisContext) async {
|
|
final driver = analysisContext.driver;
|
|
var paths = analysisContext.contextRoot
|
|
.analyzedFiles()
|
|
// TODO(srawlins): Enable analysis on other files, even if only
|
|
// YAML files for analysis options and pubspec analysis and quick
|
|
// fixes.
|
|
.where((p) => file_paths.isDart(_resourceProvider.pathContext, p))
|
|
.toSet();
|
|
|
|
for (var path in paths) {
|
|
driver.addFile(path);
|
|
}
|
|
});
|
|
}
|
|
|
|
/// Computes and returns [protocol.AnalysisError]s for each of the parts in
|
|
/// the library at [libraryPath].
|
|
Future<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(
|
|
AnalysisContextCollectionImpl contextCollection,
|
|
Future<void> Function(DriverBasedAnalysisContext analysisContext) fn,
|
|
) async {
|
|
var nonPriorityAnalysisContexts = <DriverBasedAnalysisContext>[];
|
|
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:
|
|
await Future.wait(
|
|
_plugins.map((p) => p.shutDown()).whereType<Future<Object?>>(),
|
|
);
|
|
_channel.sendResponse(
|
|
protocol.PluginShutdownResult().toResponse(request.id, requestTime),
|
|
);
|
|
_channel.close();
|
|
return null;
|
|
|
|
case protocol.PLUGIN_REQUEST_VERSION_CHECK:
|
|
var params = protocol.PluginVersionCheckParams.fromRequest(request);
|
|
result = await handlePluginVersionCheck(params);
|
|
|
|
default:
|
|
// Anything else is unsupported.
|
|
result = null;
|
|
}
|
|
if (result == null) {
|
|
return Response(
|
|
request.id,
|
|
requestTime,
|
|
error: RequestErrorFactory.unknownRequest(request.method),
|
|
);
|
|
}
|
|
return result.toResponse(request.id, requestTime);
|
|
}
|
|
|
|
/// Handles an 'analysis.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,
|
|
drainStreams: false,
|
|
);
|
|
_contextCollection = contextCollection;
|
|
_updatePriorityFiles();
|
|
contextCollection.scheduler.events.listen((event) {
|
|
if (event is ResolvedUnitResult) {
|
|
_handleResolvedUnit(event);
|
|
} else if (event is AnalysisStatus) {
|
|
_handleAnalysisStatus(event);
|
|
} else if (event is ErrorsResult) {
|
|
_handleErrorsResult(event);
|
|
}
|
|
});
|
|
await _analyzeAllFilesInContextCollection(
|
|
contextCollection: contextCollection,
|
|
);
|
|
return protocol.AnalysisSetContextRootsResult();
|
|
}
|
|
|
|
void _handleAnalysisStatus(AnalysisStatus status) {
|
|
_channel.sendNotification(
|
|
protocol.PluginStatusParams(
|
|
analysis: protocol.AnalysisStatus(status.isWorking),
|
|
).toNotification(),
|
|
);
|
|
}
|
|
|
|
/// Handles an 'analysis.updateContent' request.
|
|
///
|
|
/// Throws a [RequestFailure] if the request could not be handled.
|
|
Future<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 {
|
|
oldContent = _resourceProvider.getFile(path).readAsStringSync();
|
|
} catch (_) {
|
|
// Leave `oldContent` empty.
|
|
}
|
|
|
|
// Prepare the new contents.
|
|
String? newContent;
|
|
if (overlay is protocol.AddContentOverlay) {
|
|
newContent = overlay.content;
|
|
} else if (overlay is protocol.ChangeContentOverlay) {
|
|
if (oldContent == null) {
|
|
// The server should only send a ChangeContentOverlay if there is
|
|
// already an existing overlay for the source.
|
|
throw RequestFailure(
|
|
RequestErrorFactory.invalidOverlayChangeNoContent(),
|
|
);
|
|
}
|
|
try {
|
|
newContent = protocol.SourceEdit.applySequence(
|
|
oldContent,
|
|
overlay.edits,
|
|
);
|
|
} on RangeError {
|
|
throw RequestFailure(
|
|
RequestErrorFactory.invalidOverlayChangeInvalidEdit(),
|
|
);
|
|
}
|
|
} else if (overlay is protocol.RemoveContentOverlay) {
|
|
newContent = null;
|
|
}
|
|
|
|
if (newContent != null) {
|
|
if (newContent != oldContent) {
|
|
_resourceProvider.setOverlay(
|
|
path,
|
|
content: newContent,
|
|
modificationStamp: _overlayModificationStamp++,
|
|
);
|
|
changedPaths.add(path);
|
|
}
|
|
} else {
|
|
if (_resourceProvider.hasOverlay(path)) {
|
|
_resourceProvider.removeOverlay(path);
|
|
changedPaths.add(path);
|
|
}
|
|
}
|
|
});
|
|
await _handleContentChanged(modifiedPaths: changedPaths.toList());
|
|
return protocol.AnalysisUpdateContentResult();
|
|
}
|
|
|
|
/// Handles an 'analysis.handleWatchEvents' request.
|
|
Future<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?) {
|
|
_filesBeingAnalyzed.clear();
|
|
_filesBeingResolved.clear();
|
|
_channel.sendNotification(
|
|
protocol.PluginStatusParams(
|
|
analysis: protocol.AnalysisStatus(true),
|
|
).toNotification(),
|
|
);
|
|
await _forAnalysisContexts(contextCollection, (analysisContext) async {
|
|
final driver = analysisContext.driver;
|
|
for (var path in modifiedPaths) {
|
|
driver.changeFile(path);
|
|
}
|
|
for (var path in removedPaths) {
|
|
driver.removeFile(path);
|
|
}
|
|
for (var path in addedPaths) {
|
|
driver.addFile(path);
|
|
}
|
|
});
|
|
for (var path in removedPaths) {
|
|
_channel.sendNotification(
|
|
protocol.AnalysisErrorsParams(path, []).toNotification(),
|
|
);
|
|
}
|
|
_channel.sendNotification(
|
|
protocol.PluginStatusParams(
|
|
analysis: protocol.AnalysisStatus(false),
|
|
).toNotification(),
|
|
);
|
|
}
|
|
}
|
|
|
|
void _handleErrorsResult(ErrorsResult event) {
|
|
var path = event.path;
|
|
var session = event.session;
|
|
var analysisContext = session.analysisContext;
|
|
if (analysisContext is DriverBasedAnalysisContext) {
|
|
if (analysisContext.contextRoot.isAnalyzed(path)) {
|
|
var activeSession = _filesBeingResolved[path];
|
|
if (activeSession != session) {
|
|
_filesBeingResolved[path] = session;
|
|
analysisContext.driver.getResolvedUnit(path);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
Future<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),
|
|
),
|
|
);
|
|
}
|
|
|
|
Future<void> _handleResolvedUnit(ResolvedUnitResult unitResult) async {
|
|
var analysisContext = unitResult.session.analysisContext;
|
|
if (!analysisContext.contextRoot.isAnalyzed(unitResult.path)) {
|
|
return;
|
|
}
|
|
if (unitResult.unit.declaredFragment !=
|
|
unitResult.libraryElement.firstFragment) {
|
|
return;
|
|
}
|
|
var session = unitResult.session;
|
|
var activeSession = _filesBeingAnalyzed[unitResult.path];
|
|
if (activeSession == session) {
|
|
return;
|
|
}
|
|
_filesBeingAnalyzed[unitResult.path] = session;
|
|
try {
|
|
var file = _resourceProvider.getFile(unitResult.path);
|
|
var analysisOptions = analysisContext.getAnalysisOptionsForFile(file);
|
|
var analysisErrorsByPath = await _computeAnalysisErrors(
|
|
analysisContext,
|
|
unitResult.path,
|
|
analysisOptions: analysisOptions as AnalysisOptionsImpl,
|
|
);
|
|
for (var MapEntry(key: path, value: analysisErrors)
|
|
in analysisErrorsByPath.entries) {
|
|
_channel.sendNotification(
|
|
protocol.AnalysisErrorsParams(path, analysisErrors).toNotification(),
|
|
);
|
|
}
|
|
} on InconsistentAnalysisException {
|
|
// State changed during resolution; a subsequent pass will handle it.
|
|
}
|
|
}
|
|
|
|
bool _isPriorityAnalysisContext(AnalysisContext analysisContext) =>
|
|
_priorityPaths.any(analysisContext.contextRoot.isAnalyzed);
|
|
|
|
void _print(String pluginName, String message) {
|
|
_channel.sendNotification(
|
|
protocol.PluginPrintParams(
|
|
protocol.PluginPrint(
|
|
pluginName,
|
|
message,
|
|
DateTime.now().millisecondsSinceEpoch,
|
|
),
|
|
).toNotification(),
|
|
);
|
|
}
|
|
|
|
void _updatePriorityFiles() {
|
|
if (_contextCollection case var contextCollection?) {
|
|
for (var analysisContext in contextCollection.contexts) {
|
|
analysisContext.driver.priorityFiles = _priorityPaths.toList();
|
|
}
|
|
}
|
|
}
|
|
|
|
/// The `onError` handler for use in [runZonedGuarded].
|
|
void _zoneOnErrorHandler(Object error, StackTrace stackTrace) {
|
|
_channel.sendNotification(
|
|
protocol.PluginErrorParams(
|
|
false /* isFatal */,
|
|
error.toString(),
|
|
stackTrace.toString(),
|
|
).toNotification(),
|
|
);
|
|
}
|
|
|
|
static protocol.Location _locationFor(
|
|
CompilationUnit unit,
|
|
String path,
|
|
Diagnostic diagnostic,
|
|
) {
|
|
var lineInfo = unit.lineInfo;
|
|
var startLocation = lineInfo.getLocation(diagnostic.offset);
|
|
var endLocation = lineInfo.getLocation(
|
|
diagnostic.offset + diagnostic.length,
|
|
);
|
|
return protocol.Location(
|
|
path,
|
|
diagnostic.offset,
|
|
diagnostic.length,
|
|
startLocation.lineNumber,
|
|
startLocation.columnNumber,
|
|
endLine: endLocation.lineNumber,
|
|
endColumn: endLocation.columnNumber,
|
|
);
|
|
}
|
|
}
|