| // 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(); |
| } |