blob: 9df1ebb8c16da6cc241624c3abf37a2f80f9fbad [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/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/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/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/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/dart/analysis/analysis_context_collection.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/analysis/session.dart';
import 'package:analyzer/src/dart/element/type_system.dart';
import 'package:analyzer/src/generated/engine.dart' show AnalysisOptionsImpl;
import 'package:analyzer/src/lint/lint_rule_timers.dart';
import 'package:analyzer/src/lint/linter.dart';
import 'package:analyzer/src/lint/linter_visitor.dart';
import 'package:analyzer_plugin/channel/channel.dart';
import 'package:analyzer_plugin/protocol/protocol.dart';
import 'package:analyzer_plugin/protocol/protocol_common.dart' as protocol;
import 'package:analyzer_plugin/protocol/protocol_constants.dart' as protocol;
import 'package:analyzer_plugin/protocol/protocol_generated.dart' as protocol;
import 'package:analyzer_plugin/src/protocol/protocol_internal.dart';
typedef _ErrorAndProtocolError = ({
AnalysisError error,
protocol.AnalysisError protocolError,
});
typedef _PluginState = ({
AnalysisContext analysisContext,
List<_ErrorAndProtocolError> errors,
});
/// The server that communicates with the analysis server, passing requests and
/// responses between the analysis server and individual plugins.
class PluginServer {
/// The communication channel being used to communicate with the analysis
/// server.
late PluginCommunicationChannel _channel;
final OverlayResourceProvider _resourceProvider;
late final ByteStore _byteStore =
MemoryCachingByteStore(NullByteStore(), 1024 * 1024 * 256);
AnalysisContextCollectionImpl? _contextCollection;
String? _sdkPath;
final List<Plugin> _plugins;
final _registry = PluginRegistryImpl();
/// The recent state of analysis reults, to be cleared on file changes.
final _recentState = <String, _PluginState>{};
PluginServer({
required ResourceProvider resourceProvider,
required List<Plugin> plugins,
}) : _resourceProvider = OverlayResourceProvider(resourceProvider),
_plugins = plugins {
for (var plugin in plugins) {
plugin.register(_registry);
}
}
/// 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),
);
_contextCollection = contextCollection;
await _analyzeAllFilesInContextCollection(
contextCollection: contextCollection);
return protocol.AnalysisSetContextRootsResult();
}
/// 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 result = await analysisContext.currentSession.getResolvedUnit(path);
if (result is! ResolvedUnitResult) {
return protocol.EditGetFixesResult(const []);
}
var lintAtOffset = errors.where((error) => error.error.offset == offset);
if (lintAtOffset.isEmpty) return protocol.EditGetFixesResult(const []);
var errorFixesList = <protocol.AnalysisErrorFixes>[];
var workspace = DartChangeWorkspace([analysisContext.currentSession]);
for (var (:error, :protocolError) in lintAtOffset) {
var context = DartFixContext(
// TODO(srawlins): Use a real instrumentation service. Other
// implementations get InstrumentationService from AnalysisServer.
instrumentationService: InstrumentationService.NULL_SERVICE,
workspace: workspace,
resolvedResult: result,
error: error,
);
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(protocolError);
errorFixesList.add(errorFixes);
for (var fix in fixes) {
errorFixes.fixes.add(protocol.PrioritizedSourceChange(1, fix.change));
}
}
}
return protocol.EditGetFixesResult(errorFixesList);
}
/// Handles a 'plugin.versionCheck' request.
Future<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.
onError: () {},
// 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.
///
/// By default analyzes every [AnalysisContext] with [_analyzeFiles].
Future<void> _analyzeAllFilesInContextCollection({
required AnalysisContextCollection contextCollection,
}) async {
await _forAnalysisContexts(contextCollection, (analysisContext) async {
var paths = analysisContext.contextRoot.analyzedFiles().toList();
await _analyzeFiles(
analysisContext: analysisContext,
paths: paths,
);
});
}
/// Analyzes the files at the given [paths].
Future<void> _analyzeFiles({
required AnalysisContext analysisContext,
required List<String> paths,
}) async {
// TODO(srawlins): Implement "priority files" and analyze them first.
// TODO(srawlins): Analyze libraries instead of files, for efficiency.
for (var path in paths.toSet()) {
var file = _resourceProvider.getFile(path);
var analysisOptions = analysisContext.getAnalysisOptionsForFile(file);
var lints = await _computeLints(
analysisContext,
path,
analysisOptions: analysisOptions as AnalysisOptionsImpl,
);
_channel.sendNotification(
protocol.AnalysisErrorsParams(path, lints).toNotification());
}
}
Future<List<protocol.AnalysisError>> _computeLints(
AnalysisContext analysisContext,
String path, {
required AnalysisOptionsImpl analysisOptions,
}) async {
var libraryResult =
await analysisContext.currentSession.getResolvedLibrary(path);
if (libraryResult is! ResolvedLibraryResult) {
return [];
}
var result = await analysisContext.currentSession.getResolvedUnit(path);
if (result is! ResolvedUnitResult) {
return [];
}
var listener = RecordingErrorListener();
var errorReporter = ErrorReporter(listener, result.libraryElement.source);
var currentUnit = LintRuleUnitContext(
file: result.file,
content: result.content,
errorReporter: errorReporter,
unit: result.unit,
);
var allUnits = [
for (var unit in libraryResult.units)
LintRuleUnitContext(
file: unit.file,
content: unit.content,
errorReporter: errorReporter,
unit: unit.unit,
),
];
var enableTiming = analysisOptions.enableTiming;
var nodeRegistry = NodeLintRegistry(enableTiming);
var context = LinterContextWithResolvedResults(
allUnits,
currentUnit,
libraryResult.element.typeProvider,
libraryResult.element.typeSystem as TypeSystemImpl,
(analysisContext.currentSession as AnalysisSessionImpl)
.inheritanceManager,
// TODO(srawlins): Support 'package' parameter.
null,
);
// TODO(srawlins): Distinguish between registered rules and enabled rules.
for (var rule in _registry.registeredRules) {
rule.reporter = errorReporter;
var timer = enableTiming ? lintRuleTimers.getTimer(rule) : null;
timer?.start();
rule.registerNodeProcessors(nodeRegistry, context);
timer?.stop();
}
currentUnit.unit.accept(LinterVisitor(nodeRegistry));
// The list of the `AnalysisError`s and their associated
// `protocol.AnalysisError`s.
var errorsAndProtocolErrors = [
for (var e in listener.errors)
(
error: e,
protocolError: protocol.AnalysisError(
protocol.AnalysisErrorSeverity.INFO,
protocol.AnalysisErrorType.STATIC_WARNING,
_locationFor(currentUnit.unit, path, e),
e.message,
e.errorCode.name,
correction: e.correction,
// TODO(srawlins): Use a valid value here.
hasFix: true,
)
),
];
_recentState[path] = (
analysisContext: analysisContext,
errors: [...errorsAndProtocolErrors],
);
return errorsAndProtocolErrors.map((e) => e.protocolError).toList();
}
/// Invokes [fn] for all analysis contexts.
Future<void> _forAnalysisContexts(
AnalysisContextCollection contextCollection,
Future<void> Function(AnalysisContext analysisContext) fn,
) async {
for (var analysisContext in contextCollection.contexts) {
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_GET_NAVIGATION:
case protocol.ANALYSIS_REQUEST_HANDLE_WATCH_EVENTS:
result = null;
case protocol.ANALYSIS_REQUEST_SET_CONTEXT_ROOTS:
var params =
protocol.AnalysisSetContextRootsParams.fromRequest(request);
result = await handleAnalysisSetContextRoots(params);
case protocol.ANALYSIS_REQUEST_SET_PRIORITY_FILES:
case protocol.ANALYSIS_REQUEST_SET_SUBSCRIPTIONS:
case protocol.ANALYSIS_REQUEST_UPDATE_CONTENT:
case protocol.COMPLETION_REQUEST_GET_SUGGESTIONS:
case protocol.EDIT_REQUEST_GET_ASSISTS:
case protocol.EDIT_REQUEST_GET_AVAILABLE_REFACTORINGS:
result = null;
case protocol.EDIT_REQUEST_GET_FIXES:
var params = protocol.EditGetFixesParams.fromRequest(request);
result = await handleEditGetFixes(params);
case protocol.EDIT_REQUEST_GET_REFACTORING:
result = null;
case protocol.PLUGIN_REQUEST_SHUTDOWN:
_channel.sendResponse(protocol.PluginShutdownResult()
.toResponse(request.id, requestTime));
_channel.close();
return null;
case protocol.PLUGIN_REQUEST_VERSION_CHECK:
var params = protocol.PluginVersionCheckParams.fromRequest(request);
result = await handlePluginVersionCheck(params);
}
if (result == null) {
return Response(request.id, requestTime,
error: RequestErrorFactory.unknownRequest(request.method));
}
return result.toResponse(request.id, requestTime);
}
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());
},
);
}
static protocol.Location _locationFor(
CompilationUnit unit, String path, AnalysisError error) {
var lineInfo = unit.lineInfo;
var startLocation = lineInfo.getLocation(error.offset);
var endLocation = lineInfo.getLocation(error.offset + error.length);
return protocol.Location(
path,
error.offset,
error.length,
startLocation.lineNumber,
startLocation.columnNumber,
endLine: endLocation.lineNumber,
endColumn: endLocation.columnNumber,
);
}
}