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