Analyzer plugins run in a processes started and managed by the analysis server. This is useful when shipping plugins to users, but unfortunate for us as we can't attach a debugger to the plugin process.
Luckily, we can use a little trick to help us here. For debugging purposes, we instead use the following model. The actual plugin will start a websocket server. When the proxy loaded by the analysis server gets started, it will connect to that server and relay all operations. In fact, almost no code changes are necessary to the plugin.
First, let's write the proxy that can be loaded by the analysis server.
# tool/analyzer_plugin/pubspec.yaml
name: analyzer_load_my_plugin
version: 1.0.0
dependencies:
your_real_plugin: ^2.0.0 # remove if you don't have a published analyzer plugin yet, or use an (absolute!) path dependency
web_socket_channel: ^1.0.15// tool/analyzer_plugin/bin/plugin.dart
import 'dart:convert';
import 'dart:isolate';
import 'package:your_real_plugin/plugin.dart' as plugin;
import 'package:web_socket_channel/io.dart';
const useDebuggingVariant = true; // set to false before publishing
void main(List<String> args, SendPort sendPort) {
if (useDebuggingVariant) {
_PluginProxy(sendPort).start();
} else {
// start the regular plugin like you normally would
plugin.start(args, sendPort);
}
}
class _PluginProxy {
final SendPort sendToAnalysisServer;
ReceivePort _receive;
IOWebSocketChannel _channel;
_PluginProxy(this.sendToAnalysisServer);
Future<void> start() async {
_channel = IOWebSocketChannel.connect('ws://localhost:9999');
_receive = ReceivePort();
sendToAnalysisServer.send(_receive.sendPort);
_receive.listen((data) {
// the server will send messages as maps, convert to json
_channel.sink.add(json.encode(data));
});
_channel.stream.listen((data) {
sendToAnalysisServer.send(json.decode(data as String));
});
}
}Next, we need to adapt the plugin so that it opens the websocket server. For that, we'll write a drop-in replacement for ServerPluginStarter:
class _WebSocketPluginServer implements PluginCommunicationChannel {
final dynamic address;
final int port;
HttpServer server;
WebSocket _currentClient;
final StreamController<WebSocket> _clientStream =
StreamController.broadcast();
_WebSocketPluginServer({dynamic address, this.port = 9999})
: address = address ?? InternetAddress.loopbackIPv4 {
_init();
}
Future<void> _init() async {
server = await HttpServer.bind(address, port);
print('listening on $address at port $port');
server.transform(WebSocketTransformer()).listen(_handleClientAdded);
}
void _handleClientAdded(WebSocket socket) {
if (_currentClient != null) {
print('ignoring connection attempt because an active client already '
'exists');
socket.close();
} else {
print('client connected');
_currentClient = socket;
_clientStream.add(_currentClient);
_currentClient.done.then((_) {
print('client disconnected');
_currentClient = null;
_clientStream.add(null);
});
}
}
@override
void close() {
server?.close(force: true);
}
@override
void listen(void Function(Request request) onRequest,
{Function onError, void Function() onDone}) {
final stream = _clientStream.stream;
// wait until we're connected
stream.firstWhere((socket) => socket != null).then((_) {
_currentClient.listen((data) {
print('I: $data');
onRequest(Request.fromJson(
json.decode(data as String) as Map<String, dynamic>));
});
});
stream.firstWhere((socket) => socket == null).then((_) => onDone());
}
@override
void sendNotification(Notification notification) {
print('N: ${notification.toJson()}');
_currentClient?.add(json.encode(notification.toJson()));
}
@override
void sendResponse(Response response) {
print('O: ${response.toJson()}');
_currentClient?.add(json.encode(response.toJson()));
}
}You can then write a new Dart file where the main method looks like
void main() {
final plugin = YourSubClassOfServerPlugin();
plugin.start(_WebSocketPluginServer());
}To debug the plugin, all you need to do is
-
Run the file with the
mainmethod from an IDE. You have full control over that process, so you can debug it -
Start another IDE with code that uses the plugin. It will start an analysis server that loads the proxy, which will then connect to your server.
While debugging only requires some additions and almost no changes to existing code, proper testing for plugins is much harder. Personally, I tried to have strong layers of abstraction in my plugin
-
My business logic, which implements analysis services when given an element or ast model from the analyzer
-
a lower-level interface wrapping analyzer apis
-
one of them uses a raw
AnalysisDriver, created via theanalyzer_plugin -
one of them uses the build system
-
one of them uses
build_test, this is the one I use for testing
-
I have both an analyzer plugin and a source_gen builder in my Dart package, and I wanted to go for maximum code reuse between them. For inspiration, you can see my interface, the build-based implementation or the one using an analysis driver (see also driver.dart in the same dir). I've written a small wrapper around build_test so that I can declare tests like this.
Technically it should be possible to do all of this with a raw AnalysisDriver, but I didn't have the motivation to do that yet.
Why can't I use relative paths in the plugin's pubspec? The analysis server will copy the tools/analyzer_plugin directory into .dartServer/.pluginManager/someHash before running it, so we can't use relative paths in our pubspec.
Nothing ever connects to the server! The first plugin start can take up to a minute, after that it's still around 10 seconds before the analysis server loads the plugin. You can view a set of loaded plugins and what's wrong with them in the analyzer diagnostics website (set dart.analyzerDiagnosticsPort in the VS Code extension). If nothing helps, detailed logs can be written to a file with dart.analyzerInstrumentationLogFile.



