blob: 8e90b0622944f1114dd3c5e43ea656c5e56ac868 [file] [log] [blame]
// Copyright (c) 2021, 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.
// Dart debug symbol information stored by DDC.
//
// The data format below stores descriptions of dart code objects and their
// mapping to JS that is generated by DDC. Every field, except ids, describes
// dart.
// Note that 'localId' and 'scopeId' combine into a unique id that is used for
// object lookup and mapping between JS and dart concepts. As a result, it
// needs to be either stored or easily computed for each corresponding JS object
// created by DDC, so the debugger is able to look up dart symbol from JS ones.
//
// For example, to detect all dart variables in current scope and display
// their values, the debugger can do the following:
//
// - map current JS location to dart location using source maps
// - find all nested dart scopes that include the current dart location
// - collect all dart variables in scope
// - look up corresponding variables and their values in JS scope by their
// JS ids
// - display their values (non-expanded)
//
// To display a JS value of variable 'v' (non-expanded)
//
// - v: <dart type name> (jsvalue.toString())
//
// Where <dart type name> is the dart type of the dart variable 'v'
// at runtime.
//
// TODO: describe displaying specific non-expanded JS instances in dart
// way, for example, lists, maps, types - is JS toString() enough?
//
// To display a value (expanded)
//
// - look up the JS runtime type of the value
// - find the dart value's runtime type by JS id value's runtime type id
// - collect all dart fields of that type, including the inherited fields
// - map dart fields to JS field ids and look up their values using object
// ids referenced by the original displayed value.
// - display their values (non-expanded)
class SemanticVersion {
final int major;
final int minor;
final int patch;
const SemanticVersion(
this.major,
this.minor,
this.patch,
);
static SemanticVersion parse(String version) {
var parts = version.split('.');
if (parts.length != 3) {
throw FormatException('Version: $version '
'does not follow simple semantic versioning format');
}
var major = int.parse(parts[0]);
var minor = int.parse(parts[1]);
var patch = int.parse(parts[2]);
return SemanticVersion(major, minor, patch);
}
/// Text version.
String get version => '$major.$minor.$patch';
/// True if this version is compatible with [version].
///
/// The minor and patch version changes never remove any fields that current
/// version supports, so the reader can create current metadata version from
/// any file created with a later reader, as long as the major version does
/// not change.
bool isCompatibleWith(String version) {
var other = parse(version);
return other.major == major && other.minor >= minor && other.patch >= patch;
}
}
abstract class SymbolTableElement {
Map<String, dynamic> toJson();
}
class ModuleSymbols implements SymbolTableElement {
/// Current symbol information version.
///
/// Version follows simple semantic versioning format 'major.minor.patch'
/// See https://semver.org
static const SemanticVersion current = SemanticVersion(0, 0, 1);
/// Semantic version of the format.
final String version;
/// Module name as used in the module metadata
final String moduleName;
/// All dart libraries included in the module.
///
/// Note here and below that imported elements are not included in
/// the current module but can be referenced by their ids.
final List<LibrarySymbol> libraries;
/// All dart scripts included in the module.
final List<Script> scripts;
/// All dart classes included in the module.
final List<ClassSymbol> classes;
/// All dart function types included in the module.
final List<FunctionTypeSymbol> functionTypes;
/// All dart function types included in the module.
final List<FunctionSymbol> functions;
/// All dart scopes included in the module.
///
/// Does not include scopes listed in other fields,
/// such as libraries, classes, and functions.
final List<ScopeSymbol> scopes;
/// All Dart variables included in the module.
List<VariableSymbol> variables;
ModuleSymbols({
String? version,
required this.moduleName,
List<LibrarySymbol>? libraries,
List<Script>? scripts,
List<ClassSymbol>? classes,
List<FunctionTypeSymbol>? functionTypes,
List<FunctionSymbol>? functions,
List<ScopeSymbol>? scopes,
List<VariableSymbol>? variables,
}) : version = version ??= current.version,
libraries = libraries ?? [],
scripts = scripts ?? [],
classes = classes ?? [],
functionTypes = functionTypes ?? [],
functions = functions ?? [],
scopes = scopes ?? [],
variables = variables ?? [];
ModuleSymbols.fromJson(Map<String, dynamic> json)
: version = _readAndValidateVersionFromJson(json['version']),
moduleName = _createValue(json['moduleName']),
libraries =
_createObjectList(json['libraries'], LibrarySymbol.fromJson),
scripts = _createObjectList(json['scripts'], Script.fromJson),
classes = _createObjectList(json['classes'], ClassSymbol.fromJson),
functionTypes = _createObjectList(
json['functionTypes'], FunctionTypeSymbol.fromJson),
functions =
_createObjectList(json['functions'], FunctionSymbol.fromJson),
scopes = _createObjectList(json['scopes'], ScopeSymbol.fromJson),
variables =
_createObjectList(json['variables'], VariableSymbol.fromJson);
@override
Map<String, dynamic> toJson() {
final json = <String, dynamic>{
'version': version,
'moduleName': moduleName,
};
_setObjectListIfNotNullOrEmpty(json, 'libraries', libraries);
_setObjectListIfNotNullOrEmpty(json, 'scripts', scripts);
_setObjectListIfNotNullOrEmpty(json, 'classes', classes);
_setObjectListIfNotNullOrEmpty(json, 'functionTypes', functionTypes);
_setObjectListIfNotNullOrEmpty(json, 'functions', functions);
_setObjectListIfNotNullOrEmpty(json, 'scopes', scopes);
_setObjectListIfNotNullOrEmpty(json, 'variables', variables);
return json;
}
static String _readAndValidateVersionFromJson(dynamic json) {
if (json == null) return current.version;
var version = _createValue<String>(json);
if (!current.isCompatibleWith(version)) {
throw Exception('Unsupported version $version. '
'Current version: ${current.version}');
}
return version;
}
}
class Symbol implements SymbolTableElement {
/// Local id (such as JS name) for the symbol.
///
/// Used to map from Dart objects to JS objects inside a scope.
final String localId;
/// Enclosing scope of the symbol.
final String? scopeId;
/// Source location of the symbol.
final SourceLocation? location;
/// Unique Id, shared with JS representation (if any).
///
/// '<scope id>|<js name>'
///
/// Where scope refers to a Library, Class, Function, or Scope.
String get id => scopeId == null ? localId : '$scopeId|$localId';
Symbol({required this.localId, this.scopeId, this.location});
Symbol.fromJson(Map<String, dynamic> json)
: localId = _createValue(json['localId']),
scopeId = _createValue(json['scopeId']),
location =
_createNullableObject(json['location'], SourceLocation.fromJson);
@override
Map<String, dynamic> toJson() {
final json = <String, dynamic>{
'localId': localId,
if (scopeId != null) 'scopeId': scopeId,
};
_setObjectIfNotNull(json, 'location', location);
return json;
}
}
abstract class TypeSymbol {
String get id;
}
enum VariableSymbolKind { global, local, property, field, formal, none }
VariableSymbolKind parseVariableSymbolKind(String value) {
return VariableSymbolKind.values.singleWhere((e) => value == '$e',
orElse: () {
throw ArgumentError('$value is not VariableSymbolKind');
});
}
class VariableSymbol extends Symbol {
/// Name of the variable in Dart source code.
final String name;
/// Symbol kind.
final VariableSymbolKind kind;
/// The declared type of this symbol in Dart source code.
// TODO(nshahan) Only nullable until we design how to identify types from
// other modules.
final String? typeId;
/// True if this variable const.
final bool isConst;
/// True if this variable final.
final bool isFinal;
/// True if this variable static.
final bool isStatic;
/// Property getter, if any.
final String? getterId;
/// Property setter, if any.
final String? setterId;
VariableSymbol({
required this.name,
required this.kind,
required this.typeId,
bool? isConst,
bool? isFinal,
bool? isStatic,
this.getterId,
this.setterId,
required String localId,
required String scopeId,
required SourceLocation location,
}) : isConst = isConst ?? false,
isFinal = isFinal ?? false,
isStatic = isStatic ?? false,
super(localId: localId, scopeId: scopeId, location: location);
VariableSymbol.fromJson(Map<String, dynamic> json)
: name = _createValue(json['name']),
kind = _createValue(json['kind'],
parse: parseVariableSymbolKind, ifNull: VariableSymbolKind.none),
typeId = _createValue(json['typeId']),
isConst = _createValue(json['isConst']),
isFinal = _createValue(json['isFinal']),
isStatic = _createValue(json['isStatic']),
getterId = _createValue(json['getterId']),
setterId = _createValue(json['setterId']),
super.fromJson(json);
@override
Map<String, dynamic> toJson() => {
...super.toJson(),
'name': name,
'kind': kind.toString(),
if (typeId != null) 'typeId': typeId,
'isConst': isConst,
'isFinal': isFinal,
'isStatic': isStatic,
if (getterId != null) 'getterId': getterId,
if (setterId != null) 'setterId': setterId,
};
}
class ClassSymbol extends ScopeSymbol implements TypeSymbol {
/// The name of this class in Dart source code.
final String name;
/// True if this class is abstract.
final bool isAbstract;
/// True if this class is const.
final bool isConst;
/// The superclass of this class, if any.
final String? superClassId;
/// A list of interface types for this class.
final List<String> interfaceIds;
/// Mapping of type parameter Dart names to JS names.
final Map<String, String> typeParameters;
/// Library that contains this class.
String get libraryId => scopeId!;
/// Fields in this class.
///
/// Including static fields, methods, and properties.
List<String> get fieldIds => variableIds;
/// Functions in this class.
///
/// Includes all static functions, methods, getters,
/// and setters in the current class.
///
/// Does not include functions from superclasses.
List<String> get functionIds => scopeIds;
ClassSymbol({
required this.name,
bool? isAbstract,
bool? isConst,
this.superClassId,
List<String>? interfaceIds,
Map<String, String>? typeParameters,
required String localId,
required String scopeId,
required SourceLocation location,
List<String>? variableIds,
List<String>? scopeIds,
}) : isAbstract = isAbstract ?? false,
isConst = isConst ?? false,
interfaceIds = interfaceIds ?? [],
typeParameters = typeParameters ?? {},
super(
localId: localId,
scopeId: scopeId,
variableIds: variableIds,
scopeIds: scopeIds,
location: location);
ClassSymbol.fromJson(Map<String, dynamic> json)
: name = _createValue(json['name']),
isAbstract = _createValue(json['isAbstract']),
isConst = _createValue(json['isConst']),
superClassId = _createValue(json['superClassId']),
interfaceIds = _createValueList(json['interfaceIds']),
typeParameters = _createValueMap(json['typeParameters']),
super.fromJson(json);
@override
Map<String, dynamic> toJson() => {
...super.toJson(),
'name': name,
'isAbstract': isAbstract,
'isConst': isConst,
if (superClassId != null) 'superClassId': superClassId,
if (interfaceIds.isNotEmpty) 'interfaceIds': interfaceIds,
if (typeParameters.isNotEmpty) 'typeParameters': typeParameters,
};
}
class FunctionTypeSymbol extends Symbol implements TypeSymbol {
/// Mapping of dart type parameter names to JS names.
final Map<String, String> typeParameters;
/// Types for positional parameters for this function.
final List<String> parameterTypeIds;
/// Types for optional positional parameters for this function.
final List<String> optionalParameterTypeIds;
/// Names and types for named parameters for this function.
final Map<String, String> namedParameterTypeIds;
/// The return type for this function.
final String returnTypeId;
FunctionTypeSymbol({
Map<String, String>? typeParameters,
List<String>? parameterTypeIds,
List<String>? optionalParameterTypeIds,
Map<String, String>? namedParameterTypeIds,
required this.returnTypeId,
required String localId,
required String scopeId,
required SourceLocation location,
}) : typeParameters = typeParameters ?? {},
parameterTypeIds = parameterTypeIds ?? [],
optionalParameterTypeIds = optionalParameterTypeIds ?? [],
namedParameterTypeIds = namedParameterTypeIds ?? {},
super(localId: localId, scopeId: scopeId, location: location);
FunctionTypeSymbol.fromJson(Map<String, dynamic> json)
: parameterTypeIds = _createValueList(json['parameterTypeIds']),
optionalParameterTypeIds =
_createValueList(json['optionalParameterTypeIds']),
typeParameters = _createValueMap(json['typeParameters']),
namedParameterTypeIds = _createValueMap(json['namedParameterTypeIds']),
returnTypeId = _createValue(json['returnTypeId']),
super.fromJson(json);
@override
Map<String, dynamic> toJson() => {
...super.toJson(),
if (typeParameters.isNotEmpty) 'typeParameters': typeParameters,
if (parameterTypeIds.isNotEmpty) 'parameterTypeIds': parameterTypeIds,
if (optionalParameterTypeIds.isNotEmpty)
'optionalParameterTypeIds': optionalParameterTypeIds,
if (namedParameterTypeIds.isNotEmpty)
'namedParameterTypeIds': namedParameterTypeIds,
'returnTypeId': returnTypeId,
};
}
class FunctionSymbol extends ScopeSymbol {
/// The name of this function.
final String name;
/// Unique Id, shared with JS representation (if any).
///
/// Format:
/// '<scope id>|<js name>'
///
/// Where scope refers to a Library, Class, Function, or Scope.
/// String id;
/// Declared type of this function.
// TODO(nshahan) Only nullable because unused at this time.
final String? typeId;
/// True if this function is static.
final bool isStatic;
/// True if this function is const.
final bool isConst;
FunctionSymbol({
required this.name,
required this.typeId,
bool? isStatic,
bool? isConst,
required String localId,
required String scopeId,
List<String>? variableIds,
List<String>? scopeIds,
required SourceLocation location,
}) : isStatic = isStatic ?? false,
isConst = isConst ?? false,
super(
localId: localId,
scopeId: scopeId,
variableIds: variableIds,
scopeIds: scopeIds,
location: location,
);
FunctionSymbol.fromJson(Map<String, dynamic> json)
: name = _createValue(json['name']),
typeId = _createValue(json['typeId']),
isStatic = _createValue(json['isStatic']),
isConst = _createValue(json['isConst']),
super.fromJson(json);
@override
Map<String, dynamic> toJson() => {
...super.toJson(),
'name': name,
if (typeId != null) 'typeId': typeId,
'isStatic': isStatic,
'isConst': isConst,
};
}
class LibrarySymbol extends ScopeSymbol {
/// The name of this library.
final String name;
/// The uri of this library.
final String uri;
/// A list of the imports for this library.
final List<LibrarySymbolDependency> dependencies;
/// A list of the scripts which constitute this library.
final List<String> scriptIds;
LibrarySymbol({
String? name,
required this.uri,
List<LibrarySymbolDependency>? dependencies,
required this.scriptIds,
List<String>? variableIds,
List<String>? scopeIds,
}) : name = name ?? '',
dependencies = dependencies ?? [],
super(
localId: uri,
variableIds: variableIds,
scopeIds: scopeIds,
);
LibrarySymbol.fromJson(Map<String, dynamic> json)
: name = _createValue(json['name'], ifNull: ''),
uri = _createValue(json['uri']),
scriptIds = _createValueList(json['scriptIds']),
dependencies = _createObjectList(
json['dependencies'], LibrarySymbolDependency.fromJson),
super.fromJson(json);
@override
Map<String, dynamic> toJson() {
final json = {
...super.toJson(),
if (name.isNotEmpty) 'name': name,
'uri': uri,
if (scriptIds.isNotEmpty) 'scriptIds': scriptIds,
};
_setObjectListIfNotNullOrEmpty(json, 'dependencies', dependencies);
return json;
}
}
class LibrarySymbolDependency implements SymbolTableElement {
/// True if this dependency an import, false if an an export.
final bool isImport;
/// True if this dependency is deferred.
final bool isDeferred;
/// The prefix of an 'as' import, or null.
final String? prefix;
/// The library being imported or exported.
final String targetId;
LibrarySymbolDependency({
required this.isImport,
bool? isDeferred,
this.prefix,
required this.targetId,
}) : isDeferred = isDeferred ?? false;
LibrarySymbolDependency.fromJson(Map<String, dynamic> json)
: isImport = _createValue(json['isImport']),
isDeferred = _createValue(json['isDeferred']),
prefix = _createValue(json['prefix']),
targetId = _createValue(json['targetId']);
@override
Map<String, dynamic> toJson() => {
'isImport': isImport,
'isDeferred': isDeferred,
if (prefix != null) 'prefix': prefix,
'targetId': targetId,
};
}
class Script implements SymbolTableElement {
/// The uri from which this script was loaded.
final String uri;
/// Unique Id.
///
/// This can be just an integer. The mapping from JS to dart script
/// happens using the source map. The id is only used for references
/// in other elements.
final String localId;
final String libraryId;
String get id => '$libraryId|$localId';
Script({
required this.uri,
required this.localId,
required this.libraryId,
});
Script.fromJson(Map<String, dynamic> json)
: uri = _createValue(json['uri']),
localId = _createValue(json['localId']),
libraryId = _createValue(json['libraryId']);
@override
Map<String, dynamic> toJson() => {
'uri': uri,
'localId': localId,
'libraryId': libraryId,
};
}
class ScopeSymbol extends Symbol {
/// A list of the top-level variables in this scope.
final List<String> variableIds;
/// Enclosed scopes.
///
/// Includes all top classes, functions, inner scopes.
final List<String> scopeIds;
ScopeSymbol({
List<String>? variableIds,
List<String>? scopeIds,
required String localId,
String? scopeId,
SourceLocation? location,
}) : variableIds = variableIds ?? [],
scopeIds = scopeIds ?? [],
super(
localId: localId,
scopeId: scopeId,
location: location,
);
ScopeSymbol.fromJson(Map<String, dynamic> json)
: variableIds = _createValueList(json['variableIds']),
scopeIds = _createValueList(json['scopeIds']),
super.fromJson(json);
@override
Map<String, dynamic> toJson() => {
...super.toJson(),
if (variableIds.isNotEmpty) 'variableIds': variableIds,
if (scopeIds.isNotEmpty) 'scopeIds': scopeIds,
};
}
class SourceLocation implements SymbolTableElement {
/// The script containing the source location.
final String scriptId;
/// The first token of the location.
final int tokenPos;
/// The last token of the location if this is a range.
final int? endTokenPos;
SourceLocation({
required this.scriptId,
required this.tokenPos,
this.endTokenPos,
});
SourceLocation.fromJson(Map<String, dynamic> json)
: scriptId = _createValue(json['scriptId']),
tokenPos = _createValue(json['tokenPos']),
endTokenPos = _createValue(json['endTokenPos']);
@override
Map<String, dynamic> toJson() => {
'scriptId': scriptId,
'tokenPos': tokenPos,
if (endTokenPos != null) 'endTokenPos': endTokenPos,
};
}
List<T> _createObjectList<T>(
dynamic json, T Function(Map<String, dynamic>) creator) {
if (json == null) return <T>[];
if (json is List) {
return json.map((e) => _createObject(e, creator)).toList();
}
throw ArgumentError('Not a list: $json');
}
T _createObject<T>(dynamic json, T Function(Map<String, dynamic>) creator) {
if (json is Map<String, dynamic>) {
return creator(json);
}
throw ArgumentError('Not a map: $json');
}
T? _createNullableObject<T>(
dynamic json, T Function(Map<String, dynamic>) creator) =>
json == null ? null : _createObject(json, creator);
List<T> _createValueList<T>(dynamic json,
{T? ifNull, T Function(String)? parse}) {
if (json == null) return <T>[];
if (json is List) {
return json
.map((e) => _createValue<T>(e, ifNull: ifNull, parse: parse))
.toList();
}
throw ArgumentError('Not a list: $json');
}
Map<String, T> _createValueMap<T>(dynamic json) {
if (json == null) return <String, T>{};
return Map<String, T>.from(json as Map<String, dynamic>);
}
T _createValue<T>(dynamic json, {T? ifNull, T Function(String)? parse}) {
if (json == null && ifNull is T) return ifNull;
if (json is T) {
return json;
}
if (json is String && parse != null) {
return parse(json);
}
throw ArgumentError('Cannot parse $json as $T');
}
void _setObjectListIfNotNullOrEmpty<T extends SymbolTableElement>(
Map<String, dynamic> json, String key, List<T>? values) {
if (values == null || values.isEmpty) return;
json[key] = values.map((e) => e.toJson()).toList();
}
void _setObjectIfNotNull<T extends SymbolTableElement>(
Map<String, dynamic> json, String key, T? value) {
if (value == null) return;
json[key] = value.toJson();
}