blob: 73f14cfabb2ecfabb43e8ec29eb919b0fc2fefd5 [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 = 2.9
// 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.
String version;
/// Module name as used in the module metadata
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.
List<LibrarySymbol> libraries;
/// All dart scripts included in the module.
List<Script> scripts;
/// All dart classes included in the module.
List<ClassSymbol> classes;
/// All dart function types included in the module.
List<FunctionTypeSymbol> functionTypes;
/// All dart function types included in the module.
List<FunctionSymbol> functions;
/// All dart scopes included in the module.
///
/// Does not include scopes listed in other fields,
/// such as libraries, classes, and functions.
List<ScopeSymbol> scopes;
/// All dart variables included in the module.
List<VariableSymbol> variables;
ModuleSymbols({
this.version,
this.moduleName,
this.libraries,
this.scripts,
this.classes,
this.functionTypes,
this.functions,
this.scopes,
this.variables,
});
ModuleSymbols.fromJson(Map<String, dynamic> json) {
version = _createValue(json['version'], ifNull: current.version);
if (!current.isCompatibleWith(version)) {
throw Exception('Unsupported version $version. '
'Current version: ${current.version}');
}
moduleName = _createValue(json['moduleName']);
libraries = _createObjectList(
json['libraries'], (json) => LibrarySymbol.fromJson(json));
scripts =
_createObjectList(json['scripts'], (json) => Script.fromJson(json));
classes = _createObjectList(
json['classes'], (json) => ClassSymbol.fromJson(json));
functionTypes = _createObjectList(
json['functionTypes'], (json) => FunctionTypeSymbol.fromJson(json));
functions = _createObjectList(
json['functions'], (json) => FunctionSymbol.fromJson(json));
scopes =
_createObjectList(json['scopes'], (json) => ScopeSymbol.fromJson(json));
variables = _createObjectList(
json['variables'], (json) => VariableSymbol.fromJson(json));
}
@override
Map<String, dynamic> toJson() {
final json = <String, dynamic>{};
_setValueIfNotNull(json, 'version', version);
_setValueIfNotNull(json, 'moduleName', moduleName);
_setObjectListIfNotNull(json, 'libraries', libraries);
_setObjectListIfNotNull(json, 'scripts', scripts);
_setObjectListIfNotNull(json, 'classes', classes);
_setObjectListIfNotNull(json, 'functionTypes', functionTypes);
_setObjectListIfNotNull(json, 'functions', functions);
_setObjectListIfNotNull(json, 'scopes', scopes);
_setObjectListIfNotNull(json, 'variables', variables);
return json;
}
}
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.
String localId;
/// Enclosing scope of the symbol.
String scopeId;
/// 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';
/// Source location of the symbol.
SourceLocation location;
Symbol({this.localId, this.scopeId, this.location});
Symbol.fromJson(Map<String, dynamic> json) {
localId = _createValue(json['localId']);
scopeId = _createValue(json['scopeId']);
location = _createObject(
json['location'], (json) => SourceLocation.fromJson(json));
}
@override
Map<String, dynamic> toJson() {
final json = <String, dynamic>{};
_setValueIfNotNull(json, 'localId', localId);
_setValueIfNotNull(json, '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 {
/// Variable name
String name;
/// Symbol kind.
VariableSymbolKind kind;
/// The declared type of this symbol.
String typeId;
/// Is this variable const?
bool isConst;
/// Is this variable final?
bool isFinal;
/// Is this variable static?
bool isStatic;
/// Property getter, if any.
String getterId;
/// Property setter, if any.
String setterId;
VariableSymbol({
this.name,
this.kind,
this.isConst,
this.isFinal,
this.isStatic,
this.typeId,
String localId,
String scopeId,
SourceLocation location,
}) : super(localId: localId, scopeId: scopeId, location: location);
VariableSymbol.fromJson(Map<String, dynamic> json) : super.fromJson(json) {
name = _createValue(json['name']);
kind = _createValue(json['kind'],
parse: parseVariableSymbolKind, ifNull: VariableSymbolKind.none);
isConst = _createValue(json['isConst']);
isFinal = _createValue(json['isFinal']);
isStatic = _createValue(json['isStatic']);
typeId = _createValue(json['typeId']);
setterId = _createValue(json['setterId']);
getterId = _createValue(json['getterId']);
}
@override
Map<String, dynamic> toJson() {
final json = super.toJson();
_setValueIfNotNull(json, 'name', name);
_setValueIfNotNull(json, 'kind', kind.toString());
_setValueIfNotNull(json, 'isConst', isConst);
_setValueIfNotNull(json, 'isFinal', isFinal);
_setValueIfNotNull(json, 'isStatic', isStatic);
_setValueIfNotNull(json, 'typeId', typeId);
_setValueIfNotNull(json, 'setterId', setterId);
_setValueIfNotNull(json, 'getterId', getterId);
return json;
}
}
class ClassSymbol extends ScopeSymbol implements TypeSymbol {
/// The name of this class.
String name;
/// Is this an abstract class?
bool isAbstract;
/// Is this a const class?
bool isConst;
/// The superclass of this class, if any.
String superClassId;
/// A list of interface types for this class.
List<String> interfaceIds;
/// Mapping of type parameter dart names to JS names.
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;
/// A list of 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({
this.name,
this.isAbstract,
this.isConst,
this.superClassId,
this.interfaceIds,
this.typeParameters,
String localId,
String scopeId,
SourceLocation location,
List<String> variableIds,
List<String> scopeIds,
}) : super(
localId: localId,
scopeId: scopeId,
variableIds: variableIds,
scopeIds: scopeIds,
location: location);
ClassSymbol.fromJson(Map<String, dynamic> json) : super.fromJson(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']);
}
@override
Map<String, dynamic> toJson() {
final json = super.toJson();
_setValueIfNotNull(json, 'name', name);
_setValueIfNotNull(json, 'isAbstract', isAbstract);
_setValueIfNotNull(json, 'isConst', isConst);
_setValueIfNotNull(json, 'superClassId', superClassId);
_setValueIfNotNull(json, 'interfaceIds', interfaceIds);
_setValueIfNotNull(json, 'typeParameters', typeParameters);
return json;
}
}
class FunctionTypeSymbol extends Symbol implements TypeSymbol {
/// Mapping of dart type parameter names to JS names.
Map<String, String> typeParameters;
/// Types for positional parameters for this function.
List<String> parameterTypeIds;
/// Types for optional positional parameters for this function.
List<String> optionalParameterTypeIds;
/// Names and types for named parameters for this function.
Map<String, String> namedParameterTypeIds;
/// A return type for this function.
String returnTypeId;
FunctionTypeSymbol({
this.typeParameters,
this.parameterTypeIds,
this.optionalParameterTypeIds,
this.namedParameterTypeIds,
this.returnTypeId,
String localId,
String scopeId,
SourceLocation location,
}) : super(localId: localId, scopeId: scopeId, location: location);
FunctionTypeSymbol.fromJson(Map<String, dynamic> json)
: super.fromJson(json) {
parameterTypeIds = _createValueList(json['parameterTypeIds']);
optionalParameterTypeIds =
_createValueList(json['optionalParameterTypeIds']);
typeParameters = _createValueMap(json['typeParameters']);
namedParameterTypeIds = _createValueMap(json['namedParameterTypeIds']);
returnTypeId = _createValue(json['returnTypeId']);
}
@override
Map<String, dynamic> toJson() {
final json = super.toJson();
_setValueIfNotNull(json, 'typeParameters', typeParameters);
_setValueIfNotNull(json, 'parameterTypeIds', parameterTypeIds);
_setValueIfNotNull(
json, 'optionalParameterTypeIds', optionalParameterTypeIds);
_setValueIfNotNull(json, 'namedParameterTypeIds', namedParameterTypeIds);
_setValueIfNotNull(json, 'returnTypeId', returnTypeId);
return json;
}
}
class FunctionSymbol extends ScopeSymbol {
/// The name of this function.
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.
String typeId;
/// Is this function static?
bool isStatic;
/// Is this function const?
bool isConst;
FunctionSymbol({
this.name,
this.typeId,
this.isStatic,
this.isConst,
String localId,
String scopeId,
List<String> variableIds,
List<String> scopeIds,
SourceLocation location,
}) : super(
localId: localId,
scopeId: scopeId,
variableIds: variableIds,
scopeIds: scopeIds,
location: location,
);
FunctionSymbol.fromJson(Map<String, dynamic> json) : super.fromJson(json) {
name = _createValue(json['name']);
typeId = _createValue(json['typeId']);
isStatic = _createValue(json['isStatic']);
isConst = _createValue(json['isConst']);
}
@override
Map<String, dynamic> toJson() {
final json = super.toJson();
_setValueIfNotNull(json, 'name', name);
_setValueIfNotNull(json, 'typeId', typeId);
_setValueIfNotNull(json, 'isStatic', isStatic);
_setValueIfNotNull(json, 'isConst', isConst);
return json;
}
}
class LibrarySymbol extends ScopeSymbol {
/// The name of this library.
String name;
/// Unique Id.
///
/// Currently the debugger can find the library uri from JS location
/// using source maps and module metadata.
///
/// Can be same as library uri.
/// String id;
/// The uri of this library.
String uri;
/// A list of the imports for this library.
List<LibrarySymbolDependency> dependencies;
/// A list of the scripts which constitute this library.
List<String> scriptIds;
LibrarySymbol({
this.name,
this.uri,
this.dependencies,
this.scriptIds,
List<String> variableIds,
List<String> scopeIds,
}) : super(
localId: uri,
variableIds: variableIds,
scopeIds: scopeIds,
);
LibrarySymbol.fromJson(Map<String, dynamic> json) : super.fromJson(json) {
name = _createValue(json['name'], ifNull: '');
uri = _createValue(json['uri']);
scriptIds = _createValueList(json['scriptIds']);
dependencies = _createObjectList(
json['dependencies'], (json) => LibrarySymbolDependency.fromJson(json));
}
@override
Map<String, dynamic> toJson() {
final json = super.toJson();
_setValueIfNotNull(json, 'name', name);
_setValueIfNotNull(json, 'uri', uri);
_setValueIfNotNull(json, 'scriptIds', scriptIds);
_setObjectListIfNotNull(json, 'dependencies', dependencies);
return json;
}
}
class LibrarySymbolDependency implements SymbolTableElement {
/// Is this dependency an import (rather than an export)?
bool isImport;
/// Is this dependency deferred?
bool isDeferred;
/// The prefix of an 'as' import, or null.
String prefix;
/// The library being imported or exported.
String targetId;
LibrarySymbolDependency({
this.isImport,
this.isDeferred,
this.prefix,
this.targetId,
});
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() {
final json = <String, dynamic>{};
_setValueIfNotNull(json, 'isImport', isImport);
_setValueIfNotNull(json, 'isDeferred', isDeferred);
_setValueIfNotNull(json, 'prefix', prefix);
_setValueIfNotNull(json, 'targetId', targetId);
return json;
}
}
class Script implements SymbolTableElement {
/// The uri from which this script was loaded.
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.
String localId;
String libraryId;
String get id => '$libraryId|$localId';
Script({
this.uri,
this.localId,
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() {
final json = <String, dynamic>{};
_setValueIfNotNull(json, 'uri', uri);
_setValueIfNotNull(json, 'localId', localId);
_setValueIfNotNull(json, 'libraryId', libraryId);
return json;
}
}
class ScopeSymbol extends Symbol {
/// A list of the top-level variables in this scope.
List<String> variableIds;
/// Enclosed scopes.
///
/// Includes all top classes, functions, inner scopes.
List<String> scopeIds;
ScopeSymbol({
this.variableIds,
this.scopeIds,
String localId,
String scopeId,
SourceLocation location,
}) : super(
localId: localId,
scopeId: scopeId,
location: location,
);
ScopeSymbol.fromJson(Map<String, dynamic> json) : super.fromJson(json) {
variableIds = _createValueList(json['variableIds']);
scopeIds = _createValueList(json['scopeIds']);
}
@override
Map<String, dynamic> toJson() {
final json = super.toJson();
_setValueIfNotNull(json, 'variableIds', variableIds);
_setValueIfNotNull(json, 'scopeIds', scopeIds);
return json;
}
}
class SourceLocation implements SymbolTableElement {
/// The script containing the source location.
String scriptId;
/// The first token of the location.
int tokenPos;
/// The last token of the location if this is a range.
int endTokenPos;
SourceLocation({
this.scriptId,
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() {
final json = <String, dynamic>{};
_setValueIfNotNull(json, 'scriptId', scriptId);
_setValueIfNotNull(json, 'tokenPos', tokenPos);
_setValueIfNotNull(json, 'endTokenPos', endTokenPos);
return json;
}
}
List<T> _createObjectList<T>(
dynamic json, T Function(Map<String, dynamic>) creator) {
if (json == null) return null;
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 == null) return null;
if (json is Map<String, dynamic>) {
return creator(json);
}
throw ArgumentError('Not a map: $json');
}
List<T> _createValueList<T>(dynamic json,
{T ifNull, T Function(String) parse}) {
if (json == null) return null;
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 null;
return Map<String, T>.from(json as Map<String, dynamic>);
}
T _createValue<T>(dynamic json, {T ifNull, T Function(String) parse}) {
if (json == null) 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 _setObjectListIfNotNull<T extends SymbolTableElement>(
Map<String, dynamic> json, String key, List<T> values) {
if (values == null) 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();
}
void _setValueIfNotNull<T>(Map<String, dynamic> json, String key, T value) {
if (value == null) return;
json[key] = value;
}