diff --git a/pkgs/hooks/tool/normalize.dart b/pkgs/hooks/tool/normalize.dart index 83b479c40f..a1e188eaee 100644 --- a/pkgs/hooks/tool/normalize.dart +++ b/pkgs/hooks/tool/normalize.dart @@ -29,6 +29,7 @@ void main(List arguments) { Directory.fromUri(packageUri.resolve('../code_assets/')), Directory.fromUri(packageUri.resolve('../data_assets/')), Directory.fromUri(packageUri.resolve('../pub_formats/')), + Directory.fromUri(packageUri.resolve('../record_use/')), ]; for (final directory in directories) { final result = processDirectory(directory); @@ -62,7 +63,12 @@ ProcessDirectoryResult processDirectory(Directory directory) { for (final entity in entities) { if (entity is File && p.extension(entity.path) == '.json' && - !entity.path.contains('.dart_tool/')) { + // Don't sort non-source files. + !entity.uri.toFilePath(windows: false).contains('.dart_tool/') && + // Don't sort recorded uses files, they have ordered arrays. + !entity.uri + .toFilePath(windows: false) + .contains('pkgs/record_use/test_data/json/')) { processedCount++; if (processFile(entity)) { changedCount += 1; @@ -212,15 +218,8 @@ dynamic sortJson(dynamic data, String filePath) { return sortedMap; } if (data is List) { - return data.map((item) => sortJson(item, filePath)).toList()..sort((a, b) { - if (a is Map && b is Map) { - return compareMaps(a, b); - } - if (a is String && b is String) { - return a.compareTo(b); - } - throw UnimplementedError('Not implemented to compare $a and $b.'); - }); + return data.map((item) => sortJson(item, filePath)).toList() + ..sort(_compareTwoItems); } return data; } @@ -229,15 +228,27 @@ int _compareTwoItems(dynamic a, dynamic b) { if (a is Map && b is Map) { return compareMaps(a, b); } - if (a is String && b is String) { - return a.compareTo(b); - } if (a is List && b is List) { return compareLists(a, b); } + if (a is String && b is String) { + return a.compareTo(b); + } if (a is int && b is int) { return a.compareTo(b); } + if (a == b) { + return 0; + } + if (a is bool && b is bool) { + return a ? 1 : -1; + } + if (a == null && b != null) { + return -1; + } + if (b == null && a != null) { + return 1; + } throw UnimplementedError('Not implemented to compare $a and $b.'); } diff --git a/pkgs/json_syntax_generator/lib/src/generator/property_generator.dart b/pkgs/json_syntax_generator/lib/src/generator/property_generator.dart index 8a8db0812c..c8c82fc0b2 100644 --- a/pkgs/json_syntax_generator/lib/src/generator/property_generator.dart +++ b/pkgs/json_syntax_generator/lib/src/generator/property_generator.dart @@ -420,7 +420,17 @@ List $validateName() => _reader.validateOptionalMap<${dartType.valueType '''); } } else { - throw UnimplementedError(valueType.toString()); + buffer.writeln(''' +$dartType get $fieldName => _reader.optionalMap<${dartType.valueType}>('$jsonKey', $keyPattern); + +set $setterName($dartType value) { + _checkArgumentMapKeys(value, $keyPattern); + json.setOrRemove('$jsonKey', value); + $sortOnKey +} + +List $validateName() => _reader.validateMap<${dartType.valueType}>('$jsonKey', $keyPattern); +'''); } default: throw UnimplementedError(valueType.toString()); @@ -536,6 +546,25 @@ set $setterName($dartType value) { List $validateName() => _reader.$jsonValidate('$jsonKey'); '''); + case 'Object': + case 'int': + final jsonRead = isNullable + ? 'optionalList<$itemType>' + : 'list<$itemType>'; + final jsonValidate = isNullable + ? 'validateOptionalList<$itemType>' + : 'validateList<$itemType>'; + final setter = setOrRemove(dartType, jsonKey); + buffer.writeln(''' +$dartType get $fieldName => _reader.$jsonRead('$jsonKey'); + +set $setterName($dartType value) { + $setter + $sortOnKey +} + +List $validateName() => _reader.$jsonValidate('$jsonKey'); +'''); default: throw UnimplementedError(itemType.toString()); } diff --git a/pkgs/json_syntax_generator/lib/src/parser/schema_analyzer.dart b/pkgs/json_syntax_generator/lib/src/parser/schema_analyzer.dart index ff820124d2..caeb3951fa 100644 --- a/pkgs/json_syntax_generator/lib/src/parser/schema_analyzer.dart +++ b/pkgs/json_syntax_generator/lib/src/parser/schema_analyzer.dart @@ -300,28 +300,33 @@ class SchemaAnalyzer { bool required, { bool allowEnum = true, }) { - final type = schemas.type; + final (type, typeIsNullable) = schemas.typeAndNullable; + final isNullable = typeIsNullable || !required; + final DartType dartType; switch (type) { case null: - dartType = ObjectDartType(isNullable: !required); + dartType = ObjectDartType(isNullable: isNullable); case SchemaType.boolean: - dartType = BoolDartType(isNullable: !required); + dartType = BoolDartType(isNullable: isNullable); case SchemaType.integer: - dartType = IntDartType(isNullable: !required); + dartType = IntDartType(isNullable: isNullable); case SchemaType.string: if (schemas.generateUri) { - dartType = UriDartType(isNullable: !required); + dartType = UriDartType(isNullable: isNullable); } else if (schemas.generateEnum && allowEnum) { _analyzeEnumClass(schemas); final classInfo = _classes[schemas.className]!; - dartType = ClassDartType(classInfo: classInfo, isNullable: !required); + dartType = ClassDartType( + classInfo: classInfo, + isNullable: isNullable, + ); } else { if (schemas.patterns.length > 1) { throw UnsupportedError('Only one pattern is supported.'); } final pattern = schemas.patterns.firstOrNull; - dartType = StringDartType(isNullable: !required, pattern: pattern); + dartType = StringDartType(isNullable: isNullable, pattern: pattern); } case SchemaType.object: final additionalPropertiesSchema = @@ -352,7 +357,7 @@ class SchemaAnalyzer { ), isNullable: false, ), - isNullable: !required, + isNullable: isNullable, ); default: throw UnimplementedError(itemType.toString()); @@ -366,7 +371,7 @@ class SchemaAnalyzer { dartType = MapDartType( keyType: keyDartType, valueType: ClassDartType(classInfo: clazz, isNullable: false), - isNullable: !required, + isNullable: isNullable, ); } else { dartType = MapDartType( @@ -375,17 +380,21 @@ class SchemaAnalyzer { valueType: ObjectDartType(isNullable: true), isNullable: false, ), - isNullable: !required, + isNullable: isNullable, ); } case null: if (schemas.additionalPropertiesBool == true) { dartType = ClassDartType( classInfo: jsonObjectClassInfo, - isNullable: !required, + isNullable: isNullable, ); } else { final oneOfs = additionalPropertiesSchema.oneOfs; + if (oneOfs.isEmpty) { + // No type information. + return const ObjectDartType(isNullable: true); + } if (oneOfs.length != 1) { throw UnimplementedError(); } @@ -413,7 +422,7 @@ class SchemaAnalyzer { isNullable: true, pattern: stringPattern, ), - isNullable: !required, + isNullable: isNullable, ); } else { throw UnimplementedError(); @@ -423,7 +432,13 @@ class SchemaAnalyzer { dartType = MapDartType( keyType: keyDartType, valueType: const StringDartType(isNullable: false), - isNullable: !required, + isNullable: isNullable, + ); + case SchemaType.integer: + dartType = MapDartType( + keyType: keyDartType, + valueType: const IntDartType(isNullable: false), + isNullable: isNullable, ); default: throw UnimplementedError(additionalPropertiesType.toString()); @@ -433,31 +448,60 @@ class SchemaAnalyzer { typeName ??= _ucFirst(_snakeToCamelCase(propertyKey)); _analyzeClass(schemas, name: typeName); final classInfo = _classes[typeName]!; - dartType = ClassDartType(classInfo: classInfo, isNullable: !required); + dartType = ClassDartType( + classInfo: classInfo, + isNullable: isNullable, + ); } case SchemaType.array: final items = schemas.items; - final itemType = items.type; + final (itemType, itemNullable) = items.typeAndNullable; switch (itemType) { case SchemaType.string: if (items.generateUri) { dartType = ListDartType( - itemType: const UriDartType(isNullable: false), - isNullable: !required, + itemType: UriDartType(isNullable: itemNullable), + isNullable: isNullable, ); } else { dartType = ListDartType( - itemType: const StringDartType(isNullable: false), - isNullable: !required, + itemType: StringDartType(isNullable: itemNullable), + isNullable: isNullable, ); } - case SchemaType.object: - final typeName = items.className!; - _analyzeClass(items); - final classInfo = _classes[typeName]!; + case SchemaType.integer: dartType = ListDartType( - itemType: ClassDartType(classInfo: classInfo, isNullable: false), - isNullable: !required, + itemType: IntDartType(isNullable: itemNullable), + isNullable: isNullable, + ); + case SchemaType.object: + final typeName = items.className; + if (typeName != null) { + _analyzeClass(items); + final classInfo = _classes[typeName]!; + dartType = ListDartType( + itemType: ClassDartType( + classInfo: classInfo, + isNullable: itemNullable, + ), + isNullable: isNullable, + ); + } else if (items.generateMapOf) { + dartType = const ListDartType( + itemType: MapDartType( + valueType: ObjectDartType(isNullable: true), + isNullable: true, + ), + isNullable: true, + ); + } else { + throw UnimplementedError(itemType.toString()); + } + case null: + // No type information. + dartType = const ListDartType( + itemType: ObjectDartType(isNullable: true), + isNullable: true, ); default: throw UnimplementedError(itemType.toString()); @@ -550,7 +594,7 @@ extension type JsonSchemas._(List _schemas) { for (final schema in _schemas) ...schema.requiredProperties ?? [], }.toList()..sort(); - SchemaType? get type { + Set get types { final types = {}; for (final schema in _schemas) { final schemaTypes = schema.typeList; @@ -560,12 +604,27 @@ extension type JsonSchemas._(List _schemas) { } } } + return types; + } + + SchemaType? get type { if (types.length > 1) { throw StateError('Multiple types found'); } return types.singleOrNull; } + (SchemaType?, bool) get typeAndNullable { + if (types.length <= 1) { + return (types.singleOrNull, false); + } else if (types.length == 2 && types.contains(SchemaType.nullValue)) { + final type = types.firstWhere((t) => t != SchemaType.nullValue); + return (type, true); + } else { + throw UnsupportedError('Multiple types: $types.'); + } + } + List get patterns { final patterns = {}; for (final schema in _schemas) { @@ -745,10 +804,6 @@ extension on JsonSchemas { String? get generateSubClassesKey { if (type != SchemaType.object) return null; - // A tagged union either has only a key, or a key and an encoding. - // Classes with more than 2 properties have their a property that has - // predefined values generated as an enum class. - if (propertyKeys.length > 2) return null; for (final p in propertyKeys) { final propertySchemas = property(p); if (propertySchemas.anyOfs.isNotEmpty) { @@ -785,7 +840,10 @@ extension on JsonSchemas { if (path.contains('#/definitions/')) { final splits = path.split('/'); final indexOf = splits.indexOf('definitions'); - final nameParts = splits.skip(indexOf + 1).where((e) => e.isNotEmpty); + final nameParts = splits + .skip(indexOf + 1) + .where((e) => e.isNotEmpty) + .toList(); if (nameParts.length == 1 && nameParts.single.startsWithUpperCase()) { return nameParts.single; } diff --git a/pkgs/record_use/CHANGELOG.md b/pkgs/record_use/CHANGELOG.md index 29db7fc659..a7e1942ff2 100644 --- a/pkgs/record_use/CHANGELOG.md +++ b/pkgs/record_use/CHANGELOG.md @@ -2,6 +2,7 @@ - Made locations optional to accomodate for dart2js compiler not providing source locations for constant instances. +- Introduce a JSON schema for the json encoding. ## 0.4.2 diff --git a/pkgs/record_use/doc/schema/record_use.schema.json b/pkgs/record_use/doc/schema/record_use.schema.json new file mode 100644 index 0000000000..4040ac69f2 --- /dev/null +++ b/pkgs/record_use/doc/schema/record_use.schema.json @@ -0,0 +1,347 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "$ref": "#/definitions/RecordedUses", + "definitions": { + "Call": { + "type": "object", + "properties": { + "type": { + "type": "string", + "anyOf": [ + { + "enum": [ + "tearoff", + "with_arguments" + ] + }, + { + "type": "string" + } + ] + }, + "@": { + "type": "integer" + }, + "loading_unit": { + "type": "string" + } + }, + "required": [ + "loading_unit", + "type" + ], + "if": { + "properties": { + "type": { + "const": "with_arguments" + } + } + }, + "then": { + "properties": { + "named": { + "type": "object", + "additionalProperties": { + "type": "integer" + } + }, + "positional": { + "type": "array", + "items": { + "type": [ + "integer", + "null" + ] + } + } + } + } + }, + "Constant": { + "type": "object", + "properties": { + "type": { + "type": "string", + "anyOf": [ + { + "enum": [ + "Instance", + "Null", + "String", + "bool", + "int", + "list", + "map" + ] + }, + { + "type": "string" + } + ] + } + }, + "required": [ + "type" + ], + "allOf": [ + { + "if": { + "properties": { + "type": { + "const": "Instance" + } + } + }, + "then": { + "properties": { + "value": { + "type": "object", + "additionalProperties": true + } + } + } + }, + { + "if": { + "properties": { + "type": { + "const": "Null" + } + } + }, + "then": { + "not": { + "required": [ + "value" + ] + } + } + }, + { + "if": { + "properties": { + "type": { + "const": "String" + } + } + }, + "then": { + "properties": { + "value": { + "type": "string" + } + }, + "required": [ + "value" + ] + } + }, + { + "if": { + "properties": { + "type": { + "const": "bool" + } + } + }, + "then": { + "properties": { + "value": { + "type": "boolean" + } + }, + "required": [ + "value" + ] + } + }, + { + "if": { + "properties": { + "type": { + "const": "int" + } + } + }, + "then": { + "properties": { + "value": { + "type": "integer" + } + }, + "required": [ + "value" + ] + } + }, + { + "if": { + "properties": { + "type": { + "const": "list" + } + } + }, + "then": { + "properties": { + "value": { + "type": "array" + } + }, + "required": [ + "value" + ] + } + }, + { + "if": { + "properties": { + "type": { + "const": "map" + } + } + }, + "then": { + "properties": { + "value": { + "type": "object", + "additionalProperties": true + } + }, + "required": [ + "value" + ] + } + } + ] + }, + "Identifier": { + "type": "object", + "properties": { + "name": { + "type": "string" + }, + "scope": { + "type": "string" + }, + "uri": { + "type": "string" + } + }, + "required": [ + "name", + "uri" + ] + }, + "Instance": { + "type": "object", + "properties": { + "@": { + "type": "integer" + }, + "constant_index": { + "type": "integer" + }, + "loading_unit": { + "type": "string" + } + }, + "required": [ + "constant_index", + "loading_unit" + ] + }, + "Location": { + "type": "object", + "properties": { + "column": { + "type": "integer" + }, + "line": { + "type": "integer" + }, + "uri": { + "type": "string" + } + }, + "required": [ + "uri" + ] + }, + "RecordedUses": { + "type": "object", + "properties": { + "constants": { + "type": "array", + "items": { + "$ref": "#/definitions/Constant" + } + }, + "locations": { + "type": "array", + "items": { + "$ref": "#/definitions/Location" + } + }, + "metadata": { + "type": "object", + "properties": { + "comment": { + "type": "string" + }, + "version": { + "type": "string" + } + }, + "required": [ + "comment", + "version" + ] + }, + "recordings": { + "type": "array", + "items": { + "$ref": "#/definitions/Recording" + } + } + }, + "required": [ + "metadata" + ] + }, + "Recording": { + "type": "object", + "properties": { + "calls": { + "type": "array", + "items": { + "$ref": "#/definitions/Call" + } + }, + "definition": { + "type": "object", + "properties": { + "identifier": { + "$ref": "#/definitions/Identifier" + }, + "loading_unit": { + "type": "string" + } + }, + "required": [ + "identifier" + ] + }, + "instances": { + "type": "array", + "items": { + "$ref": "#/definitions/Instance" + } + } + }, + "required": [ + "definition" + ] + } + } +} diff --git a/pkgs/record_use/lib/record_use.dart b/pkgs/record_use/lib/record_use.dart index 8ef2d7e7c3..0bfb7fa750 100644 --- a/pkgs/record_use/lib/record_use.dart +++ b/pkgs/record_use/lib/record_use.dart @@ -3,6 +3,6 @@ // BSD-style license that can be found in the LICENSE file. export 'src/identifier.dart' show Identifier; -export 'src/metadata.dart' show Metadata, MetadataExt; +export 'src/metadata.dart' show Metadata; export 'src/record_use.dart' show ConstantInstance, RecordedUsages; export 'src/recorded_usage_from_file.dart' show parseFromFile; diff --git a/pkgs/record_use/lib/record_use_internal.dart b/pkgs/record_use/lib/record_use_internal.dart index cbb9a75d44..06a086c298 100644 --- a/pkgs/record_use/lib/record_use_internal.dart +++ b/pkgs/record_use/lib/record_use_internal.dart @@ -16,7 +16,7 @@ export 'src/constant.dart' export 'src/definition.dart' show Definition; export 'src/identifier.dart' show Identifier; export 'src/location.dart' show Location; -export 'src/metadata.dart' show Metadata, MetadataExt; +export 'src/metadata.dart' show Metadata; export 'src/record_use.dart' show RecordedUsages; export 'src/recordings.dart' show FlattenConstantsExtension, MapifyIterableExtension, Recordings; diff --git a/pkgs/record_use/lib/src/constant.dart b/pkgs/record_use/lib/src/constant.dart index 6417949eff..0f6d2ad24f 100644 --- a/pkgs/record_use/lib/src/constant.dart +++ b/pkgs/record_use/lib/src/constant.dart @@ -3,9 +3,7 @@ // BSD-style license that can be found in the LICENSE file. import 'helper.dart'; - -const _typeKey = 'type'; -const _valueKey = 'value'; +import 'syntax.g.dart'; /// A constant value that can be recorded and serialized. /// @@ -22,7 +20,11 @@ sealed class Constant { /// /// [constants] needs to be passed, as the [Constant]s are normalized and /// stored separately in the JSON. - Map toJson(Map constants); + Map toJson(Map constants) => + _toSyntax(constants).json; + + /// Converts this [Constant] object to a syntax representation. + ConstantSyntax _toSyntax(Map constants); /// Converts this [Constant] to the value it represents. Object? toValue() => switch (this) { @@ -44,44 +46,40 @@ sealed class Constant { static Constant fromJson( Map value, List constants, - ) => switch (value[_typeKey] as String) { - NullConstant._type => const NullConstant(), - BoolConstant._type => BoolConstant(value[_valueKey] as bool), - IntConstant._type => IntConstant(value[_valueKey] as int), - StringConstant._type => StringConstant(value[_valueKey] as String), - ListConstant._type => ListConstant( - (value[_valueKey] as List) - .map((value) => value as int) - .map((value) => constants[value]) - .toList(), + ) => _fromSyntax(ConstantSyntax.fromJson(value), constants); + + /// Creates a [Constant] object from its syntax representation. + static Constant _fromSyntax( + ConstantSyntax syntax, + List constants, + ) => switch (syntax) { + NullConstantSyntax() => const NullConstant(), + BoolConstantSyntax(:final value) => BoolConstant(value), + IntConstantSyntax(:final value) => IntConstant(value), + StringConstantSyntax(:final value) => StringConstant(value), + ListConstantSyntax(:final value) => ListConstant( + value!.cast().map((i) => constants[i]).toList(), ), - MapConstant._type => MapConstant( - (value[_valueKey] as Map).map( - (key, value) => MapEntry(key, constants[value as int]), - ), + MapConstantSyntax(:final value) => MapConstant( + value.json.map((key, value) => MapEntry(key, constants[value as int])), ), - InstanceConstant._type => InstanceConstant( - fields: (value[_valueKey] as Map? ?? {}).map( + InstanceConstantSyntax(value: final value) => InstanceConstant( + fields: (value?.json ?? {}).map( (key, value) => MapEntry(key, constants[value as int]), ), ), - String() => throw UnimplementedError( - 'This type is not a supported constant', - ), + _ => throw UnimplementedError('This type is not a supported constant'), }; } /// Represents the `null` constant value. final class NullConstant extends Constant { - /// The type identifier for JSON serialization. - static const _type = 'Null'; - /// Creates a [NullConstant] object. const NullConstant() : super(); @override - Map toJson(Map constants) => - _toJson(_type, null); + NullConstantSyntax _toSyntax(Map constants) => + NullConstantSyntax(); @override bool operator ==(Object other) => other is NullConstant; @@ -107,56 +105,41 @@ sealed class PrimitiveConstant extends Constant { return other is PrimitiveConstant && other.value == value; } - - @override - Map toJson(Map constants) => valueToJson(); - - /// Converts this primitive constant to a JSON representation. - Map valueToJson(); } /// Represents a constant boolean value. final class BoolConstant extends PrimitiveConstant { - /// The type identifier for JSON serialization. - static const _type = 'bool'; - /// Creates a [BoolConstant] object with the given boolean [value]. // ignore: avoid_positional_boolean_parameters const BoolConstant(super.value); @override - Map valueToJson() => _toJson(_type, value); + BoolConstantSyntax _toSyntax(Map constants) => + BoolConstantSyntax(value: value); } /// Represents a constant integer value. final class IntConstant extends PrimitiveConstant { - /// The type identifier for JSON serialization. - static const _type = 'int'; - /// Creates an [IntConstant] object with the given integer [value]. const IntConstant(super.value); @override - Map valueToJson() => _toJson(_type, value); + IntConstantSyntax _toSyntax(Map constants) => + IntConstantSyntax(value: value); } /// Represents a constant string value. final class StringConstant extends PrimitiveConstant { - /// The type identifier for JSON serialization. - static const _type = 'String'; - /// Creates a [StringConstant] object with the given string [value]. const StringConstant(super.value); @override - Map valueToJson() => _toJson(_type, value); + StringConstantSyntax _toSyntax(Map constants) => + StringConstantSyntax(value: value); } /// Represents a constant list of [Constant] values. final class ListConstant extends Constant { - /// The type identifier for JSON serialization. - static const _type = 'list'; - /// The underlying list of constant values. final List value; @@ -174,15 +157,14 @@ final class ListConstant extends Constant { } @override - Map toJson(Map constants) => - _toJson(_type, value.map((constant) => constants[constant]).toList()); + ListConstantSyntax _toSyntax(Map constants) => + ListConstantSyntax( + value: value.map((constant) => constants[constant]).toList(), + ); } /// Represents a constant map from string keys to [Constant] values. final class MapConstant extends Constant { - /// The type identifier for JSON serialization. - static const _type = 'map'; - /// The underlying map of constant values. final Map value; @@ -200,10 +182,12 @@ final class MapConstant extends Constant { } @override - Map toJson(Map constants) => _toJson( - _type, - value.map((key, constant) => MapEntry(key, constants[constant]!)), - ); + MapConstantSyntax _toSyntax(Map constants) => + MapConstantSyntax( + value: JsonObjectSyntax.fromJson( + value.map((key, constant) => MapEntry(key, constants[constant]!)), + ), + ); } /// A constant instance of a class with its fields @@ -211,38 +195,23 @@ final class MapConstant extends Constant { /// Only as far as they can also be represented by constants. This is more or /// less the same as a [MapConstant]. final class InstanceConstant extends Constant { - /// The type identifier for JSON serialization. - static const _type = 'Instance'; - /// The fields of this instance, mapped from field name to [Constant] value. final Map fields; /// Creates an [InstanceConstant] object with the given [fields]. const InstanceConstant({required this.fields}); - /// Creates an [InstanceConstant] object from JSON. - /// - /// [json] is a map representing the JSON structure. - /// [constants] is a list of [Constant] objects that are referenced by index - /// in the JSON. - factory InstanceConstant.fromJson( - Map json, - List constants, - ) => InstanceConstant( - fields: json.map( - (key, constantIndex) => MapEntry(key, constants[constantIndex as int]), - ), - ); - @override - Map toJson(Map constants) => _toJson( - _type, - fields.isNotEmpty - ? fields.map( - (name, constantIndex) => MapEntry(name, constants[constantIndex]!), - ) - : null, - ); + InstanceConstantSyntax _toSyntax(Map constants) => + InstanceConstantSyntax( + value: fields.isNotEmpty + ? JsonObjectSyntax.fromJson( + fields.map( + (name, constant) => MapEntry(name, constants[constant]!), + ), + ) + : null, + ); @override bool operator ==(Object other) { @@ -255,9 +224,13 @@ final class InstanceConstant extends Constant { int get hashCode => deepHash(fields); } -/// Helper to create the JSON structure of constants by storing the value with -/// the type. -Map _toJson(String type, Object? value) => { - _typeKey: type, - if (value != null) _valueKey: value, -}; +/// Package private (protected) methods for [Constant]. +/// +/// This avoids bloating the public API and public API docs and prevents +/// internal types from leaking from the API. +extension ConstantProtected on Constant { + ConstantSyntax toSyntax(Map constants) => _toSyntax(constants); + + static Constant fromSyntax(ConstantSyntax syntax, List constants) => + Constant._fromSyntax(syntax, constants); +} diff --git a/pkgs/record_use/lib/src/definition.dart b/pkgs/record_use/lib/src/definition.dart index bbc14e2a55..9fd6319c99 100644 --- a/pkgs/record_use/lib/src/definition.dart +++ b/pkgs/record_use/lib/src/definition.dart @@ -2,7 +2,8 @@ // 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 'identifier.dart' show Identifier; +import 'identifier.dart'; +import 'syntax.g.dart'; /// A definition is an [identifier] with its [loadingUnit]. class Definition { @@ -11,20 +12,20 @@ class Definition { const Definition({required this.identifier, this.loadingUnit}); - static const _identifierKey = 'identifier'; - static const _loadingUnitKey = 'loading_unit'; + factory Definition.fromJson(Map json) => + Definition._fromSyntax(DefinitionSyntax.fromJson(json)); - factory Definition.fromJson(Map json) => Definition( - identifier: Identifier.fromJson( - json[_identifierKey] as Map, - ), - loadingUnit: json[_loadingUnitKey] as String?, + factory Definition._fromSyntax(DefinitionSyntax syntax) => Definition( + identifier: IdentifierProtected.fromSyntax(syntax.identifier), + loadingUnit: syntax.loadingUnit, ); - Map toJson() => { - _identifierKey: identifier.toJson(), - if (loadingUnit != null) _loadingUnitKey: loadingUnit, - }; + Map toJson() => _toSyntax().json; + + DefinitionSyntax _toSyntax() => DefinitionSyntax( + identifier: identifier.toSyntax(), + loadingUnit: loadingUnit, + ); @override bool operator ==(Object other) { @@ -38,3 +39,14 @@ class Definition { @override int get hashCode => Object.hash(identifier, loadingUnit); } + +/// Package private (protected) methods for [Definition]. +/// +/// This avoids bloating the public API and public API docs and prevents +/// internal types from leaking from the API. +extension DefinitionProtected on Definition { + DefinitionSyntax toSyntax() => _toSyntax(); + + static Definition fromSyntax(DefinitionSyntax syntax) => + Definition._fromSyntax(syntax); +} diff --git a/pkgs/record_use/lib/src/identifier.dart b/pkgs/record_use/lib/src/identifier.dart index b50a730414..781b02c532 100644 --- a/pkgs/record_use/lib/src/identifier.dart +++ b/pkgs/record_use/lib/src/identifier.dart @@ -2,6 +2,8 @@ // 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 'syntax.g.dart'; + /// Represents a unique identifier for a code element, such as a class, method, /// or field, within a Dart program. /// @@ -35,23 +37,20 @@ class Identifier { /// [name] is the name of the element. const Identifier({required this.importUri, this.scope, required this.name}); - static const String _uriKey = 'uri'; - static const String _scopeKey = 'scope'; - static const String _nameKey = 'name'; - /// Creates an [Identifier] object from its JSON representation. - factory Identifier.fromJson(Map json) => Identifier( - importUri: json[_uriKey] as String, - scope: json[_scopeKey] as String?, - name: json[_nameKey] as String, - ); + factory Identifier.fromJson(Map json) => + Identifier._fromSyntax(IdentifierSyntax.fromJson(json)); + + /// Creates an [Identifier] object from its syntax representation. + factory Identifier._fromSyntax(IdentifierSyntax syntax) => + Identifier(importUri: syntax.uri, scope: syntax.scope, name: syntax.name); /// Converts this [Identifier] object to a JSON representation. - Map toJson() => { - _uriKey: importUri, - if (scope != null) _scopeKey: scope, - _nameKey: name, - }; + Map toJson() => _toSyntax().json; + + /// Converts this [Identifier] object to a syntax representation. + IdentifierSyntax _toSyntax() => + IdentifierSyntax(uri: importUri, scope: scope, name: name); @override bool operator ==(Object other) { @@ -66,3 +65,14 @@ class Identifier { @override int get hashCode => Object.hash(importUri, scope, name); } + +/// Package private (protected) methods for [Identifier]. +/// +/// This avoids bloating the public API and public API docs and prevents +/// internal types from leaking from the API. +extension IdentifierProtected on Identifier { + IdentifierSyntax toSyntax() => _toSyntax(); + + static Identifier fromSyntax(IdentifierSyntax syntax) => + Identifier._fromSyntax(syntax); +} diff --git a/pkgs/record_use/lib/src/location.dart b/pkgs/record_use/lib/src/location.dart index 1a3ee10e89..6760222a77 100644 --- a/pkgs/record_use/lib/src/location.dart +++ b/pkgs/record_use/lib/src/location.dart @@ -2,6 +2,8 @@ // 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 'syntax.g.dart'; + class Location { final String uri; final int? line; @@ -9,21 +11,16 @@ class Location { const Location({required this.uri, this.line, this.column}); - static const _uriKey = 'uri'; - static const _lineKey = 'line'; - static const _columnKey = 'column'; + factory Location.fromJson(Map map) => + Location._fromSyntax(LocationSyntax.fromJson(map)); + + factory Location._fromSyntax(LocationSyntax syntax) => + Location(uri: syntax.uri, line: syntax.line, column: syntax.column); - factory Location.fromJson(Map map) => Location( - uri: map[_uriKey] as String, - line: map[_lineKey] as int?, - column: map[_columnKey] as int?, - ); + Map toJson() => _toSyntax().json; - Map toJson() => { - _uriKey: uri, - if (line != null) _lineKey: line, - if (line != null) _columnKey: column, - }; + LocationSyntax _toSyntax() => + LocationSyntax(uri: uri, line: line, column: column); @override bool operator ==(Object other) { @@ -38,3 +35,14 @@ class Location { @override int get hashCode => Object.hash(uri, line, column); } + +/// Package private (protected) methods for [Location]. +/// +/// This avoids bloating the public API and public API docs and prevents +/// internal types from leaking from the API. +extension LocationProtected on Location { + LocationSyntax toSyntax() => _toSyntax(); + + static Location fromSyntax(LocationSyntax syntax) => + Location._fromSyntax(syntax); +} diff --git a/pkgs/record_use/lib/src/metadata.dart b/pkgs/record_use/lib/src/metadata.dart index 819b614f22..330622b529 100644 --- a/pkgs/record_use/lib/src/metadata.dart +++ b/pkgs/record_use/lib/src/metadata.dart @@ -5,6 +5,7 @@ import 'package:pub_semver/pub_semver.dart'; import 'helper.dart'; +import 'syntax.g.dart'; /// Metadata attached to a recorded usages file. /// @@ -12,17 +13,24 @@ import 'helper.dart'; /// applied to not include non-deterministic or dynamic data such as timestamps, /// as this would mess with the usage recording caching. class Metadata { + final MetadataSyntax _syntax; + + const Metadata._(this._syntax); + + factory Metadata.fromJson(Map json) => + Metadata._(MetadataSyntax.fromJson(json)); + /// The underlying data. /// - /// Together with the metadata extension [MetadataExt], this makes the - /// metadata extensible by the user implementing the recording. For example, - /// dart2js might want to store different metadata than the Dart VM. - final Map json; + /// This makes the metadata extensible by the user implementing the recording. + /// For example, dart2js might want to store different metadata than the Dart + /// VM. + Map get json => _syntax.json; - const Metadata._({required this.json}); + Map toJson() => _syntax.json; - factory Metadata.fromJson(Map json) => - Metadata._(json: json); + Version get version => Version.parse(_syntax.version); + String get comment => _syntax.comment; @override bool operator ==(covariant Metadata other) { @@ -35,7 +43,12 @@ class Metadata { int get hashCode => deepHash(json); } -extension MetadataExt on Metadata { - Version get version => Version.parse(json['version'] as String); - String get comment => json['comment'] as String; +/// Package private (protected) methods for [Metadata]. +/// +/// This avoids bloating the public API and public API docs and prevents +/// internal types from leaking from the API. +extension MetadataProtected on Metadata { + MetadataSyntax toSyntax() => _syntax; + + static Metadata fromSyntax(MetadataSyntax syntax) => Metadata._(syntax); } diff --git a/pkgs/record_use/lib/src/recordings.dart b/pkgs/record_use/lib/src/recordings.dart index 2390a77289..88416c9b76 100644 --- a/pkgs/record_use/lib/src/recordings.dart +++ b/pkgs/record_use/lib/src/recordings.dart @@ -9,9 +9,10 @@ import 'constant.dart'; import 'definition.dart'; import 'helper.dart'; import 'identifier.dart'; -import 'location.dart' show Location; +import 'location.dart'; import 'metadata.dart'; import 'reference.dart'; +import 'syntax.g.dart'; /// [Recordings] combines recordings of calls and instances with metadata. /// @@ -41,14 +42,6 @@ class Recordings { (definition, instances) => MapEntry(definition.identifier, instances), ); - static const _metadataKey = 'metadata'; - static const _constantsKey = 'constants'; - static const _locationsKey = 'locations'; - static const _recordingsKey = 'recordings'; - static const _callsKey = 'calls'; - static const _instancesKey = 'instances'; - static const _definitionKey = 'definition'; - Recordings({ required this.metadata, required this.callsForDefinition, @@ -61,82 +54,82 @@ class Recordings { /// efficiency. Identifiers and constants are stored in separate tables, /// allowing them to be referenced by index in the `recordings` map. factory Recordings.fromJson(Map json) { - if (json case { - _constantsKey: final List? constantJsons, - _locationsKey: final List? locationJsons, - _recordingsKey: final List? recordingJsons, - }) { - final constants = []; - for (final constantJsonObj in constantJsons ?? []) { - final constantJson = constantJsonObj as Map; - final constant = Constant.fromJson(constantJson, constants); - if (!constants.contains(constant)) { - constants.add(constant); - } - } - final locations = []; - for (final locationJsonObj in locationJsons ?? []) { - final locationJson = locationJsonObj as Map; - final location = Location.fromJson(locationJson); - if (!locations.contains(location)) { - locations.add(location); - } + try { + final syntax = RecordedUsesSyntax.fromJson(json); + return Recordings._fromSyntax(syntax); + } on FormatException catch (e) { + throw ArgumentError(''' +Invalid JSON format for Recordings: +${const JsonEncoder.withIndent(' ').convert(json)} +Error: $e +'''); + } + } + + factory Recordings._fromSyntax(RecordedUsesSyntax syntax) { + final constants = []; + for (final constantSyntax in syntax.constants ?? []) { + final constant = ConstantProtected.fromSyntax(constantSyntax, constants); + if (!constants.contains(constant)) { + constants.add(constant); } + } - final recordings = - recordingJsons?.whereType>() ?? []; + final locations = []; + for (final locationSyntax in syntax.locations ?? []) { + final location = LocationProtected.fromSyntax(locationSyntax); + if (!locations.contains(location)) { + locations.add(location); + } + } - final recordedCalls = recordings.where( - (recording) => recording[_callsKey] != null, - ); - final recordedInstances = recordings.where( - (recording) => recording[_instancesKey] != null, - ); + final callsForDefinition = >{}; + final instancesForDefinition = >{}; - return Recordings( - metadata: Metadata.fromJson(json[_metadataKey] as Map), - callsForDefinition: { - for (final recording in recordedCalls) - Definition.fromJson( - recording[_definitionKey] as Map, - ): (recording[_callsKey] as List) - .map( - (json) => CallReference.fromJson( - json as Map, - constants, - locations, - ), - ) - .toList(), - }, - instancesForDefinition: { - for (final recording in recordedInstances) - Definition.fromJson( - recording[_definitionKey] as Map, - ): (recording[_instancesKey] as List) - .map( - (json) => InstanceReference.fromJson( - json as Map, - constants, - locations, - ), - ) - .toList(), - }, + for (final recordingSyntax in syntax.recordings ?? []) { + final definition = DefinitionProtected.fromSyntax( + recordingSyntax.definition, ); - } else { - throw ArgumentError(''' -Invalid JSON format for Recordings: -${const JsonEncoder.withIndent(' ').convert(json)} -'''); + if (recordingSyntax.calls case final callSyntaxes?) { + final callReferences = callSyntaxes + .map( + (callSyntax) => CallReferenceProtected.fromSyntax( + callSyntax, + constants, + locations, + ), + ) + .toList(); + callsForDefinition[definition] = callReferences; + } + if (recordingSyntax.instances case final instanceSyntaxes?) { + final instanceReferences = instanceSyntaxes + .map( + (instanceSyntax) => InstanceReferenceProtected.fromSyntax( + instanceSyntax, + constants, + locations, + ), + ) + .toList(); + instancesForDefinition[definition] = instanceReferences; + } } + + return Recordings( + metadata: MetadataProtected.fromSyntax(syntax.metadata), + callsForDefinition: callsForDefinition, + instancesForDefinition: instancesForDefinition, + ); } /// Encodes this object into a JSON representation. /// /// This method normalizes identifiers and constants for storage efficiency. - Map toJson() { - final constants = { + Map toJson() => _toSyntax().json; + + RecordedUsesSyntax _toSyntax() { + final constantsIndex = { ...callsForDefinition.values .expand((calls) => calls) .whereType() @@ -156,7 +149,8 @@ ${const JsonEncoder.withIndent(' ').convert(json)} }, ), }.flatten().asMapToIndices; - final locations = { + + final locationsIndex = { ...callsForDefinition.values .expand((calls) => calls) .map((call) => call.location) @@ -166,38 +160,48 @@ ${const JsonEncoder.withIndent(' ').convert(json)} .map((instance) => instance.location) .nonNulls, }.asMapToIndices; - return { - _metadataKey: metadata.json, - if (constants.isNotEmpty) - _constantsKey: constants.keys - .map((constant) => constant.toJson(constants)) - .toList(), - if (locations.isNotEmpty) - _locationsKey: locations.keys - .map((location) => location.toJson()) - .toList(), - if (callsForDefinition.isNotEmpty || instancesForDefinition.isNotEmpty) - _recordingsKey: [ - if (callsForDefinition.isNotEmpty) - ...callsForDefinition.entries.map( - (entry) => { - _definitionKey: entry.key.toJson(), - _callsKey: entry.value - .map((call) => call.toJson(constants, locations)) - .toList(), - }, - ), - if (instancesForDefinition.isNotEmpty) - ...instancesForDefinition.entries.map( - (entry) => { - _definitionKey: entry.key.toJson(), - _instancesKey: entry.value - .map((instance) => instance.toJson(constants, locations)) - .toList(), - }, - ), - ], - }; + + final recordings = []; + if (callsForDefinition.isNotEmpty) { + recordings.addAll( + callsForDefinition.entries.map( + (entry) => RecordingSyntax( + definition: entry.key.toSyntax(), + calls: entry.value + .map((call) => call.toSyntax(constantsIndex, locationsIndex)) + .toList(), + ), + ), + ); + } + if (instancesForDefinition.isNotEmpty) { + recordings.addAll( + instancesForDefinition.entries.map( + (entry) => RecordingSyntax( + definition: entry.key.toSyntax(), + instances: entry.value + .map( + (instance) => + instance.toSyntax(constantsIndex, locationsIndex), + ) + .toList(), + ), + ), + ); + } + + return RecordedUsesSyntax( + metadata: metadata.toSyntax(), + constants: constantsIndex.isEmpty + ? null + : constantsIndex.keys + .map((constant) => constant.toSyntax(constantsIndex)) + .toList(), + locations: locationsIndex.isEmpty + ? null + : locationsIndex.keys.map((location) => location.toSyntax()).toList(), + recordings: recordings.isEmpty ? null : recordings, + ); } @override diff --git a/pkgs/record_use/lib/src/reference.dart b/pkgs/record_use/lib/src/reference.dart index 44778de208..277e29ba4e 100644 --- a/pkgs/record_use/lib/src/reference.dart +++ b/pkgs/record_use/lib/src/reference.dart @@ -6,8 +6,7 @@ import 'constant.dart'; import 'helper.dart'; import 'identifier.dart'; import 'location.dart' show Location; - -const _loadingUnitKey = 'loading_unit'; +import 'syntax.g.dart'; /// A reference to *something*. /// @@ -37,13 +36,13 @@ sealed class Reference { Map toJson( Map constants, Map locations, - ) => {_loadingUnitKey: loadingUnit, _locationKey: locations[location]}; -} + ) => _toSyntax(constants, locations).json; -const _locationKey = '@'; -const _positionalKey = 'positional'; -const _namedKey = 'named'; -const _typeKey = 'type'; + JsonObjectSyntax _toSyntax( + Map constants, + Map locations, + ); +} /// A reference to a call to some [Identifier]. /// @@ -56,32 +55,47 @@ sealed class CallReference extends Reference { Map json, List constants, List locations, + ) => _fromSyntax(CallSyntax.fromJson(json), constants, locations); + + static CallReference _fromSyntax( + CallSyntax syntax, + List constants, + List locations, ) { - final loadingUnit = json[_loadingUnitKey] as String?; - final locationIndex = json[_locationKey] as int?; + final locationIndex = syntax.at; final location = locationIndex == null ? null : locations[locationIndex]; - return json[_typeKey] == 'tearoff' - ? CallTearOff(loadingUnit: loadingUnit, location: location) - : CallWithArguments( - positionalArguments: (json[_positionalKey] as List? ?? []) - .whereType() - .map( - (constantsIndex) => - constantsIndex != null ? constants[constantsIndex] : null, - ) - .toList(), - namedArguments: (json[_namedKey] as Map? ?? {}) - .map((key, value) => MapEntry(key, value as int?)) - .map( - (name, constantsIndex) => MapEntry( - name, + return switch (syntax) { + TearoffCallSyntax() => CallTearOff( + loadingUnit: syntax.loadingUnit, + location: location, + ), + WithArgumentsCallSyntax( + :final named, + :final positional, + :final loadingUnit, + ) => + CallWithArguments( + positionalArguments: (positional ?? []) + .map( + (constantsIndex) => constantsIndex != null ? constants[constantsIndex] : null, - ), - ), - loadingUnit: loadingUnit, - location: location, - ); + ) + .toList(), + namedArguments: (named ?? {}).map( + (name, constantsIndex) => MapEntry(name, constants[constantsIndex]), + ), + loadingUnit: loadingUnit, + location: location, + ), + _ => throw UnimplementedError('Unknown CallSyntax type'), + }; } + + @override + CallSyntax _toSyntax( + Map constants, + Map locations, + ); } /// A reference to a call to some [Identifier] with [positionalArguments] and @@ -98,22 +112,28 @@ final class CallWithArguments extends CallReference { }); @override - Map toJson( + WithArgumentsCallSyntax _toSyntax( Map constants, Map locations, ) { - final positionalJson = positionalArguments - .map((constant) => constants[constant]) - .toList(); - final namedJson = namedArguments.map( - (name, constant) => MapEntry(name, constants[constant]), + final namedArgs = {}; + for (final entry in namedArguments.entries) { + if (entry.value != null) { + final index = constants[entry.value!]; + if (index != null) { + namedArgs[entry.key] = index; + } + } + } + + return WithArgumentsCallSyntax( + at: locations[location]!, + loadingUnit: loadingUnit!, + named: namedArgs.isNotEmpty ? namedArgs : null, + positional: positionalArguments.isEmpty + ? null + : positionalArguments.map((constant) => constants[constant]).toList(), ); - return { - _typeKey: 'with_arguments', - if (positionalJson.isNotEmpty) _positionalKey: positionalJson, - if (namedJson.isNotEmpty) _namedKey: namedJson, - ...super.toJson(constants, locations), - }; } @override @@ -140,10 +160,10 @@ final class CallTearOff extends CallReference { const CallTearOff({required super.loadingUnit, required super.location}); @override - Map toJson( + TearoffCallSyntax _toSyntax( Map constants, Map locations, - ) => {_typeKey: 'tearoff', ...super.toJson(constants, locations)}; + ) => TearoffCallSyntax(at: locations[location]!, loadingUnit: loadingUnit!); } final class InstanceReference extends Reference { @@ -155,30 +175,35 @@ final class InstanceReference extends Reference { required super.location, }); - static const _constantKey = 'constant_index'; - factory InstanceReference.fromJson( Map json, List constants, List locations, + ) => _fromSyntax(InstanceSyntax.fromJson(json), constants, locations); + + static InstanceReference _fromSyntax( + InstanceSyntax syntax, + List constants, + List locations, ) { - final locationIndex = json[_locationKey] as int?; + final locationIndex = syntax.at; + final location = locationIndex == null ? null : locations[locationIndex]; return InstanceReference( - instanceConstant: - constants[json[_constantKey] as int] as InstanceConstant, - loadingUnit: json[_loadingUnitKey] as String?, - location: locationIndex == null ? null : locations[locationIndex], + instanceConstant: constants[syntax.constantIndex] as InstanceConstant, + loadingUnit: syntax.loadingUnit, + location: location, ); } @override - Map toJson( + InstanceSyntax _toSyntax( Map constants, Map locations, - ) => { - _constantKey: constants[instanceConstant]!, - ...super.toJson(constants, locations), - }; + ) => InstanceSyntax( + at: locations[location]!, + constantIndex: constants[instanceConstant]!, + loadingUnit: loadingUnit!, + ); @override bool operator ==(Object other) { @@ -192,3 +217,37 @@ final class InstanceReference extends Reference { @override int get hashCode => Object.hash(instanceConstant, super.hashCode); } + +/// Package private (protected) methods for [CallReference]. +/// +/// This avoids bloating the public API and public API docs and prevents +/// internal types from leaking from the API. +extension CallReferenceProtected on CallReference { + CallSyntax toSyntax( + Map constants, + Map locations, + ) => _toSyntax(constants, locations); + + static CallReference fromSyntax( + CallSyntax syntax, + List constants, + List locations, + ) => CallReference._fromSyntax(syntax, constants, locations); +} + +/// Package private (protected) methods for [InstanceReference]. +/// +/// This avoids bloating the public API and public API docs and prevents +/// internal types from leaking from the API. +extension InstanceReferenceProtected on InstanceReference { + InstanceSyntax toSyntax( + Map constants, + Map locations, + ) => _toSyntax(constants, locations); + + static InstanceReference fromSyntax( + InstanceSyntax syntax, + List constants, + List locations, + ) => InstanceReference._fromSyntax(syntax, constants, locations); +} diff --git a/pkgs/record_use/lib/src/syntax.g.dart b/pkgs/record_use/lib/src/syntax.g.dart new file mode 100644 index 0000000000..c64448c070 --- /dev/null +++ b/pkgs/record_use/lib/src/syntax.g.dart @@ -0,0 +1,1400 @@ +// Copyright (c) 2025, 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. + +// This file is generated, do not edit. +// File generated by pkg/record_use/tool/generate_syntax.dart. + +// ignore_for_file: unused_element, public_member_api_docs + +import 'dart:io'; + +class BoolConstantSyntax extends ConstantSyntax { + BoolConstantSyntax.fromJson(super.json, {super.path}) : super._fromJson(); + + BoolConstantSyntax({required bool value, super.path = const []}) + : super(type: 'bool') { + _value = value; + json.sortOnKey(); + } + + /// Setup all fields for [BoolConstantSyntax] that are not in + /// [ConstantSyntax]. + void setup({required bool value}) { + _value = value; + json.sortOnKey(); + } + + bool get value => _reader.get('value'); + + set _value(bool value) { + json.setOrRemove('value', value); + } + + List _validateValue() => _reader.validate('value'); + + @override + List validate() => [...super.validate(), ..._validateValue()]; + + @override + String toString() => 'BoolConstantSyntax($json)'; +} + +extension BoolConstantSyntaxExtension on ConstantSyntax { + bool get isBoolConstant => type == 'bool'; + + BoolConstantSyntax get asBoolConstant => + BoolConstantSyntax.fromJson(json, path: path); +} + +class CallSyntax extends JsonObjectSyntax { + factory CallSyntax.fromJson( + Map json, { + List path = const [], + }) { + final result = CallSyntax._fromJson(json, path: path); + if (result.isTearoffCall) { + return result.asTearoffCall; + } + if (result.isWithArgumentsCall) { + return result.asWithArgumentsCall; + } + return result; + } + + CallSyntax._fromJson(super.json, {super.path = const []}) : super.fromJson(); + + CallSyntax({ + int? at, + required String loadingUnit, + required String type, + super.path = const [], + }) : super() { + _at = at; + _loadingUnit = loadingUnit; + _type = type; + json.sortOnKey(); + } + + int? get at => _reader.get('@'); + + set _at(int? value) { + json.setOrRemove('@', value); + } + + List _validateAt() => _reader.validate('@'); + + String get loadingUnit => _reader.get('loading_unit'); + + set _loadingUnit(String value) { + json.setOrRemove('loading_unit', value); + } + + List _validateLoadingUnit() => + _reader.validate('loading_unit'); + + String get type => _reader.get('type'); + + set _type(String value) { + json.setOrRemove('type', value); + } + + List _validateType() => _reader.validate('type'); + + @override + List validate() => [ + ...super.validate(), + ..._validateAt(), + ..._validateLoadingUnit(), + ..._validateType(), + ]; + + @override + String toString() => 'CallSyntax($json)'; +} + +class ConstantSyntax extends JsonObjectSyntax { + factory ConstantSyntax.fromJson( + Map json, { + List path = const [], + }) { + final result = ConstantSyntax._fromJson(json, path: path); + if (result.isInstanceConstant) { + return result.asInstanceConstant; + } + if (result.isNullConstant) { + return result.asNullConstant; + } + if (result.isStringConstant) { + return result.asStringConstant; + } + if (result.isBoolConstant) { + return result.asBoolConstant; + } + if (result.isIntConstant) { + return result.asIntConstant; + } + if (result.isListConstant) { + return result.asListConstant; + } + if (result.isMapConstant) { + return result.asMapConstant; + } + return result; + } + + ConstantSyntax._fromJson(super.json, {super.path = const []}) + : super.fromJson(); + + ConstantSyntax({required String type, super.path = const []}) : super() { + _type = type; + json.sortOnKey(); + } + + String get type => _reader.get('type'); + + set _type(String value) { + json.setOrRemove('type', value); + } + + List _validateType() => _reader.validate('type'); + + @override + List validate() => [...super.validate(), ..._validateType()]; + + @override + String toString() => 'ConstantSyntax($json)'; +} + +class DefinitionSyntax extends JsonObjectSyntax { + DefinitionSyntax.fromJson(super.json, {super.path = const []}) + : super.fromJson(); + + DefinitionSyntax({ + required IdentifierSyntax identifier, + String? loadingUnit, + super.path = const [], + }) : super() { + _identifier = identifier; + _loadingUnit = loadingUnit; + json.sortOnKey(); + } + + IdentifierSyntax get identifier { + final jsonValue = _reader.map$('identifier'); + return IdentifierSyntax.fromJson(jsonValue, path: [...path, 'identifier']); + } + + set _identifier(IdentifierSyntax value) { + json['identifier'] = value.json; + } + + List _validateIdentifier() { + final mapErrors = _reader.validate>('identifier'); + if (mapErrors.isNotEmpty) { + return mapErrors; + } + return identifier.validate(); + } + + String? get loadingUnit => _reader.get('loading_unit'); + + set _loadingUnit(String? value) { + json.setOrRemove('loading_unit', value); + } + + List _validateLoadingUnit() => + _reader.validate('loading_unit'); + + @override + List validate() => [ + ...super.validate(), + ..._validateIdentifier(), + ..._validateLoadingUnit(), + ]; + + @override + String toString() => 'DefinitionSyntax($json)'; +} + +class IdentifierSyntax extends JsonObjectSyntax { + IdentifierSyntax.fromJson(super.json, {super.path = const []}) + : super.fromJson(); + + IdentifierSyntax({ + required String name, + String? scope, + required String uri, + super.path = const [], + }) : super() { + _name = name; + _scope = scope; + _uri = uri; + json.sortOnKey(); + } + + String get name => _reader.get('name'); + + set _name(String value) { + json.setOrRemove('name', value); + } + + List _validateName() => _reader.validate('name'); + + String? get scope => _reader.get('scope'); + + set _scope(String? value) { + json.setOrRemove('scope', value); + } + + List _validateScope() => _reader.validate('scope'); + + String get uri => _reader.get('uri'); + + set _uri(String value) { + json.setOrRemove('uri', value); + } + + List _validateUri() => _reader.validate('uri'); + + @override + List validate() => [ + ...super.validate(), + ..._validateName(), + ..._validateScope(), + ..._validateUri(), + ]; + + @override + String toString() => 'IdentifierSyntax($json)'; +} + +class InstanceSyntax extends JsonObjectSyntax { + InstanceSyntax.fromJson(super.json, {super.path = const []}) + : super.fromJson(); + + InstanceSyntax({ + int? at, + required int constantIndex, + required String loadingUnit, + super.path = const [], + }) : super() { + _at = at; + _constantIndex = constantIndex; + _loadingUnit = loadingUnit; + json.sortOnKey(); + } + + int? get at => _reader.get('@'); + + set _at(int? value) { + json.setOrRemove('@', value); + } + + List _validateAt() => _reader.validate('@'); + + int get constantIndex => _reader.get('constant_index'); + + set _constantIndex(int value) { + json.setOrRemove('constant_index', value); + } + + List _validateConstantIndex() => + _reader.validate('constant_index'); + + String get loadingUnit => _reader.get('loading_unit'); + + set _loadingUnit(String value) { + json.setOrRemove('loading_unit', value); + } + + List _validateLoadingUnit() => + _reader.validate('loading_unit'); + + @override + List validate() => [ + ...super.validate(), + ..._validateAt(), + ..._validateConstantIndex(), + ..._validateLoadingUnit(), + ]; + + @override + String toString() => 'InstanceSyntax($json)'; +} + +class InstanceConstantSyntax extends ConstantSyntax { + InstanceConstantSyntax.fromJson(super.json, {super.path}) : super._fromJson(); + + InstanceConstantSyntax({JsonObjectSyntax? value, super.path = const []}) + : super(type: 'Instance') { + _value = value; + json.sortOnKey(); + } + + /// Setup all fields for [InstanceConstantSyntax] that are not in + /// [ConstantSyntax]. + void setup({required JsonObjectSyntax? value}) { + _value = value; + json.sortOnKey(); + } + + JsonObjectSyntax? get value { + final jsonValue = _reader.optionalMap('value'); + if (jsonValue == null) return null; + return JsonObjectSyntax.fromJson(jsonValue, path: [...path, 'value']); + } + + set _value(JsonObjectSyntax? value) { + json.setOrRemove('value', value?.json); + } + + List _validateValue() { + final mapErrors = _reader.validate?>('value'); + if (mapErrors.isNotEmpty) { + return mapErrors; + } + return value?.validate() ?? []; + } + + @override + List validate() => [...super.validate(), ..._validateValue()]; + + @override + String toString() => 'InstanceConstantSyntax($json)'; +} + +extension InstanceConstantSyntaxExtension on ConstantSyntax { + bool get isInstanceConstant => type == 'Instance'; + + InstanceConstantSyntax get asInstanceConstant => + InstanceConstantSyntax.fromJson(json, path: path); +} + +class IntConstantSyntax extends ConstantSyntax { + IntConstantSyntax.fromJson(super.json, {super.path}) : super._fromJson(); + + IntConstantSyntax({required int value, super.path = const []}) + : super(type: 'int') { + _value = value; + json.sortOnKey(); + } + + /// Setup all fields for [IntConstantSyntax] that are not in + /// [ConstantSyntax]. + void setup({required int value}) { + _value = value; + json.sortOnKey(); + } + + int get value => _reader.get('value'); + + set _value(int value) { + json.setOrRemove('value', value); + } + + List _validateValue() => _reader.validate('value'); + + @override + List validate() => [...super.validate(), ..._validateValue()]; + + @override + String toString() => 'IntConstantSyntax($json)'; +} + +extension IntConstantSyntaxExtension on ConstantSyntax { + bool get isIntConstant => type == 'int'; + + IntConstantSyntax get asIntConstant => + IntConstantSyntax.fromJson(json, path: path); +} + +class ListConstantSyntax extends ConstantSyntax { + ListConstantSyntax.fromJson(super.json, {super.path}) : super._fromJson(); + + ListConstantSyntax({List? value, super.path = const []}) + : super(type: 'list') { + _value = value; + json.sortOnKey(); + } + + /// Setup all fields for [ListConstantSyntax] that are not in + /// [ConstantSyntax]. + void setup({required List? value}) { + _value = value; + json.sortOnKey(); + } + + List? get value => _reader.optionalList('value'); + + set _value(List? value) { + json.setOrRemove('value', value); + } + + List _validateValue() => + _reader.validateOptionalList('value'); + + @override + List validate() => [...super.validate(), ..._validateValue()]; + + @override + String toString() => 'ListConstantSyntax($json)'; +} + +extension ListConstantSyntaxExtension on ConstantSyntax { + bool get isListConstant => type == 'list'; + + ListConstantSyntax get asListConstant => + ListConstantSyntax.fromJson(json, path: path); +} + +class LocationSyntax extends JsonObjectSyntax { + LocationSyntax.fromJson(super.json, {super.path = const []}) + : super.fromJson(); + + LocationSyntax({ + int? column, + int? line, + required String uri, + super.path = const [], + }) : super() { + _column = column; + _line = line; + _uri = uri; + json.sortOnKey(); + } + + int? get column => _reader.get('column'); + + set _column(int? value) { + json.setOrRemove('column', value); + } + + List _validateColumn() => _reader.validate('column'); + + int? get line => _reader.get('line'); + + set _line(int? value) { + json.setOrRemove('line', value); + } + + List _validateLine() => _reader.validate('line'); + + String get uri => _reader.get('uri'); + + set _uri(String value) { + json.setOrRemove('uri', value); + } + + List _validateUri() => _reader.validate('uri'); + + @override + List validate() => [ + ...super.validate(), + ..._validateColumn(), + ..._validateLine(), + ..._validateUri(), + ]; + + @override + String toString() => 'LocationSyntax($json)'; +} + +class MapConstantSyntax extends ConstantSyntax { + MapConstantSyntax.fromJson(super.json, {super.path}) : super._fromJson(); + + MapConstantSyntax({required JsonObjectSyntax value, super.path = const []}) + : super(type: 'map') { + _value = value; + json.sortOnKey(); + } + + /// Setup all fields for [MapConstantSyntax] that are not in + /// [ConstantSyntax]. + void setup({required JsonObjectSyntax value}) { + _value = value; + json.sortOnKey(); + } + + JsonObjectSyntax get value { + final jsonValue = _reader.map$('value'); + return JsonObjectSyntax.fromJson(jsonValue, path: [...path, 'value']); + } + + set _value(JsonObjectSyntax value) { + json['value'] = value.json; + } + + List _validateValue() { + final mapErrors = _reader.validate>('value'); + if (mapErrors.isNotEmpty) { + return mapErrors; + } + return value.validate(); + } + + @override + List validate() => [...super.validate(), ..._validateValue()]; + + @override + String toString() => 'MapConstantSyntax($json)'; +} + +extension MapConstantSyntaxExtension on ConstantSyntax { + bool get isMapConstant => type == 'map'; + + MapConstantSyntax get asMapConstant => + MapConstantSyntax.fromJson(json, path: path); +} + +class MetadataSyntax extends JsonObjectSyntax { + MetadataSyntax.fromJson(super.json, {super.path = const []}) + : super.fromJson(); + + MetadataSyntax({ + required String comment, + required String version, + super.path = const [], + }) : super() { + _comment = comment; + _version = version; + json.sortOnKey(); + } + + String get comment => _reader.get('comment'); + + set _comment(String value) { + json.setOrRemove('comment', value); + } + + List _validateComment() => _reader.validate('comment'); + + String get version => _reader.get('version'); + + set _version(String value) { + json.setOrRemove('version', value); + } + + List _validateVersion() => _reader.validate('version'); + + @override + List validate() => [ + ...super.validate(), + ..._validateComment(), + ..._validateVersion(), + ]; + + @override + String toString() => 'MetadataSyntax($json)'; +} + +class NullConstantSyntax extends ConstantSyntax { + NullConstantSyntax.fromJson(super.json, {super.path}) : super._fromJson(); + + NullConstantSyntax({super.path = const []}) : super(type: 'Null'); + + @override + List validate() => [...super.validate()]; + + @override + String toString() => 'NullConstantSyntax($json)'; +} + +extension NullConstantSyntaxExtension on ConstantSyntax { + bool get isNullConstant => type == 'Null'; + + NullConstantSyntax get asNullConstant => + NullConstantSyntax.fromJson(json, path: path); +} + +class RecordedUsesSyntax extends JsonObjectSyntax { + RecordedUsesSyntax.fromJson(super.json, {super.path = const []}) + : super.fromJson(); + + RecordedUsesSyntax({ + List? constants, + List? locations, + required MetadataSyntax metadata, + List? recordings, + super.path = const [], + }) : super() { + _constants = constants; + _locations = locations; + _metadata = metadata; + _recordings = recordings; + json.sortOnKey(); + } + + List? get constants { + final jsonValue = _reader.optionalList('constants'); + if (jsonValue == null) return null; + return [ + for (final (index, element) in jsonValue.indexed) + ConstantSyntax.fromJson( + element as Map, + path: [...path, 'constants', index], + ), + ]; + } + + set _constants(List? value) { + if (value == null) { + json.remove('constants'); + } else { + json['constants'] = [for (final item in value) item.json]; + } + } + + List _validateConstants() { + final listErrors = _reader.validateOptionalList>( + 'constants', + ); + if (listErrors.isNotEmpty) { + return listErrors; + } + final elements = constants; + if (elements == null) { + return []; + } + return [for (final element in elements) ...element.validate()]; + } + + List? get locations { + final jsonValue = _reader.optionalList('locations'); + if (jsonValue == null) return null; + return [ + for (final (index, element) in jsonValue.indexed) + LocationSyntax.fromJson( + element as Map, + path: [...path, 'locations', index], + ), + ]; + } + + set _locations(List? value) { + if (value == null) { + json.remove('locations'); + } else { + json['locations'] = [for (final item in value) item.json]; + } + } + + List _validateLocations() { + final listErrors = _reader.validateOptionalList>( + 'locations', + ); + if (listErrors.isNotEmpty) { + return listErrors; + } + final elements = locations; + if (elements == null) { + return []; + } + return [for (final element in elements) ...element.validate()]; + } + + MetadataSyntax get metadata { + final jsonValue = _reader.map$('metadata'); + return MetadataSyntax.fromJson(jsonValue, path: [...path, 'metadata']); + } + + set _metadata(MetadataSyntax value) { + json['metadata'] = value.json; + } + + List _validateMetadata() { + final mapErrors = _reader.validate>('metadata'); + if (mapErrors.isNotEmpty) { + return mapErrors; + } + return metadata.validate(); + } + + List? get recordings { + final jsonValue = _reader.optionalList('recordings'); + if (jsonValue == null) return null; + return [ + for (final (index, element) in jsonValue.indexed) + RecordingSyntax.fromJson( + element as Map, + path: [...path, 'recordings', index], + ), + ]; + } + + set _recordings(List? value) { + if (value == null) { + json.remove('recordings'); + } else { + json['recordings'] = [for (final item in value) item.json]; + } + } + + List _validateRecordings() { + final listErrors = _reader.validateOptionalList>( + 'recordings', + ); + if (listErrors.isNotEmpty) { + return listErrors; + } + final elements = recordings; + if (elements == null) { + return []; + } + return [for (final element in elements) ...element.validate()]; + } + + @override + List validate() => [ + ...super.validate(), + ..._validateConstants(), + ..._validateLocations(), + ..._validateMetadata(), + ..._validateRecordings(), + ]; + + @override + String toString() => 'RecordedUsesSyntax($json)'; +} + +class RecordingSyntax extends JsonObjectSyntax { + RecordingSyntax.fromJson(super.json, {super.path = const []}) + : super.fromJson(); + + RecordingSyntax({ + List? calls, + required DefinitionSyntax definition, + List? instances, + super.path = const [], + }) : super() { + _calls = calls; + _definition = definition; + _instances = instances; + json.sortOnKey(); + } + + List? get calls { + final jsonValue = _reader.optionalList('calls'); + if (jsonValue == null) return null; + return [ + for (final (index, element) in jsonValue.indexed) + CallSyntax.fromJson( + element as Map, + path: [...path, 'calls', index], + ), + ]; + } + + set _calls(List? value) { + if (value == null) { + json.remove('calls'); + } else { + json['calls'] = [for (final item in value) item.json]; + } + } + + List _validateCalls() { + final listErrors = _reader.validateOptionalList>( + 'calls', + ); + if (listErrors.isNotEmpty) { + return listErrors; + } + final elements = calls; + if (elements == null) { + return []; + } + return [for (final element in elements) ...element.validate()]; + } + + DefinitionSyntax get definition { + final jsonValue = _reader.map$('definition'); + return DefinitionSyntax.fromJson(jsonValue, path: [...path, 'definition']); + } + + set _definition(DefinitionSyntax value) { + json['definition'] = value.json; + } + + List _validateDefinition() { + final mapErrors = _reader.validate>('definition'); + if (mapErrors.isNotEmpty) { + return mapErrors; + } + return definition.validate(); + } + + List? get instances { + final jsonValue = _reader.optionalList('instances'); + if (jsonValue == null) return null; + return [ + for (final (index, element) in jsonValue.indexed) + InstanceSyntax.fromJson( + element as Map, + path: [...path, 'instances', index], + ), + ]; + } + + set _instances(List? value) { + if (value == null) { + json.remove('instances'); + } else { + json['instances'] = [for (final item in value) item.json]; + } + } + + List _validateInstances() { + final listErrors = _reader.validateOptionalList>( + 'instances', + ); + if (listErrors.isNotEmpty) { + return listErrors; + } + final elements = instances; + if (elements == null) { + return []; + } + return [for (final element in elements) ...element.validate()]; + } + + @override + List validate() => [ + ...super.validate(), + ..._validateCalls(), + ..._validateDefinition(), + ..._validateInstances(), + ]; + + @override + String toString() => 'RecordingSyntax($json)'; +} + +class StringConstantSyntax extends ConstantSyntax { + StringConstantSyntax.fromJson(super.json, {super.path}) : super._fromJson(); + + StringConstantSyntax({required String value, super.path = const []}) + : super(type: 'String') { + _value = value; + json.sortOnKey(); + } + + /// Setup all fields for [StringConstantSyntax] that are not in + /// [ConstantSyntax]. + void setup({required String value}) { + _value = value; + json.sortOnKey(); + } + + String get value => _reader.get('value'); + + set _value(String value) { + json.setOrRemove('value', value); + } + + List _validateValue() => _reader.validate('value'); + + @override + List validate() => [...super.validate(), ..._validateValue()]; + + @override + String toString() => 'StringConstantSyntax($json)'; +} + +extension StringConstantSyntaxExtension on ConstantSyntax { + bool get isStringConstant => type == 'String'; + + StringConstantSyntax get asStringConstant => + StringConstantSyntax.fromJson(json, path: path); +} + +class TearoffCallSyntax extends CallSyntax { + TearoffCallSyntax.fromJson(super.json, {super.path}) : super._fromJson(); + + TearoffCallSyntax({ + super.at, + required super.loadingUnit, + super.path = const [], + }) : super(type: 'tearoff'); + + @override + List validate() => [...super.validate()]; + + @override + String toString() => 'TearoffCallSyntax($json)'; +} + +extension TearoffCallSyntaxExtension on CallSyntax { + bool get isTearoffCall => type == 'tearoff'; + + TearoffCallSyntax get asTearoffCall => + TearoffCallSyntax.fromJson(json, path: path); +} + +class WithArgumentsCallSyntax extends CallSyntax { + WithArgumentsCallSyntax.fromJson(super.json, {super.path}) + : super._fromJson(); + + WithArgumentsCallSyntax({ + super.at, + required super.loadingUnit, + Map? named, + List? positional, + super.path = const [], + }) : super(type: 'with_arguments') { + _named = named; + _positional = positional; + json.sortOnKey(); + } + + /// Setup all fields for [WithArgumentsCallSyntax] that are not in + /// [CallSyntax]. + void setup({ + required Map? named, + required List? positional, + }) { + _named = named; + _positional = positional; + json.sortOnKey(); + } + + Map? get named => _reader.optionalMap('named'); + + set _named(Map? value) { + _checkArgumentMapKeys(value); + json.setOrRemove('named', value); + } + + List _validateNamed() => _reader.validateMap('named'); + + List? get positional => _reader.optionalList('positional'); + + set _positional(List? value) { + json.setOrRemove('positional', value); + } + + List _validatePositional() => + _reader.validateOptionalList('positional'); + + @override + List validate() => [ + ...super.validate(), + ..._validateNamed(), + ..._validatePositional(), + ]; + + @override + String toString() => 'WithArgumentsCallSyntax($json)'; +} + +extension WithArgumentsCallSyntaxExtension on CallSyntax { + bool get isWithArgumentsCall => type == 'with_arguments'; + + WithArgumentsCallSyntax get asWithArgumentsCall => + WithArgumentsCallSyntax.fromJson(json, path: path); +} + +class JsonObjectSyntax { + final Map json; + + final List path; + + _JsonReader get _reader => _JsonReader(json, path); + + JsonObjectSyntax({this.path = const []}) : json = {}; + + JsonObjectSyntax.fromJson(this.json, {this.path = const []}); + + List validate() => []; +} + +class _JsonReader { + /// The JSON Object this reader is reading. + final Map json; + + /// The path traversed by readers of the surrounding JSON. + /// + /// Contains [String] property keys and [int] indices. + /// + /// This is used to give more precise error messages. + final List path; + + _JsonReader(this.json, this.path); + + T get(String key) { + final value = json[key]; + if (value is T) return value; + throwFormatException(value, T, [key]); + } + + List validate(String key) { + final value = json[key]; + if (value is T) return []; + return [ + errorString(value, T, [key]), + ]; + } + + List list(String key) => + _castList(get>(key), key); + + List validateList(String key) { + final listErrors = validate>(key); + if (listErrors.isNotEmpty) { + return listErrors; + } + return _validateListElements(get>(key), key); + } + + List? optionalList(String key) => + switch (get?>(key)?.cast()) { + null => null, + final l => _castList(l, key), + }; + + List validateOptionalList(String key) { + final listErrors = validate?>(key); + if (listErrors.isNotEmpty) { + return listErrors; + } + final list = get?>(key); + if (list == null) { + return []; + } + return _validateListElements(list, key); + } + + /// [List.cast] but with [FormatException]s. + List _castList(List list, String key) { + for (final (index, value) in list.indexed) { + if (value is! T) { + throwFormatException(value, T, [key, index]); + } + } + return list.cast(); + } + + List _validateListElements( + List list, + String key, + ) { + final result = []; + for (final (index, value) in list.indexed) { + if (value is! T) { + result.add(errorString(value, T, [key, index])); + } + } + return result; + } + + Map map$(String key, {RegExp? keyPattern}) { + final map = get>(key); + final keyErrors = _validateMapKeys(map, key, keyPattern: keyPattern); + if (keyErrors.isNotEmpty) { + throw FormatException(keyErrors.join('\n')); + } + return _castMap(map, key); + } + + List validateMap( + String key, { + RegExp? keyPattern, + }) { + final mapErrors = validate>(key); + if (mapErrors.isNotEmpty) { + return mapErrors; + } + final map = get>(key); + return [ + ..._validateMapKeys(map, key, keyPattern: keyPattern), + ..._validateMapElements(map, key), + ]; + } + + Map? optionalMap( + String key, { + RegExp? keyPattern, + }) { + final map = get?>(key); + if (map == null) return null; + final keyErrors = _validateMapKeys(map, key, keyPattern: keyPattern); + if (keyErrors.isNotEmpty) { + throw FormatException(keyErrors.join('\n')); + } + return _castMap(map, key); + } + + List validateOptionalMap( + String key, { + RegExp? keyPattern, + }) { + final mapErrors = validate?>(key); + if (mapErrors.isNotEmpty) { + return mapErrors; + } + final map = get?>(key); + if (map == null) { + return []; + } + return [ + ..._validateMapKeys(map, key, keyPattern: keyPattern), + ..._validateMapElements(map, key), + ]; + } + + /// [Map.cast] but with [FormatException]s. + Map _castMap( + Map map_, + String parentKey, + ) { + for (final MapEntry(:key, :value) in map_.entries) { + if (value is! T) { + throwFormatException(value, T, [parentKey, key]); + } + } + return map_.cast(); + } + + List _validateMapKeys( + Map map_, + String parentKey, { + required RegExp? keyPattern, + }) { + if (keyPattern == null) return []; + final result = []; + for (final key in map_.keys) { + if (!keyPattern.hasMatch(key)) { + result.add( + keyErrorString(key, pattern: keyPattern, pathExtension: [parentKey]), + ); + } + } + return result; + } + + List _validateMapElements( + Map map_, + String parentKey, + ) { + final result = []; + for (final MapEntry(:key, :value) in map_.entries) { + if (value is! T) { + result.add(errorString(value, T, [parentKey, key])); + } + } + return result; + } + + List validateMapStringElements( + Map map_, + String parentKey, { + RegExp? valuePattern, + }) { + final result = []; + for (final MapEntry(:key, :value) in map_.entries) { + if (value != null && + valuePattern != null && + !valuePattern.hasMatch(value)) { + result.add( + errorString(value, T, [parentKey, key], pattern: valuePattern), + ); + } + } + return result; + } + + String string(String key, RegExp? pattern) { + final value = get(key); + if (pattern != null && !pattern.hasMatch(value)) { + throwFormatException(value, String, [key], pattern: pattern); + } + return value; + } + + String? optionalString(String key, RegExp? pattern) { + final value = get(key); + if (value == null) return null; + if (pattern != null && !pattern.hasMatch(value)) { + throwFormatException(value, String, [key], pattern: pattern); + } + return value; + } + + List validateString(String key, RegExp? pattern) { + final errors = validate(key); + if (errors.isNotEmpty) { + return errors; + } + final value = get(key); + if (pattern != null && !pattern.hasMatch(value)) { + return [ + errorString(value, String, [key], pattern: pattern), + ]; + } + return []; + } + + List validateOptionalString(String key, RegExp? pattern) { + final errors = validate(key); + if (errors.isNotEmpty) { + return errors; + } + final value = get(key); + if (value == null) return []; + if (pattern != null && !pattern.hasMatch(value)) { + return [ + errorString(value, String, [key], pattern: pattern), + ]; + } + return []; + } + + List? optionalStringList(String key) => optionalList(key); + + List validateOptionalStringList(String key) => + validateOptionalList(key); + + List stringList(String key) => list(key); + + List validateStringList(String key) => validateList(key); + + Uri path$(String key) => _fileSystemPathToUri(get(key)); + + List validatePath(String key) => validate(key); + + Uri? optionalPath(String key) { + final value = get(key); + if (value == null) return null; + return _fileSystemPathToUri(value); + } + + List validateOptionalPath(String key) => validate(key); + + List? optionalPathList(String key) { + final strings = optionalStringList(key); + if (strings == null) { + return null; + } + return [for (final string in strings) _fileSystemPathToUri(string)]; + } + + List validateOptionalPathList(String key) => + validateOptionalStringList(key); + + static Uri _fileSystemPathToUri(String path) { + if (path.endsWith(Platform.pathSeparator)) { + return Uri.directory(path); + } + return Uri.file(path); + } + + String _jsonPathToString(List pathEnding) => + [...path, ...pathEnding].join('.'); + + Never throwFormatException( + Object? value, + Type expectedType, + List pathExtension, { + RegExp? pattern, + }) { + throw FormatException( + errorString(value, expectedType, pathExtension, pattern: pattern), + ); + } + + String errorString( + Object? value, + Type expectedType, + List pathExtension, { + RegExp? pattern, + }) { + final pathString = _jsonPathToString(pathExtension); + if (value == null) { + return "No value was provided for '$pathString'." + ' Expected a $expectedType.'; + } + final satisfying = pattern == null ? '' : ' satisfying ${pattern.pattern}'; + return "Unexpected value '$value' (${value.runtimeType}) for '$pathString'." + ' Expected a $expectedType$satisfying.'; + } + + String keyErrorString( + String key, { + required RegExp pattern, + List pathExtension = const [], + }) { + final pathString = _jsonPathToString(pathExtension); + return "Unexpected key '$key' in '$pathString'." + ' Expected a key satisfying ${pattern.pattern}.'; + } + + /// Traverses a JSON path, returns `null` if the path cannot be traversed. + Object? tryTraverse(List path) { + Object? json = this.json; + for (final key in path) { + if (json is! Map) { + return null; + } + json = json[key]; + } + return json; + } +} + +extension on Map { + void setOrRemove(String key, Object? value) { + if (value == null) { + remove(key); + } else { + this[key] = value; + } + } +} + +extension on List { + List toJson() => [for (final uri in this) uri.toFilePath()]; +} + +extension, V extends Object?> on Map { + void sortOnKey() { + final result = {}; + final keysSorted = keys.toList()..sort(); + for (final key in keysSorted) { + result[key] = this[key] as V; + } + clear(); + addAll(result); + } +} + +void _checkArgumentMapKeys(Map? map, {RegExp? keyPattern}) { + if (map == null) return; + if (keyPattern == null) return; + for (final key in map.keys) { + if (!keyPattern.hasMatch(key)) { + throw ArgumentError.value( + map, + "Unexpected key '$key'." + ' Expected a key satisfying ${keyPattern.pattern}.', + ); + } + } +} + +void _checkArgumentMapStringElements( + Map? map, { + RegExp? valuePattern, +}) { + if (map == null) return; + if (valuePattern == null) return; + for (final entry in map.entries) { + final value = entry.value; + if (value != null && !valuePattern.hasMatch(value)) { + throw ArgumentError.value( + map, + "Unexpected value '$value' under key '${entry.key}'." + ' Expected a value satisfying ${valuePattern.pattern}.', + ); + } + } +} diff --git a/pkgs/record_use/pubspec.yaml b/pkgs/record_use/pubspec.yaml index a4e48d9f45..0c67335455 100644 --- a/pkgs/record_use/pubspec.yaml +++ b/pkgs/record_use/pubspec.yaml @@ -15,4 +15,7 @@ dependencies: dev_dependencies: dart_flutter_team_lints: ^3.5.2 + json_schema: ^5.2.2 + json_syntax_generator: ^0.1.0-wip + native_test_helpers: ^0.1.0-wip test: ^1.25.15 diff --git a/pkgs/record_use/test/json_schema/schema_test.dart b/pkgs/record_use/test/json_schema/schema_test.dart new file mode 100644 index 0000000000..75992af9bb --- /dev/null +++ b/pkgs/record_use/test/json_schema/schema_test.dart @@ -0,0 +1,212 @@ +// Copyright (c) 2025, 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 'dart:convert'; +import 'dart:io'; + +import 'package:json_schema/json_schema.dart'; +import 'package:native_test_helpers/native_test_helpers.dart'; +import 'package:test/test.dart'; + +void main() { + final schemaUri = packageUri.resolve('doc/schema/record_use.schema.json'); + final schemaJson = + jsonDecode(File.fromUri(schemaUri).readAsStringSync()) + as Map; + final schema = JsonSchema.create(schemaJson); + + final testDataUri = packageUri.resolve('test_data/json/'); + final allTestData = loadTestsData(testDataUri); + + testAllTestData(allTestData, schemaUri, schema); + + final dataUri = testDataUri.resolve('recorded_uses.json'); + + for (final field in recordUseFields) { + testField( + schemaUri: schemaUri, + dataUri: dataUri, + schema: schema, + data: allTestData[dataUri]!, + field: field.$1, + missingExpectations: field.$2, + ); + } +} + +const constNullIndex = 3; +const constInstanceIndex = 5; +List<(List, void Function(ValidationResults result))> +recordUseFields = [ + (['constants'], expectOptionalFieldMissing), + for (var index = 0; index < 7; index++) ...[ + (['constants', index, 'type'], expectRequiredFieldMissing), + if (index != constNullIndex && index != constInstanceIndex) + (['constants', index, 'value'], expectRequiredFieldMissing), + if (index == constInstanceIndex) + (['constants', index, 'value'], expectOptionalFieldMissing), + // Note the value for 'Instance' is optional because an empty map is + // omitted. Also, Null has no value field. + ], + (['locations'], expectOptionalFieldMissing), + (['locations', 0, 'uri'], expectRequiredFieldMissing), + (['locations', 0, 'line'], expectOptionalFieldMissing), + (['locations', 0, 'column'], expectOptionalFieldMissing), + (['recordings'], expectOptionalFieldMissing), + (['recordings', 0, 'definition'], expectRequiredFieldMissing), + (['recordings', 0, 'definition', 'identifier'], expectRequiredFieldMissing), + ( + ['recordings', 0, 'definition', 'identifier', 'uri'], + expectRequiredFieldMissing, + ), + // TODO(https://github.com/dart-lang/native/issues/1093): Potentially split + // out the concept of a class definition (which should never have a scope), + // and static method definition, which optionally have a scope. And the scope + // is always an enclosing class. + ( + ['recordings', 0, 'definition', 'identifier', 'scope'], + expectOptionalFieldMissing, + ), + ( + ['recordings', 0, 'definition', 'identifier', 'name'], + expectRequiredFieldMissing, + ), + // TODO: Why is this optional in the package test data? + (['recordings', 0, 'definition', 'loading_unit'], expectOptionalFieldMissing), + + // TODO(https://github.com/dart-lang/native/issues/1093): Whether calls or + // instances is required depends on whether the definition is a class or + // method. This should be cleaned up. + (['recordings', 0, 'calls'], expectOptionalFieldMissing), + (['recordings', 0, 'calls', 0, 'type'], expectRequiredFieldMissing), + (['recordings', 0, 'calls', 0, 'named'], expectOptionalFieldMissing), + (['recordings', 0, 'calls', 0, 'named', 'a'], expectOptionalFieldMissing), + (['recordings', 0, 'calls', 0, 'positional'], expectOptionalFieldMissing), + (['recordings', 0, 'calls', 0, 'positional', 0], expectOptionalFieldMissing), + (['recordings', 0, 'calls', 0, 'loading_unit'], expectRequiredFieldMissing), + (['recordings', 0, 'calls', 0, '@'], expectOptionalFieldMissing), + (['recordings', 1, 'instances'], expectOptionalFieldMissing), + ( + ['recordings', 1, 'instances', 0, 'constant_index'], + expectRequiredFieldMissing, + ), + ( + ['recordings', 1, 'instances', 0, 'loading_unit'], + expectRequiredFieldMissing, + ), + (['recordings', 1, 'instances', 0, '@'], expectOptionalFieldMissing), + + // TODO: Locations are not always provided by dart2js for const values. So we + // need to make it optional. +]; + +void testAllTestData( + AllTestData allTestData, + Uri schemaUri, + JsonSchema schema, +) { + for (final entry in allTestData.entries) { + final dataUri = entry.key; + final dataString = entry.value; + test('Validate $dataUri against $schemaUri', () { + printOnFailure(dataUri.toString()); + printOnFailure(schemaUri.toString()); + final result = schema.validate(jsonDecode(dataString)); + for (final e in result.errors) { + printOnFailure(e.toString()); + } + expect(result.isValid, isTrue); + }); + } +} + +typedef AllTestData = Map; + +/// The data is modified in tests, so load but don't json decode them all. +AllTestData loadTestsData(Uri directory) { + final allTestData = {}; + for (final file in Directory.fromUri(directory).listSync()) { + file as File; + allTestData[file.uri] = file.readAsStringSync(); + } + return allTestData; +} + +Uri packageUri = findPackageRoot('record_use'); + +/// Test removing a field or modifying it. +/// +/// Changing a field to a wrong type is always expected to fail. +/// +/// Removing a field can be valid, the expectations must be passed in +/// [missingExpectations]. +void testField({ + required Uri schemaUri, + required Uri dataUri, + required JsonSchema schema, + required String data, + required List field, + required void Function(ValidationResults result) missingExpectations, +}) { + final fieldPath = field.join('.'); + test('$schemaUri $dataUri $fieldPath missing', () { + final dataDecoded = jsonDecode(data); + final dataToModify = _traverseJson( + dataDecoded, + field.sublist(0, field.length - 1), + ); + if (dataToModify is List) { + final index = field.last as int; + dataToModify.removeAt(index); + } else { + // ignore: avoid_dynamic_calls + dataToModify.remove(field.last); + } + + final result = schema.validate(dataDecoded); + printOnFailure(result.toString()); + missingExpectations(result); + }); + + test('$schemaUri $fieldPath wrong type', () { + final dataDecoded = jsonDecode(data); + final dataToModify = _traverseJson( + dataDecoded, + field.sublist(0, field.length - 1), + ); + // ignore: avoid_dynamic_calls + final originalValue = dataToModify[field.last]; + final wrongTypeValue = originalValue is int ? '123' : 123; + // ignore: avoid_dynamic_calls + dataToModify[field.last] = wrongTypeValue; + + final result = schema.validate(dataDecoded); + expect(result.isValid, isFalse); + }); +} + +void expectRequiredFieldMissing(ValidationResults result) { + expect(result.isValid, isFalse); +} + +void expectOptionalFieldMissing(ValidationResults result) { + expect(result.isValid, isTrue); +} + +dynamic _traverseJson(dynamic json, List path) { + while (path.isNotEmpty) { + final key = path.removeAt(0); + switch (key) { + case final int i: + json = (json as List)[i] as Object; + break; + case final String s: + json = (json as Map)[s] as Object; + break; + default: + throw UnsupportedError(key.toString()); + } + } + return json; +} diff --git a/pkgs/record_use/test/storage_2_test.dart b/pkgs/record_use/test/storage_2_test.dart new file mode 100644 index 0000000000..a4f4f0bb42 --- /dev/null +++ b/pkgs/record_use/test/storage_2_test.dart @@ -0,0 +1,55 @@ +// Copyright (c) 2025, 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 'dart:convert'; +import 'dart:io'; + +import 'package:native_test_helpers/native_test_helpers.dart'; +import 'package:record_use/record_use_internal.dart'; +import 'package:test/test.dart'; + +void main() { + final update = Platform.environment['UPDATE'] != null; + final testDataUri = packageUri.resolve('test_data/json/'); + final allTestData = loadTestsData(testDataUri); + + for (final entry in allTestData.entries) { + final dataUri = entry.key; + final dataString = entry.value; + test('Deserialize and serialize $dataUri', () { + printOnFailure(dataUri.toString()); + final uses = Recordings.fromJson( + jsonDecode(dataString) as Map, + ); + const encoder = JsonEncoder.withIndent(' '); + final serializedString = encoder.convert({ + '\$schema': '../../doc/schema/record_use.schema.json', + ...uses.toJson(), + }); + + if (update) { + File.fromUri(dataUri).writeAsStringSync('$serializedString\n'); + } else { + expect( + serializedString.replaceAll('\r\n', '\n').trim(), + dataString.replaceAll('\r\n', '\n').trim(), + ); + } + }); + } +} + +typedef AllTestData = Map; + +/// The data is modified in tests, so load but don't json decode them all. +AllTestData loadTestsData(Uri directory) { + final allTestData = {}; + for (final file in Directory.fromUri(directory).listSync()) { + file as File; + allTestData[file.uri] = file.readAsStringSync(); + } + return allTestData; +} + +Uri packageUri = findPackageRoot('record_use'); diff --git a/pkgs/record_use/test/test_data.dart b/pkgs/record_use/test/test_data.dart index 4a11adbabd..4ebb3dc2e0 100644 --- a/pkgs/record_use/test/test_data.dart +++ b/pkgs/record_use/test/test_data.dart @@ -39,7 +39,7 @@ final recordedUses = Recordings( 'leroy': StringConstant('jenkins'), }, loadingUnit: 'o.js', - location: Location(uri: 'lib/test.dart'), + location: Location(uri: 'lib/test.dart', line: 12, column: 36), ), const CallWithArguments( positionalArguments: [ @@ -193,7 +193,9 @@ const recordedUsesJson = '''{ ], "locations": [ { - "uri": "lib/test.dart" + "uri": "lib/test.dart", + "line": 12, + "column": 36 }, { "uri": "lib/test2.dart" diff --git a/pkgs/record_use/test_data/json/complex.json b/pkgs/record_use/test_data/json/complex.json new file mode 100644 index 0000000000..2921adecf1 --- /dev/null +++ b/pkgs/record_use/test_data/json/complex.json @@ -0,0 +1,44 @@ +{ + "$schema": "../../doc/schema/record_use.schema.json", + "constants": [ + { + "type": "int", + "value": 42 + } + ], + "locations": [ + { + "uri": "complex.dart" + } + ], + "metadata": { + "comment": "Recorded usages of objects tagged with a `RecordUse` annotation", + "version": "0.4.0" + }, + "recordings": [ + { + "calls": [ + { + "@": 0, + "loading_unit": "1", + "positional": [ + null, + null, + null, + null, + 0 + ], + "type": "with_arguments" + } + ], + "definition": { + "identifier": { + "name": "generate", + "scope": "OtherClass", + "uri": "complex.dart" + }, + "loading_unit": "1" + } + } + ] +} diff --git a/pkgs/record_use/test_data/json/different.json b/pkgs/record_use/test_data/json/different.json new file mode 100644 index 0000000000..b9e76a58fc --- /dev/null +++ b/pkgs/record_use/test_data/json/different.json @@ -0,0 +1,7 @@ +{ + "$schema": "../../doc/schema/record_use.schema.json", + "metadata": { + "comment": "Recorded usages of objects tagged with a `RecordUse` annotation", + "version": "0.4.0" + } +} diff --git a/pkgs/record_use/test_data/json/extension.json b/pkgs/record_use/test_data/json/extension.json new file mode 100644 index 0000000000..a9bb08c9ab --- /dev/null +++ b/pkgs/record_use/test_data/json/extension.json @@ -0,0 +1,39 @@ +{ + "$schema": "../../doc/schema/record_use.schema.json", + "constants": [ + { + "type": "String", + "value": "42" + } + ], + "locations": [ + { + "uri": "extension.dart" + } + ], + "metadata": { + "comment": "Recorded usages of objects tagged with a `RecordUse` annotation", + "version": "0.4.0" + }, + "recordings": [ + { + "calls": [ + { + "@": 0, + "loading_unit": "1", + "positional": [ + 0 + ], + "type": "with_arguments" + } + ], + "definition": { + "identifier": { + "name": "_extension#0|callWithArgs", + "uri": "extension.dart" + }, + "loading_unit": "1" + } + } + ] +} diff --git a/pkgs/record_use/test_data/json/instance_class.json b/pkgs/record_use/test_data/json/instance_class.json new file mode 100644 index 0000000000..c845673ffd --- /dev/null +++ b/pkgs/record_use/test_data/json/instance_class.json @@ -0,0 +1,42 @@ +{ + "$schema": "../../doc/schema/record_use.schema.json", + "constants": [ + { + "type": "int", + "value": 42 + }, + { + "type": "Instance", + "value": { + "i": 0 + } + } + ], + "locations": [ + { + "uri": "instance_class.dart" + } + ], + "metadata": { + "comment": "Recorded usages of objects tagged with a `RecordUse` annotation", + "version": "0.4.0" + }, + "recordings": [ + { + "definition": { + "identifier": { + "name": "MyClass", + "uri": "instance_class.dart" + }, + "loading_unit": "1" + }, + "instances": [ + { + "@": 0, + "constant_index": 1, + "loading_unit": "1" + } + ] + } + ] +} diff --git a/pkgs/record_use/test_data/json/instance_complex.json b/pkgs/record_use/test_data/json/instance_complex.json new file mode 100644 index 0000000000..caf5f6feb8 --- /dev/null +++ b/pkgs/record_use/test_data/json/instance_complex.json @@ -0,0 +1,84 @@ +{ + "$schema": "../../doc/schema/record_use.schema.json", + "constants": [ + { + "type": "int", + "value": 15 + }, + { + "type": "String", + "value": "s" + }, + { + "type": "bool", + "value": false + }, + { + "type": "map", + "value": { + "h": 2 + } + }, + { + "type": "bool", + "value": true + }, + { + "type": "int", + "value": 3 + }, + { + "type": "map", + "value": { + "l": 5 + } + }, + { + "type": "list", + "value": [ + 6 + ] + }, + { + "type": "Null" + }, + { + "type": "Instance", + "value": { + "i": 0, + "s": 1, + "m": 3, + "b": 4, + "l": 7, + "n": 8 + } + } + ], + "locations": [ + { + "uri": "instance_complex.dart" + } + ], + "metadata": { + "comment": "Recorded usages of objects tagged with a `RecordUse` annotation", + "version": "0.4.0" + }, + "recordings": [ + { + "definition": { + "identifier": { + "name": "MyClass", + "uri": "instance_complex.dart" + }, + "loading_unit": "1" + }, + "instances": [ + { + "@": 0, + "constant_index": 9, + "loading_unit": "1" + } + ] + } + ] +} diff --git a/pkgs/record_use/test_data/json/instance_duplicates.json b/pkgs/record_use/test_data/json/instance_duplicates.json new file mode 100644 index 0000000000..dded9b866c --- /dev/null +++ b/pkgs/record_use/test_data/json/instance_duplicates.json @@ -0,0 +1,57 @@ +{ + "$schema": "../../doc/schema/record_use.schema.json", + "constants": [ + { + "type": "int", + "value": 42 + }, + { + "type": "Instance", + "value": { + "i": 0 + } + }, + { + "type": "int", + "value": 43 + }, + { + "type": "Instance", + "value": { + "i": 2 + } + } + ], + "locations": [ + { + "uri": "instance_duplicates.dart" + } + ], + "metadata": { + "comment": "Recorded usages of objects tagged with a `RecordUse` annotation", + "version": "0.4.0" + }, + "recordings": [ + { + "definition": { + "identifier": { + "name": "MyClass", + "uri": "instance_duplicates.dart" + }, + "loading_unit": "1" + }, + "instances": [ + { + "@": 0, + "constant_index": 1, + "loading_unit": "1" + }, + { + "@": 0, + "constant_index": 3, + "loading_unit": "1" + } + ] + } + ] +} diff --git a/pkgs/record_use/test_data/json/instance_method.json b/pkgs/record_use/test_data/json/instance_method.json new file mode 100644 index 0000000000..12fba2bce6 --- /dev/null +++ b/pkgs/record_use/test_data/json/instance_method.json @@ -0,0 +1,42 @@ +{ + "$schema": "../../doc/schema/record_use.schema.json", + "constants": [ + { + "type": "int", + "value": 42 + }, + { + "type": "Instance", + "value": { + "i": 0 + } + } + ], + "locations": [ + { + "uri": "instance_method.dart" + } + ], + "metadata": { + "comment": "Recorded usages of objects tagged with a `RecordUse` annotation", + "version": "0.4.0" + }, + "recordings": [ + { + "definition": { + "identifier": { + "name": "MyClass", + "uri": "instance_method.dart" + }, + "loading_unit": "1" + }, + "instances": [ + { + "@": 0, + "constant_index": 1, + "loading_unit": "1" + } + ] + } + ] +} diff --git a/pkgs/record_use/test_data/json/instance_not_annotation.json b/pkgs/record_use/test_data/json/instance_not_annotation.json new file mode 100644 index 0000000000..d51f08a7f7 --- /dev/null +++ b/pkgs/record_use/test_data/json/instance_not_annotation.json @@ -0,0 +1,35 @@ +{ + "$schema": "../../doc/schema/record_use.schema.json", + "constants": [ + { + "type": "Instance" + } + ], + "locations": [ + { + "uri": "instance_not_annotation.dart" + } + ], + "metadata": { + "comment": "Recorded usages of objects tagged with a `RecordUse` annotation", + "version": "0.4.0" + }, + "recordings": [ + { + "definition": { + "identifier": { + "name": "MyClass", + "uri": "instance_not_annotation.dart" + }, + "loading_unit": "1" + }, + "instances": [ + { + "@": 0, + "constant_index": 0, + "loading_unit": "1" + } + ] + } + ] +} diff --git a/pkgs/record_use/test_data/json/loading_units_multiple.json b/pkgs/record_use/test_data/json/loading_units_multiple.json new file mode 100644 index 0000000000..a0c8446914 --- /dev/null +++ b/pkgs/record_use/test_data/json/loading_units_multiple.json @@ -0,0 +1,51 @@ +{ + "$schema": "../../doc/schema/record_use.schema.json", + "constants": [ + { + "type": "int", + "value": 42 + } + ], + "locations": [ + { + "uri": "loading_units_multiple.dart" + }, + { + "uri": "loading_units_multiple_helper.dart" + } + ], + "metadata": { + "comment": "Recorded usages of objects tagged with a `RecordUse` annotation", + "version": "0.4.0" + }, + "recordings": [ + { + "calls": [ + { + "@": 0, + "loading_unit": "1", + "positional": [ + 0 + ], + "type": "with_arguments" + }, + { + "@": 1, + "loading_unit": "2", + "positional": [ + 0 + ], + "type": "with_arguments" + } + ], + "definition": { + "identifier": { + "name": "someStaticMethod", + "scope": "SomeClass", + "uri": "loading_units_multiple_helper_shared.dart" + }, + "loading_unit": "1" + } + } + ] +} diff --git a/pkgs/record_use/test_data/json/loading_units_simple.json b/pkgs/record_use/test_data/json/loading_units_simple.json new file mode 100644 index 0000000000..a1047ebf87 --- /dev/null +++ b/pkgs/record_use/test_data/json/loading_units_simple.json @@ -0,0 +1,63 @@ +{ + "$schema": "../../doc/schema/record_use.schema.json", + "constants": [ + { + "type": "int", + "value": 42 + } + ], + "locations": [ + { + "uri": "loading_units_simple.dart" + }, + { + "uri": "loading_units_simple_helper.dart" + } + ], + "metadata": { + "comment": "Recorded usages of objects tagged with a `RecordUse` annotation", + "version": "0.4.0" + }, + "recordings": [ + { + "calls": [ + { + "@": 0, + "loading_unit": "1", + "positional": [ + 0 + ], + "type": "with_arguments" + } + ], + "definition": { + "identifier": { + "name": "someStaticMethod", + "scope": "SomeClass", + "uri": "loading_units_simple.dart" + }, + "loading_unit": "1" + } + }, + { + "calls": [ + { + "@": 1, + "loading_unit": "2", + "positional": [ + 0 + ], + "type": "with_arguments" + } + ], + "definition": { + "identifier": { + "name": "someStaticMethod", + "scope": "SomeClass", + "uri": "loading_units_simple_helper.dart" + }, + "loading_unit": "2" + } + } + ] +} diff --git a/pkgs/record_use/test_data/json/named_and_positional.json b/pkgs/record_use/test_data/json/named_and_positional.json new file mode 100644 index 0000000000..abed16dab0 --- /dev/null +++ b/pkgs/record_use/test_data/json/named_and_positional.json @@ -0,0 +1,99 @@ +{ + "$schema": "../../doc/schema/record_use.schema.json", + "constants": [ + { + "type": "int", + "value": 3 + }, + { + "type": "Null" + }, + { + "type": "int", + "value": 5 + }, + { + "type": "int", + "value": 1 + }, + { + "type": "int", + "value": 2 + }, + { + "type": "int", + "value": 4 + } + ], + "locations": [ + { + "uri": "named_and_positional.dart" + } + ], + "metadata": { + "comment": "Recorded usages of objects tagged with a `RecordUse` annotation", + "version": "0.4.0" + }, + "recordings": [ + { + "calls": [ + { + "@": 0, + "loading_unit": "1", + "named": { + "l": 1, + "k": 0 + }, + "positional": [ + 0 + ], + "type": "with_arguments" + }, + { + "@": 0, + "loading_unit": "1", + "named": { + "k": 3, + "l": 1 + }, + "positional": [ + 2 + ], + "type": "with_arguments" + }, + { + "@": 0, + "loading_unit": "1", + "named": { + "l": 4, + "k": 0 + }, + "positional": [ + 2 + ], + "type": "with_arguments" + }, + { + "@": 0, + "loading_unit": "1", + "named": { + "l": 4, + "k": 5 + }, + "positional": [ + 2 + ], + "type": "with_arguments" + } + ], + "definition": { + "identifier": { + "name": "someStaticMethod", + "scope": "SomeClass", + "uri": "named_and_positional.dart" + }, + "loading_unit": "1" + } + } + ] +} diff --git a/pkgs/record_use/test_data/json/named_both.json b/pkgs/record_use/test_data/json/named_both.json new file mode 100644 index 0000000000..1c4ff3798d --- /dev/null +++ b/pkgs/record_use/test_data/json/named_both.json @@ -0,0 +1,91 @@ +{ + "$schema": "../../doc/schema/record_use.schema.json", + "constants": [ + { + "type": "int", + "value": 3 + }, + { + "type": "Null" + }, + { + "type": "int", + "value": 5 + }, + { + "type": "int", + "value": 1 + }, + { + "type": "int", + "value": 2 + }, + { + "type": "int", + "value": 4 + } + ], + "locations": [ + { + "uri": "named_both.dart" + } + ], + "metadata": { + "comment": "Recorded usages of objects tagged with a `RecordUse` annotation", + "version": "0.4.0" + }, + "recordings": [ + { + "calls": [ + { + "@": 0, + "loading_unit": "1", + "named": { + "i": 0, + "l": 1, + "k": 0 + }, + "type": "with_arguments" + }, + { + "@": 0, + "loading_unit": "1", + "named": { + "i": 2, + "k": 3, + "l": 1 + }, + "type": "with_arguments" + }, + { + "@": 0, + "loading_unit": "1", + "named": { + "i": 2, + "l": 4, + "k": 0 + }, + "type": "with_arguments" + }, + { + "@": 0, + "loading_unit": "1", + "named": { + "i": 2, + "l": 4, + "k": 5 + }, + "type": "with_arguments" + } + ], + "definition": { + "identifier": { + "name": "someStaticMethod", + "scope": "SomeClass", + "uri": "named_both.dart" + }, + "loading_unit": "1" + } + } + ] +} diff --git a/pkgs/record_use/test_data/json/named_optional.json b/pkgs/record_use/test_data/json/named_optional.json new file mode 100644 index 0000000000..c87a530253 --- /dev/null +++ b/pkgs/record_use/test_data/json/named_optional.json @@ -0,0 +1,52 @@ +{ + "$schema": "../../doc/schema/record_use.schema.json", + "constants": [ + { + "type": "int", + "value": 3 + }, + { + "type": "int", + "value": 4 + } + ], + "locations": [ + { + "uri": "named_optional.dart" + } + ], + "metadata": { + "comment": "Recorded usages of objects tagged with a `RecordUse` annotation", + "version": "0.4.0" + }, + "recordings": [ + { + "calls": [ + { + "@": 0, + "loading_unit": "1", + "named": { + "i": 0 + }, + "type": "with_arguments" + }, + { + "@": 0, + "loading_unit": "1", + "named": { + "i": 1 + }, + "type": "with_arguments" + } + ], + "definition": { + "identifier": { + "name": "someStaticMethod", + "scope": "SomeClass", + "uri": "named_optional.dart" + }, + "loading_unit": "1" + } + } + ] +} diff --git a/pkgs/record_use/test_data/json/named_required.json b/pkgs/record_use/test_data/json/named_required.json new file mode 100644 index 0000000000..e419058f4a --- /dev/null +++ b/pkgs/record_use/test_data/json/named_required.json @@ -0,0 +1,52 @@ +{ + "$schema": "../../doc/schema/record_use.schema.json", + "constants": [ + { + "type": "int", + "value": 3 + }, + { + "type": "int", + "value": 5 + } + ], + "locations": [ + { + "uri": "named_required.dart" + } + ], + "metadata": { + "comment": "Recorded usages of objects tagged with a `RecordUse` annotation", + "version": "0.4.0" + }, + "recordings": [ + { + "calls": [ + { + "@": 0, + "loading_unit": "1", + "named": { + "i": 0 + }, + "type": "with_arguments" + }, + { + "@": 0, + "loading_unit": "1", + "named": { + "i": 1 + }, + "type": "with_arguments" + } + ], + "definition": { + "identifier": { + "name": "someStaticMethod", + "scope": "SomeClass", + "uri": "named_required.dart" + }, + "loading_unit": "1" + } + } + ] +} diff --git a/pkgs/record_use/test_data/json/named_with_function_arg.json b/pkgs/record_use/test_data/json/named_with_function_arg.json new file mode 100644 index 0000000000..131da92af3 --- /dev/null +++ b/pkgs/record_use/test_data/json/named_with_function_arg.json @@ -0,0 +1,42 @@ +{ + "$schema": "../../doc/schema/record_use.schema.json", + "constants": [ + { + "type": "String", + "value": "hello-world" + } + ], + "locations": [ + { + "uri": "named_with_function_arg.dart" + } + ], + "metadata": { + "comment": "Recorded usages of objects tagged with a `RecordUse` annotation", + "version": "0.4.0" + }, + "recordings": [ + { + "calls": [ + { + "@": 0, + "loading_unit": "1", + "named": { + "s": 0 + }, + "positional": [ + null + ], + "type": "with_arguments" + } + ], + "definition": { + "identifier": { + "name": "Ext|foo", + "uri": "named_with_function_arg.dart" + }, + "loading_unit": "1" + } + } + ] +} diff --git a/pkgs/record_use/test_data/json/nested.json b/pkgs/record_use/test_data/json/nested.json new file mode 100644 index 0000000000..4a6f7136d0 --- /dev/null +++ b/pkgs/record_use/test_data/json/nested.json @@ -0,0 +1,76 @@ +{ + "$schema": "../../doc/schema/record_use.schema.json", + "constants": [ + { + "type": "int", + "value": 42 + }, + { + "type": "Instance", + "value": { + "i": 0 + } + }, + { + "type": "String", + "value": "test" + }, + { + "type": "Instance", + "value": { + "i": 2 + } + }, + { + "type": "Instance" + } + ], + "locations": [ + { + "uri": "nested.dart" + } + ], + "metadata": { + "comment": "Recorded usages of objects tagged with a `RecordUse` annotation", + "version": "0.4.0" + }, + "recordings": [ + { + "definition": { + "identifier": { + "name": "MyClass", + "uri": "nested.dart" + }, + "loading_unit": "1" + }, + "instances": [ + { + "@": 0, + "constant_index": 1, + "loading_unit": "1" + }, + { + "@": 0, + "constant_index": 3, + "loading_unit": "1" + } + ] + }, + { + "definition": { + "identifier": { + "name": "MyOtherClass", + "uri": "nested.dart" + }, + "loading_unit": "1" + }, + "instances": [ + { + "@": 0, + "constant_index": 4, + "loading_unit": "1" + } + ] + } + ] +} diff --git a/pkgs/record_use/test_data/json/partfile_main.json b/pkgs/record_use/test_data/json/partfile_main.json new file mode 100644 index 0000000000..8fe7060824 --- /dev/null +++ b/pkgs/record_use/test_data/json/partfile_main.json @@ -0,0 +1,40 @@ +{ + "$schema": "../../doc/schema/record_use.schema.json", + "constants": [ + { + "type": "int", + "value": 42 + } + ], + "locations": [ + { + "uri": "partfile_main.dart" + } + ], + "metadata": { + "comment": "Recorded usages of objects tagged with a `RecordUse` annotation", + "version": "0.4.0" + }, + "recordings": [ + { + "calls": [ + { + "@": 0, + "loading_unit": "1", + "positional": [ + 0 + ], + "type": "with_arguments" + } + ], + "definition": { + "identifier": { + "name": "someStaticMethod", + "scope": "SomeClass", + "uri": "partfile_main.dart" + }, + "loading_unit": "1" + } + } + ] +} diff --git a/pkgs/record_use/test_data/json/positional_both.json b/pkgs/record_use/test_data/json/positional_both.json new file mode 100644 index 0000000000..462226b828 --- /dev/null +++ b/pkgs/record_use/test_data/json/positional_both.json @@ -0,0 +1,62 @@ +{ + "$schema": "../../doc/schema/record_use.schema.json", + "constants": [ + { + "type": "int", + "value": 5 + }, + { + "type": "int", + "value": 3 + }, + { + "type": "int", + "value": 6 + }, + { + "type": "int", + "value": 4 + } + ], + "locations": [ + { + "uri": "positional_both.dart" + } + ], + "metadata": { + "comment": "Recorded usages of objects tagged with a `RecordUse` annotation", + "version": "0.4.0" + }, + "recordings": [ + { + "calls": [ + { + "@": 0, + "loading_unit": "1", + "positional": [ + 0, + 1 + ], + "type": "with_arguments" + }, + { + "@": 0, + "loading_unit": "1", + "positional": [ + 2, + 3 + ], + "type": "with_arguments" + } + ], + "definition": { + "identifier": { + "name": "someStaticMethod", + "scope": "SomeClass", + "uri": "positional_both.dart" + }, + "loading_unit": "1" + } + } + ] +} diff --git a/pkgs/record_use/test_data/json/positional_optional.json b/pkgs/record_use/test_data/json/positional_optional.json new file mode 100644 index 0000000000..49c83f62e3 --- /dev/null +++ b/pkgs/record_use/test_data/json/positional_optional.json @@ -0,0 +1,52 @@ +{ + "$schema": "../../doc/schema/record_use.schema.json", + "constants": [ + { + "type": "int", + "value": 3 + }, + { + "type": "int", + "value": 4 + } + ], + "locations": [ + { + "uri": "positional_optional.dart" + } + ], + "metadata": { + "comment": "Recorded usages of objects tagged with a `RecordUse` annotation", + "version": "0.4.0" + }, + "recordings": [ + { + "calls": [ + { + "@": 0, + "loading_unit": "1", + "positional": [ + 0 + ], + "type": "with_arguments" + }, + { + "@": 0, + "loading_unit": "1", + "positional": [ + 1 + ], + "type": "with_arguments" + } + ], + "definition": { + "identifier": { + "name": "someStaticMethod", + "scope": "SomeClass", + "uri": "positional_optional.dart" + }, + "loading_unit": "1" + } + } + ] +} diff --git a/pkgs/record_use/test_data/json/record_enum.json b/pkgs/record_use/test_data/json/record_enum.json new file mode 100644 index 0000000000..dfe100249c --- /dev/null +++ b/pkgs/record_use/test_data/json/record_enum.json @@ -0,0 +1,53 @@ +{ + "$schema": "../../doc/schema/record_use.schema.json", + "constants": [ + { + "type": "int", + "value": 0 + }, + { + "type": "String", + "value": "a" + }, + { + "type": "Instance", + "value": { + "index": 0, + "_name": 1 + } + }, + { + "type": "Instance", + "value": { + "a": 2 + } + } + ], + "locations": [ + { + "uri": "record_enum.dart" + } + ], + "metadata": { + "comment": "Recorded usages of objects tagged with a `RecordUse` annotation", + "version": "0.4.0" + }, + "recordings": [ + { + "definition": { + "identifier": { + "name": "MyClass", + "uri": "record_enum.dart" + }, + "loading_unit": "1" + }, + "instances": [ + { + "@": 0, + "constant_index": 3, + "loading_unit": "1" + } + ] + } + ] +} diff --git a/pkgs/record_use/test_data/json/record_instance_constant.json b/pkgs/record_use/test_data/json/record_instance_constant.json new file mode 100644 index 0000000000..ee23c9d3d6 --- /dev/null +++ b/pkgs/record_use/test_data/json/record_instance_constant.json @@ -0,0 +1,48 @@ +{ + "$schema": "../../doc/schema/record_use.schema.json", + "constants": [ + { + "type": "int", + "value": 42 + }, + { + "type": "Instance", + "value": { + "i": 0 + } + }, + { + "type": "Instance", + "value": { + "a": 1 + } + } + ], + "locations": [ + { + "uri": "record_instance_constant.dart" + } + ], + "metadata": { + "comment": "Recorded usages of objects tagged with a `RecordUse` annotation", + "version": "0.4.0" + }, + "recordings": [ + { + "definition": { + "identifier": { + "name": "MyClass", + "uri": "record_instance_constant.dart" + }, + "loading_unit": "1" + }, + "instances": [ + { + "@": 0, + "constant_index": 2, + "loading_unit": "1" + } + ] + } + ] +} diff --git a/pkgs/record_use/test_data/json/record_instance_constant_empty.json b/pkgs/record_use/test_data/json/record_instance_constant_empty.json new file mode 100644 index 0000000000..e83ed4c255 --- /dev/null +++ b/pkgs/record_use/test_data/json/record_instance_constant_empty.json @@ -0,0 +1,41 @@ +{ + "$schema": "../../doc/schema/record_use.schema.json", + "constants": [ + { + "type": "Instance" + }, + { + "type": "Instance", + "value": { + "a": 0 + } + } + ], + "locations": [ + { + "uri": "record_instance_constant_empty.dart" + } + ], + "metadata": { + "comment": "Recorded usages of objects tagged with a `RecordUse` annotation", + "version": "0.4.0" + }, + "recordings": [ + { + "definition": { + "identifier": { + "name": "MyClass", + "uri": "record_instance_constant_empty.dart" + }, + "loading_unit": "1" + }, + "instances": [ + { + "@": 0, + "constant_index": 1, + "loading_unit": "1" + } + ] + } + ] +} diff --git a/pkgs/record_use/test_data/json/recorded_uses.json b/pkgs/record_use/test_data/json/recorded_uses.json new file mode 100644 index 0000000000..a0a915af7c --- /dev/null +++ b/pkgs/record_use/test_data/json/recorded_uses.json @@ -0,0 +1,98 @@ +{ + "$schema": "../../doc/schema/record_use.schema.json", + "constants": [ + { + "type": "String", + "value": "42" + }, + { + "type": "bool", + "value": false + }, + { + "type": "map", + "value": { + "h": 1 + } + }, + { + "type": "Null" + }, + { + "type": "int", + "value": 42 + }, + { + "type": "Instance", + "value": { + "i": 4 + } + }, + { + "type": "list", + "value": [ + 4 + ] + } + ], + "locations": [ + { + "column": 30, + "line": 12, + "uri": "complex.dart" + } + ], + "metadata": { + "comment": "Recorded usages of objects tagged with a `RecordUse` annotation", + "version": "0.4.0" + }, + "recordings": [ + { + "calls": [ + { + "@": 0, + "loading_unit": "1", + "named": { + "a": 0, + "b": 1, + "c": 4 + }, + "positional": [ + 0, + 2, + 3, + null, + 4, + 5, + 6 + ], + "type": "with_arguments" + } + ], + "definition": { + "identifier": { + "name": "generate", + "scope": "OtherClass", + "uri": "complex.dart" + }, + "loading_unit": "1" + } + }, + { + "definition": { + "identifier": { + "name": "MyClass", + "uri": "instance_class.dart" + }, + "loading_unit": "1" + }, + "instances": [ + { + "@": 0, + "constant_index": 5, + "loading_unit": "1" + } + ] + } + ] +} diff --git a/pkgs/record_use/test_data/json/simple.json b/pkgs/record_use/test_data/json/simple.json new file mode 100644 index 0000000000..913f55fc09 --- /dev/null +++ b/pkgs/record_use/test_data/json/simple.json @@ -0,0 +1,40 @@ +{ + "$schema": "../../doc/schema/record_use.schema.json", + "constants": [ + { + "type": "int", + "value": 42 + } + ], + "locations": [ + { + "uri": "simple.dart" + } + ], + "metadata": { + "comment": "Recorded usages of objects tagged with a `RecordUse` annotation", + "version": "0.4.0" + }, + "recordings": [ + { + "calls": [ + { + "@": 0, + "loading_unit": "1", + "positional": [ + 0 + ], + "type": "with_arguments" + } + ], + "definition": { + "identifier": { + "name": "someStaticMethod", + "scope": "SomeClass", + "uri": "simple.dart" + }, + "loading_unit": "1" + } + } + ] +} diff --git a/pkgs/record_use/test_data/json/tearoff.json b/pkgs/record_use/test_data/json/tearoff.json new file mode 100644 index 0000000000..1a57ee2041 --- /dev/null +++ b/pkgs/record_use/test_data/json/tearoff.json @@ -0,0 +1,31 @@ +{ + "$schema": "../../doc/schema/record_use.schema.json", + "locations": [ + { + "uri": "tearoff.dart" + } + ], + "metadata": { + "comment": "Recorded usages of objects tagged with a `RecordUse` annotation", + "version": "0.4.0" + }, + "recordings": [ + { + "calls": [ + { + "@": 0, + "loading_unit": "1", + "type": "tearoff" + } + ], + "definition": { + "identifier": { + "name": "someStaticMethod", + "scope": "SomeClass", + "uri": "tearoff.dart" + }, + "loading_unit": "1" + } + } + ] +} diff --git a/pkgs/record_use/test_data/json/top_level_method.json b/pkgs/record_use/test_data/json/top_level_method.json new file mode 100644 index 0000000000..d4a0b1dba5 --- /dev/null +++ b/pkgs/record_use/test_data/json/top_level_method.json @@ -0,0 +1,39 @@ +{ + "$schema": "../../doc/schema/record_use.schema.json", + "constants": [ + { + "type": "int", + "value": 42 + } + ], + "locations": [ + { + "uri": "top_level_method.dart" + } + ], + "metadata": { + "comment": "Recorded usages of objects tagged with a `RecordUse` annotation", + "version": "0.4.0" + }, + "recordings": [ + { + "calls": [ + { + "@": 0, + "loading_unit": "1", + "positional": [ + 0 + ], + "type": "with_arguments" + } + ], + "definition": { + "identifier": { + "name": "someTopLevelMethod", + "uri": "top_level_method.dart" + }, + "loading_unit": "1" + } + } + ] +} diff --git a/pkgs/record_use/test_data/json/types_of_arguments.json b/pkgs/record_use/test_data/json/types_of_arguments.json new file mode 100644 index 0000000000..7582cda8f9 --- /dev/null +++ b/pkgs/record_use/test_data/json/types_of_arguments.json @@ -0,0 +1,128 @@ +{ + "$schema": "../../doc/schema/record_use.schema.json", + "constants": [ + { + "type": "int", + "value": 42 + }, + { + "type": "Null" + }, + { + "type": "String", + "value": "s" + }, + { + "type": "bool", + "value": true + }, + { + "type": "String", + "value": "a1" + }, + { + "type": "String", + "value": "a2" + }, + { + "type": "list", + "value": [ + 4, + 5 + ] + }, + { + "type": "String", + "value": "b1" + }, + { + "type": "String", + "value": "b2" + }, + { + "type": "list", + "value": [ + 7, + 8 + ] + }, + { + "type": "map", + "value": { + "a": 6, + "b": 9 + } + } + ], + "locations": [ + { + "uri": "types_of_arguments.dart" + } + ], + "metadata": { + "comment": "Recorded usages of objects tagged with a `RecordUse` annotation", + "version": "0.4.0" + }, + "recordings": [ + { + "calls": [ + { + "@": 0, + "loading_unit": "1", + "positional": [ + 0 + ], + "type": "with_arguments" + }, + { + "@": 0, + "loading_unit": "1", + "positional": [ + 1 + ], + "type": "with_arguments" + }, + { + "@": 0, + "loading_unit": "1", + "positional": [ + 2 + ], + "type": "with_arguments" + }, + { + "@": 0, + "loading_unit": "1", + "positional": [ + 3 + ], + "type": "with_arguments" + }, + { + "@": 0, + "loading_unit": "1", + "positional": [ + 10 + ], + "type": "with_arguments" + }, + { + "@": 0, + "loading_unit": "1", + "positional": [ + null + ], + "type": "with_arguments" + } + ], + "definition": { + "identifier": { + "name": "someStaticMethod", + "scope": "SomeClass", + "uri": "types_of_arguments.dart" + }, + "loading_unit": "1" + } + } + ] +} diff --git a/pkgs/record_use/tool/generate_syntax.dart b/pkgs/record_use/tool/generate_syntax.dart new file mode 100644 index 0000000000..2b46d3d109 --- /dev/null +++ b/pkgs/record_use/tool/generate_syntax.dart @@ -0,0 +1,42 @@ +// Copyright (c) 2025, 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 'dart:convert'; +import 'dart:io'; + +import 'package:json_schema/json_schema.dart'; +import 'package:json_syntax_generator/json_syntax_generator.dart'; + +void main(List args) { + final schemaFile = File.fromUri( + Platform.script.resolve('../doc/schema/record_use.schema.json'), + ); + final schemaJson = jsonDecode(schemaFile.readAsStringSync()) as Map; + final schema = JsonSchema.create(schemaJson); + + final analyzedSchema = SchemaAnalyzer( + schema, + nameOverrides: {'@': 'at'}, + ).analyze(); + final textDumpFile = File.fromUri( + Platform.script.resolve('../lib/src/syntax.g.txt'), + ); + textDumpFile.parent.createSync(recursive: true); + if (args.contains('-d')) { + textDumpFile.writeAsStringSync(analyzedSchema.toString()); + } else if (textDumpFile.existsSync()) { + textDumpFile.deleteSync(); + } + final output = SyntaxGenerator( + analyzedSchema, + header: ''' +// This file is generated, do not edit. +// File generated by pkg/record_use/tool/generate_syntax.dart. +''', + ).generate(); + final outputUri = Platform.script.resolve('../lib/src/syntax.g.dart'); + File.fromUri(outputUri).writeAsStringSync(output); + Process.runSync(Platform.executable, ['format', outputUri.toFilePath()]); + print('Generated $outputUri'); +} diff --git a/tool/ci.dart b/tool/ci.dart index a96dd1ba35..6e6335e9f8 100644 --- a/tool/ci.dart +++ b/tool/ci.dart @@ -179,6 +179,7 @@ class GenerateTask extends Task { 'pkgs/hooks/tool/normalize.dart', 'pkgs/hooks/tool/update_snippets.dart', 'pkgs/pub_formats/tool/generate.dart', + 'pkgs/record_use/tool/generate_syntax.dart', ]; for (final generator in generators) { await _runProcess('dart', [generator, '--set-exit-if-changed']);