blob: bdd4419b060deb69a66a4c1a23d5d56250e4996e [file] [log] [blame]
// Copyright (c) 2025, 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 "dart:convert";
import "dart:io";
import "dart:typed_data";
Future<(Object, StackTrace)?> compileWithAnalyzer(AnalyzerHelper analyzerHelper,
String program, Uri fileUri, int version) async {
try {
ZoneSpecification specification =
new ZoneSpecification(print: (_1, _2, _3, String line) {
// Swallow!
});
await runZoned(() async {
Stopwatch stopwatch = new Stopwatch()..start();
await analyzerHelper.changeFileContent(fileUri, program, version);
print("Analyzer compile took ${stopwatch.elapsedMilliseconds} ms.");
}, zoneSpecification: specification);
return null;
} catch (e, st) {
print("Analyzer crashed on input.");
return (e, st);
}
}
Future<void> main() async {
AnalyzerHelper analyzerHelper = new AnalyzerHelper();
Directory root = Directory.systemTemp.createTempSync("fuzzer");
File f = new File.fromUri(root.uri.resolve("testfile.dart"));
f.writeAsStringSync("");
int version = 1;
await analyzerHelper.setup(root.uri);
await analyzerHelper.changeFileContent(
f.uri, "void main() { foo; }", version++);
await analyzerHelper.changeFileContent(f.uri, "void main() { }", version++);
await analyzerHelper.changeFileContent(
f.uri, "void main() { foo; } get foo => 42; ", version++);
analyzerHelper.shutdown();
root.deleteSync(recursive: true);
}
class AnalyzerHelper {
static const bool debug = false;
static const int _printSizeCap = 300;
late final Process _p;
late final Timer _periodicTimer;
bool _processShutDown = false;
bool _processExited = false;
Future<void> setup(Uri rootUri) async {
_checkCorrectDart();
Uri sdkUri = Uri.base
.resolveUri(Uri.file(Platform.resolvedExecutable))
.resolve("..");
if (!Directory.fromUri(rootUri).existsSync()) {
throw "Directory $rootUri doesn't exist. "
"Specify existing directory with --root=";
}
if (!Directory.fromUri(sdkUri).existsSync()) {
throw "Directory $sdkUri doesn't exist. "
"Specify existing directory with --sdk=";
}
if (!debug) {
_p = await Process.start(Platform.resolvedExecutable, [
"language-server",
"--lsp",
]);
} else {
_p = await Process.start(Platform.resolvedExecutable, [
"--enable-vm-service",
"--profiler",
"pkg/analysis_server/bin/server.dart",
"--lsp",
"--port=9101"
]);
}
// ignore: unawaited_futures
_p.exitCode.then((_) {
_processExited = true;
if (!_processShutDown) {
shutdown();
// Make sure we don't wait forever.
_setAnalyzing(false);
}
});
_p.stdout.listen(_listenToStdout);
_periodicTimer =
Timer.periodic(const Duration(seconds: 1), _checkLongRunningRequests);
await _initialize(sdkUri, rootUri, []);
}
void shutdown() {
_processShutDown = true;
_periodicTimer.cancel();
_p.kill();
}
Future<void> _initialize(
Uri sdkUri, Uri rootUri, List<Uri> additionalWorkspaceUris) async {
OutstandingRequest? request = await _send(
Messages.initMessage(pid, rootUri, additionalWorkspaceUris));
await request?.completer.future;
_resetAnalyzingBool();
await _send(Messages.initNotification);
await _send(Messages.initMore(sdkUri));
await _waitForAnalysisToComplete();
}
Set<Uri> _openFiles = {};
Future<void> changeFileContent(
Uri file, String newContent, int fileVersion) async {
_resetAnalyzingBool();
if (_openFiles.add(file)) {
await _send(Messages.openFile(file, fileVersion, newContent));
} else {
await _send(Messages.changeFileContent(file, newContent, fileVersion));
}
await _waitForAnalysisToComplete();
}
void _checkCorrectDart() {
Uri exe = Uri.base.resolveUri(Uri.file(Platform.resolvedExecutable));
Uri librariesDart =
exe.resolve("../lib/_internal/sdk_library_metadata/lib/libraries.dart");
if (!File.fromUri(librariesDart).existsSync()) {
throw "Execute with a dart that has "
"'../lib/_internal/sdk_library_metadata/lib/libraries.dart' "
"available (e.g. out/ReleaseX64/dart-sdk/bin/dart)";
}
}
void _checkLongRunningRequests(timer) {
bool reportedSomething = false;
for (MapEntry<int, OutstandingRequest> waitingFor
in _outstandingRequestsWithId.entries) {
if (waitingFor.value.stopwatch.elapsed > const Duration(seconds: 1)) {
if (!reportedSomething) {
print("----");
reportedSomething = true;
}
print("==> Has been waiting for ${waitingFor.key} for "
"${waitingFor.value.stopwatch.elapsed}");
}
}
if (reportedSomething) {
print("----");
}
}
int? _headerContentLength;
bool? _currentlyAnalyzing;
Completer<void> _analyzingCompleter = Completer();
final _buffer = <int>[];
/// There's something weird about getting (several) id 3's that wasn't
/// requested...
int _largestIdSeen = 3;
RegExp _newLineRegExp = RegExp("\r?\n");
Map<int, OutstandingRequest> _outstandingRequestsWithId = {};
bool _printedVmServiceStuff = false;
int _verbosity = 0;
void _setAnalyzing(bool b) {
_analyzingCompleter.complete(b);
_currentlyAnalyzing = b;
_analyzingCompleter = Completer<bool>();
}
void _resetAnalyzingBool() {
_currentlyAnalyzing = null;
}
Future<void> _waitForAnalysisToComplete() async {
// Wait until it's done analyzing.
if (_currentlyAnalyzing == null) {
await _analyzingCompleter.future;
}
Stopwatch stopwatch = Stopwatch()..start();
while (_currentlyAnalyzing == true) {
await _analyzingCompleter.future;
}
print("isAnalyzing is now done after ${stopwatch.elapsed}");
if (_processExited) {
// TODO(jensj): We should extract the correct stacktrace somehow.
throw "Process exited.";
}
}
void _listenToStdout(List<int> event) {
// General idea taken from
// pkg/analysis_server/lib/src/lsp/lsp_packet_transformer.dart
for (int element in event) {
_buffer.add(element);
if (_verbosity > 3 &&
_buffer.length >= 1000 &&
_buffer.length % 1000 == 0) {
print("DEBUG MESSAGE: Stdout buffer with length "
"${_buffer.length} so far: "
"${utf8.decode(_buffer)}");
}
if (_headerContentLength == null && _endsWithCrLfCrLf()) {
String headerRaw = utf8.decode(_buffer);
_buffer.clear();
// Use a regex that makes the "\r" optional to handle "The Dart VM
// service is listening on [..." message - at least on linux - being \n
// terminated which would otherwise mean that we'd be stuck because no
// message would start with "Content-Length:".
List<String> headers = headerRaw.split(_newLineRegExp);
for (String header in headers) {
if (!_printedVmServiceStuff &&
header.startsWith("The Dart VM service")) {
print("\n\n$header\n\n");
_printedVmServiceStuff = true;
}
if (header.startsWith("Content-Length:")) {
String contentLength =
header.substring("Content-Length:".length).trim();
_headerContentLength = int.parse(contentLength);
break;
}
}
} else if (_headerContentLength != null &&
_buffer.length == _headerContentLength!) {
String messageString = utf8.decode(_buffer);
_buffer.clear();
_headerContentLength = null;
Map<String, dynamic> message =
json.decode(messageString) as Map<String, dynamic>;
// {"jsonrpc":"2.0","method":"$/analyzerStatus","params":{"isAnalyzing":false}}
dynamic method = message["method"];
if (method == r"$/analyzerStatus") {
dynamic params = message["params"];
if (params is Map) {
dynamic isAnalyzing = params["isAnalyzing"];
if (isAnalyzing is bool) {
_setAnalyzing(isAnalyzing);
if (_verbosity > 0) {
print("Got analyzerStatus isAnalyzing = $isAnalyzing");
}
}
}
}
dynamic possibleId = message["id"];
if (possibleId is int) {
if (possibleId > _largestIdSeen) {
_largestIdSeen = possibleId;
}
if (_verbosity > 0) {
if (messageString.length > _printSizeCap) {
print("Got message "
"${messageString.substring(0, _printSizeCap)}...");
} else {
print("Got message $messageString");
}
}
OutstandingRequest? outstandingRequest =
_outstandingRequestsWithId.remove(possibleId);
if (outstandingRequest != null) {
outstandingRequest.stopwatch.stop();
outstandingRequest.completer.complete(message);
if (_verbosity > 2) {
print(" => Got response for $possibleId in "
"${outstandingRequest.stopwatch.elapsed}");
}
}
} else if (_verbosity > 1) {
if (messageString.length > _printSizeCap) {
print("Got message "
"${messageString.substring(0, _printSizeCap)}...");
} else {
print("Got message $messageString");
}
}
}
}
}
Future<OutstandingRequest?> _send(Map<String, dynamic> json) async {
if (_processExited) throw "Process is gone.";
// Mostly copied from
// pkg/analysis_server/lib/src/lsp/channel/lsp_byte_stream_channel.dart
String jsonEncodedBody = jsonEncode(json);
Uint8List utf8EncodedBody = utf8.encode(jsonEncodedBody);
String header = "Content-Length: ${utf8EncodedBody.length}\r\n"
"Content-Type: application/vscode-jsonrpc; charset=utf-8\r\n\r\n";
Uint8List asciiEncodedHeader = ascii.encode(header);
OutstandingRequest? result;
dynamic possibleId = json["id"];
if (possibleId is int) {
if (possibleId > _largestIdSeen) {
_largestIdSeen = possibleId;
}
result = OutstandingRequest();
_outstandingRequestsWithId[possibleId] = result;
if (_verbosity > 2) {
print("Sending message with id $possibleId");
}
}
// Header is always ascii, body is always utf8!
_p.stdin.add(asciiEncodedHeader);
_p.stdin.add(utf8EncodedBody);
await _p.stdin.flush();
if (_verbosity > 2) {
print("\n\nMessage sent...\n\n");
print("jsonEncodedBody: $jsonEncodedBody");
}
return result;
}
/// Copied from pkg/analysis_server/lib/src/lsp/lsp_packet_transformer.dart.
bool _endsWithCrLfCrLf() {
int l = _buffer.length;
return l > 4 &&
_buffer[l - 1] == 10 &&
_buffer[l - 2] == 13 &&
_buffer[l - 3] == 10 &&
_buffer[l - 4] == 13;
}
}
class Location {
final Uri uri;
final int line;
final int column;
Location(this.uri, this.line, this.column);
@override
String toString() => "Location[$uri:$line:$column]";
}
// Messages taken from what VSCode sent.
class Messages {
static Map<String, dynamic> initNotification = {
"jsonrpc": "2.0",
"method": "initialized",
"params": {}
};
static Map<String, dynamic> gotoDef(int id, Location location) {
return {
"jsonrpc": "2.0",
"id": id,
"method": "textDocument/definition",
"params": {
"textDocument": {"uri": "${location.uri}"},
"position": {"line": location.line, "character": location.column}
}
};
}
static Map<String, dynamic> implementation(int id, Location location) {
return {
"jsonrpc": "2.0",
"id": id,
"method": "textDocument/implementation",
"params": {
"textDocument": {"uri": "${location.uri}"},
"position": {"line": location.line, "character": location.column}
}
};
}
static Map<String, dynamic> initMessage(
int processId, Uri rootUri, List<Uri> additionalWorkspaceUris) {
String rootPath = rootUri.toFilePath();
String name = rootUri.pathSegments.last;
if (name.isEmpty) {
name = rootUri.pathSegments[rootUri.pathSegments.length - 2];
}
return {
"id": 0,
"jsonrpc": "2.0",
"method": "initialize",
"params": {
"processId": processId,
"clientInfo": {"name": "lspTestScript", "version": "0.0.1"},
"locale": "en",
"rootPath": rootPath,
"rootUri": "$rootUri",
"capabilities": {},
"initializationOptions": {},
"workspaceFolders": [
{"uri": "$rootUri", "name": name},
...additionalWorkspaceUris.map((uri) {
String name = uri.pathSegments.last;
if (name.isEmpty) {
name = uri.pathSegments[uri.pathSegments.length - 2];
}
return {
"uri": "$uri",
"name": name,
};
})
]
}
};
}
static Map<String, dynamic> initMore(Uri sdkUri) {
String sdkPath = sdkUri.toFilePath();
return {
// "id": 1,
"jsonrpc": "2.0",
"result": [
{
"useLsp": true,
"sdkPath": sdkPath,
"allowAnalytics": false,
}
]
};
}
static Map<String, dynamic> references(int id, Location location) {
return {
"jsonrpc": "2.0",
"id": id,
"method": "textDocument/references",
"params": {
"textDocument": {"uri": "${location.uri}"},
"position": {"line": location.line, "character": location.column},
"context": {"includeDeclaration": true}
}
};
}
static Map<String, dynamic> openFile(
Uri file, int fileVersion, String content) {
return {
"jsonrpc": "2.0",
"method": "textDocument/didOpen",
"params": {
"textDocument": {
"uri": "$file",
"languageId": "dart",
"version": fileVersion,
"text": content
},
}
};
}
static Map<String, dynamic> changeFileContent(
Uri file, String newContent, int fileVersion) {
return {
"jsonrpc": "2.0",
"method": "textDocument/didChange",
"params": {
"textDocument": {"uri": "$file", "version": fileVersion},
"contentChanges": [
{"text": newContent}
]
}
};
}
}
class OutstandingRequest {
final Stopwatch stopwatch = Stopwatch();
final Completer<Map<String, dynamic>> completer = Completer();
OutstandingRequest() {
stopwatch.start();
}
}