[vm_service] Use empty lists instead of null for non-optional fields when parsing JSON

Change 64060a8ddf accidentally lost `?? []` for non-optional fields when parsing JSON. This change restores that (in `generate_dart_common.dart`), along with some minor tweaks to get the codegen to work on Windows.

For reasons I don't understand, the generated files were formatted differently to how the formatter formats them on my machine today, which unfortunately makes the diff larger than the intended change.

Change-Id: Ibadaa3ec4c6af4e636c1e5ffd2c7e792bc1e8a14
Reviewed-on: https://dart-review.googlesource.com/c/sdk/+/497600
Reviewed-by: Ben Konyi <bkonyi@google.com>
Commit-Queue: Ben Konyi <bkonyi@google.com>
Reviewed-by: Samuel Rawlins <srawlins@google.com>
This commit is contained in:
Danny Tuppeny
2026-04-24 08:03:51 -07:00
committed by dart-scoped@luci-project-accounts.iam.gserviceaccount.com
parent b715374b28
commit 4d07f9ace4
8 changed files with 2150 additions and 2138 deletions
+3
View File
@@ -1,6 +1,9 @@
## 15.2.0
- Update to version `4.22` of the spec.
- Deprecate `Message` type and `messages` field of `Stack` type.
- Fix an issue introduced in 15.1.0 that could cause `parse()` methods (such as
`VM.parse()`) to leave collections as `null` instead of empty lists when they
were not present in the JSON.
## 15.1.0
- Update to version `4.21` of the spec.
File diff suppressed because it is too large Load Diff
+1 -1
View File
@@ -1,5 +1,5 @@
name: vm_service
version: 15.2.0
version: 15.2.0-wip
description: >-
A library to communicate with a service implementing the Dart VM
service protocol.
@@ -0,0 +1,19 @@
// Copyright (c) 2026, 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 'package:test/test.dart';
import 'package:vm_service/vm_service.dart';
// Regression test for failure to roll into Flutter found in
// https://github.com/flutter/flutter/pull/185274#discussion_r3116379311
void main() {
test('VM.parse() never results in null lists', () {
final vmService = VM.parse({})!;
expect(vmService.systemIsolateGroups, allOf(isNotNull, isEmpty));
expect(vmService.systemIsolates, allOf(isNotNull, isEmpty));
expect(vmService.isolateGroups, allOf(isNotNull, isEmpty));
expect(vmService.isolates, allOf(isNotNull, isEmpty));
});
}
+3 -1
View File
@@ -34,6 +34,8 @@ class Tokenizer {
Token? _head;
Token? _last;
final _newLineRegExp = RegExp('\r?\n');
Tokenizer(this.text);
Token? tokenize() {
@@ -45,7 +47,7 @@ class Tokenizer {
if (whitespace.contains(c)) {
// skip
} else if (c == '/' && _peek(i) == '/') {
int index = text.indexOf('\n', i);
int index = text.indexOf(_newLineRegExp, i);
if (index == -1) index = text.length;
_emit(text.substring(i, index));
i = index;
@@ -754,7 +754,7 @@ class Type extends Member {
} else {
gen.writeln(
'${field.generatableName} = _createServiceObjectListOrNull'
'<${fieldType.listTypeArg}>($ref, $typesList)');
'<${fieldType.listTypeArg}>($ref, $typesList) ?? []');
}
}
}
+10 -5
View File
@@ -27,7 +27,7 @@ Future<void> main(List<String> args) async {
);
final document = Document();
final buf = StringBuffer(file.readAsStringSync());
final nodes = document.parseLines(buf.toString().split('\n'));
final nodes = document.parseLines(buf.toString().split(RegExp('\r?\n')));
print('Parsed ${file.path}.');
print('Service protocol version ${ApiParseUtil.parseVersionString(nodes)}.');
@@ -111,7 +111,8 @@ Future<String> _generateDartCommon({
}
Future<void> _runDartFormat(String outDirPath) async {
ProcessResult result = Process.runSync('dart', ['format', outDirPath]);
ProcessResult result =
Process.runSync(Platform.resolvedExecutable, ['format', outDirPath]);
if (result.exitCode != 0) {
print('dart format: ${result.stdout}\n${result.stderr}');
throw result.exitCode;
@@ -122,10 +123,13 @@ Future<void> _generateJava(String codeGeneratorDir, List<Node> nodes) async {
var srcDirPath = normalize(join(codeGeneratorDir, '..', 'java', 'src'));
var generator = java.JavaGenerator(srcDirPath);
// We might be on Windows, but we always write paths with forward slashes.
final scriptPath = Platform.script.toFilePath();
final kSdk = '/sdk/';
final scriptLocation =
scriptPath.substring(scriptPath.indexOf(kSdk) + kSdk.length);
final kSdk = [Platform.pathSeparator, 'sdk', Platform.pathSeparator].join();
final scriptLocation = scriptPath
.substring(scriptPath.indexOf(kSdk) + kSdk.length)
.replaceAll(Platform.pathSeparator, '/');
java.api = java.Api(scriptLocation);
java.api.parse(nodes);
java.api.generate(generator);
@@ -135,6 +139,7 @@ Future<void> _generateJava(String codeGeneratorDir, List<Node> nodes) async {
// directory are).
List<String> generatedPaths = generator.allWrittenFiles
.map((path) => relative(path, from: 'java'))
.map((path) => path.replaceAll(Platform.pathSeparator, '/'))
.toList();
generatedPaths.sort();
File gitignoreFile = File(join(codeGeneratorDir, '..', 'java', '.gitignore'));
@@ -28,11 +28,8 @@ abstract interface class VmServiceInterface {
Stream<Event> onEvent(String streamId);
/// Handler for calling extra service extensions.
Future<Response> callServiceExtension(
String method, {
String? isolateId,
Map<String, dynamic>? args,
});
Future<Response> callServiceExtension(String method,
{String? isolateId, Map<String, dynamic>? args});
/// Invoked by the Dart Development Service (DDS) immediately after it
/// connects.
@@ -363,11 +360,8 @@ abstract interface class VmServiceInterface {
///
/// This method will throw a [SentinelException] in the case a [Sentinel] is
/// returned.
Future<AllocationProfile> getAllocationProfile(
String isolateId, {
bool? reset,
bool? gc,
});
Future<AllocationProfile> getAllocationProfile(String isolateId,
{bool? reset, bool? gc});
/// The `getAllocationTraces` RPC allows for the retrieval of allocation
/// traces for objects of a specific set of types (see
@@ -427,10 +421,7 @@ abstract interface class VmServiceInterface {
/// This method will throw a [SentinelException] in the case a [Sentinel] is
/// returned.
Future<CpuSamples> getCpuSamples(
String isolateId,
int timeOriginMicros,
int timeExtentMicros,
);
String isolateId, int timeOriginMicros, int timeExtentMicros);
/// The `getFlagList` RPC returns a list of all command line flags in the VM
/// along with their current values.
@@ -706,11 +697,8 @@ abstract interface class VmServiceInterface {
///
/// This method will throw a [SentinelException] in the case a [Sentinel] is
/// returned.
Future<PerfettoCpuSamples> getPerfettoCpuSamples(
String isolateId, {
int? timeOriginMicros,
int? timeExtentMicros,
});
Future<PerfettoCpuSamples> getPerfettoCpuSamples(String isolateId,
{int? timeOriginMicros, int? timeExtentMicros});
/// The `getPerfettoVMTimeline` RPC is used to retrieve an object which
/// contains a VM timeline trace represented in Perfetto's proto format. See
@@ -743,10 +731,8 @@ abstract interface class VmServiceInterface {
/// or Perfettofile, an [RPCError] with error code `114`, `invalid timeline
/// request`, will be returned as timeline events are written directly to a
/// file, and thus cannot be retrieved through the VM Service, in these modes.
Future<PerfettoTimeline> getPerfettoVMTimeline({
int? timeOriginMicros,
int? timeExtentMicros,
});
Future<PerfettoTimeline> getPerfettoVMTimeline(
{int? timeOriginMicros, int? timeExtentMicros});
/// The `getPorts` RPC is used to retrieve the list of `ReceivePort` instances
/// for a given isolate.
@@ -966,10 +952,8 @@ abstract interface class VmServiceInterface {
/// Perfettofile, an [RPCError] with error code `114`, `invalid timeline
/// request`, will be returned as timeline events are written directly to a
/// file, and thus cannot be retrieved through the VM Service, in these modes.
Future<Timeline> getVMTimeline({
int? timeOriginMicros,
int? timeExtentMicros,
});
Future<Timeline> getVMTimeline(
{int? timeOriginMicros, int? timeExtentMicros});
/// The `getVMTimelineFlags` RPC returns information about the current VM
/// timeline configuration.
@@ -1032,11 +1016,8 @@ abstract interface class VmServiceInterface {
/// of relative paths, but this is not guaranteed.
///
/// See [UriList].
Future<UriList> lookupResolvedPackageUris(
String isolateId,
List<String> uris, {
bool? local,
});
Future<UriList> lookupResolvedPackageUris(String isolateId, List<String> uris,
{bool? local});
/// The `lookupPackageUris` RPC is used to convert a list of URIs to their
/// unresolved paths. For example, URIs passed to this RPC are mapped in the
@@ -1149,11 +1130,8 @@ abstract interface class VmServiceInterface {
///
/// This method will throw a [SentinelException] in the case a [Sentinel] is
/// returned.
Future<Success> resume(
String isolateId, {
/*StepOption*/ String? step,
int? frameIndex,
});
Future<Success> resume(String isolateId,
{/*StepOption*/ String? step, int? frameIndex});
/// The `setBreakpointState` RPC allows for breakpoints to be enabled or
/// disabled, without requiring for the breakpoint to be completely removed.
@@ -1165,10 +1143,7 @@ abstract interface class VmServiceInterface {
///
/// See [Breakpoint].
Future<Breakpoint> setBreakpointState(
String isolateId,
String breakpointId,
bool enable,
);
String isolateId, String breakpointId, bool enable);
/// The `setExceptionPauseMode` RPC is used to control if an isolate pauses
/// when an exception is thrown.
@@ -1186,9 +1161,7 @@ abstract interface class VmServiceInterface {
/// returned.
@Deprecated('Use setIsolatePauseMode instead')
Future<Success> setExceptionPauseMode(
String isolateId,
/*ExceptionPauseMode*/ String mode,
);
String isolateId, /*ExceptionPauseMode*/ String mode);
/// The `setIsolatePauseMode` RPC is used to control if or when an isolate
/// will pause due to a change in execution state.
@@ -1207,11 +1180,9 @@ abstract interface class VmServiceInterface {
///
/// This method will throw a [SentinelException] in the case a [Sentinel] is
/// returned.
Future<Success> setIsolatePauseMode(
String isolateId, {
/*ExceptionPauseMode*/ String? exceptionPauseMode,
bool? shouldPauseOnExit,
});
Future<Success> setIsolatePauseMode(String isolateId,
{/*ExceptionPauseMode*/ String? exceptionPauseMode,
bool? shouldPauseOnExit});
/// The `setFlag` RPC is used to set a VM flag at runtime. Returns an error if
/// the named flag does not exist, the flag may not be set at runtime, or the
@@ -1251,10 +1222,7 @@ abstract interface class VmServiceInterface {
/// This method will throw a [SentinelException] in the case a [Sentinel] is
/// returned.
Future<Success> setLibraryDebuggable(
String isolateId,
String libraryId,
bool isDebuggable,
);
String isolateId, String libraryId, bool isDebuggable);
/// The `setName` RPC is used to change the debugging name for an isolate.
///
@@ -1282,10 +1250,7 @@ abstract interface class VmServiceInterface {
/// This method will throw a [SentinelException] in the case a [Sentinel] is
/// returned.
Future<Success> setTraceClassAllocation(
String isolateId,
String classId,
bool enable,
);
String isolateId, String classId, bool enable);
/// The `setVMName` RPC is used to change the debugging name for the vm.
///
@@ -1406,12 +1371,8 @@ class VmServerConnection {
/// Pending service extension requests to this client by id.
final _pendingServiceExtensionRequests = <dynamic, _PendingServiceRequest>{};
VmServerConnection(
this._requestStream,
this._responseSink,
this._serviceExtensionRegistry,
this._serviceImplementation,
) {
VmServerConnection(this._requestStream, this._responseSink,
this._serviceExtensionRegistry, this._serviceImplementation) {
_requestStream.listen(_delegateRequest, onDone: _doneCompleter.complete);
done.then((_) {
for (var sub in _streamSubscriptions.values) {
@@ -1426,8 +1387,7 @@ class VmServerConnection {
/// We don't attempt to do any serialization or deserialization of the
/// request or response in this case
Future<Map<String, Object?>> _forwardServiceExtensionRequest(
Map<String, Object?> request,
) {
Map<String, Object?> request) {
final originalId = request['id'];
request = Map<String, Object?>.of(request);
// Modify the request ID to ensure we don't have conflicts between
@@ -1451,12 +1411,8 @@ class VmServerConnection {
}
final method = request['method'] as String?;
if (method == null) {
throw RPCError(
null,
RPCErrorKind.kInvalidRequest.code,
'Invalid Request',
request,
);
throw RPCError(null, RPCErrorKind.kInvalidRequest.code,
'Invalid Request', request);
}
final params = request['params'] as Map<String, dynamic>?;
late Response response;
@@ -1718,10 +1674,14 @@ class VmServerConnection {
response = await _serviceImplementation.getVMTimelineMicros();
break;
case 'pause':
response = await _serviceImplementation.pause(params!['isolateId']);
response = await _serviceImplementation.pause(
params!['isolateId'],
);
break;
case 'kill':
response = await _serviceImplementation.kill(params!['isolateId']);
response = await _serviceImplementation.kill(
params!['isolateId'],
);
break;
case 'lookupResolvedPackageUris':
response = await _serviceImplementation.lookupResolvedPackageUris(
@@ -1811,7 +1771,9 @@ class VmServerConnection {
);
break;
case 'setVMName':
response = await _serviceImplementation.setVMName(params!['name']);
response = await _serviceImplementation.setVMName(
params!['name'],
);
break;
case 'setVMTimelineFlags':
response = await _serviceImplementation.setVMTimelineFlags(
@@ -1856,7 +1818,10 @@ class VmServerConnection {
_responseSink.add({
'jsonrpc': '2.0',
'method': 'streamNotify',
'params': {'streamId': id, 'event': e.toJson()},
'params': {
'streamId': id,
'event': e.toJson(),
},
});
});
response = Success();
@@ -1870,31 +1835,22 @@ class VmServerConnection {
if (registeredClient != null) {
// Check for any client which has registered this extension, if we
// have one then delegate the request to that client.
_responseSink.add(
await registeredClient._forwardServiceExtensionRequest(request),
);
_responseSink.add(await registeredClient
._forwardServiceExtensionRequest(request));
// Bail out early in this case, we are just acting as a proxy and
// never get a `Response` instance.
return;
} else if (method.startsWith('ext.')) {
// Remaining methods with `ext.` are assumed to be registered via
// dart:developer, which the service implementation handles.
final args = params == null
? null
: Map<String, dynamic>.of(params);
final args =
params == null ? null : Map<String, dynamic>.of(params);
final isolateId = args?.remove('isolateId');
response = await _serviceImplementation.callServiceExtension(
method,
isolateId: isolateId,
args: args,
);
response = await _serviceImplementation.callServiceExtension(method,
isolateId: isolateId, args: args);
} else {
throw RPCError(
method,
RPCErrorKind.kMethodNotFound.code,
'Method not found',
request,
);
throw RPCError(method, RPCErrorKind.kMethodNotFound.code,
'Method not found', request);
}
}
_responseSink.add({