| // 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:args/args.dart'; | |
| import 'package:native_test_helpers/native_test_helpers.dart'; | |
| import 'package:path/path.dart' as p; | |
| void main(List<String> arguments) { | |
| final stopwatch = Stopwatch()..start(); | |
| final parser = ArgParser() | |
| ..addFlag( | |
| 'set-exit-if-changed', | |
| negatable: false, | |
| help: 'Return a non-zero exit code if any files were changed.', | |
| ); | |
| final argResults = parser.parse(arguments); | |
| final setExitIfChanged = argResults['set-exit-if-changed'] as bool; | |
| var processedCount = 0; | |
| var changedCount = 0; | |
| final packageUri = findPackageRoot('hooks'); | |
| final directories = [ | |
| Directory.fromUri(packageUri), | |
| 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); | |
| processedCount += result.processedCount; | |
| changedCount += result.changedCount; | |
| } | |
| stopwatch.stop(); | |
| final duration = stopwatch.elapsedMilliseconds / 1000.0; | |
| print( | |
| 'Normalized $processedCount files ($changedCount changed) in ' | |
| '${duration.toStringAsFixed(2)} seconds.', | |
| ); | |
| if (setExitIfChanged && changedCount > 0) { | |
| exit(1); | |
| } | |
| } | |
| class ProcessDirectoryResult { | |
| final int processedCount; | |
| final int changedCount; | |
| ProcessDirectoryResult(this.processedCount, this.changedCount); | |
| } | |
| ProcessDirectoryResult processDirectory(Directory directory) { | |
| var processedCount = 0; | |
| var changedCount = 0; | |
| final entities = directory.listSync(recursive: true); | |
| for (final entity in entities) { | |
| if (entity is File && | |
| p.extension(entity.path) == '.json' && | |
| // 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; | |
| } | |
| } | |
| } | |
| return ProcessDirectoryResult(processedCount, changedCount); | |
| } | |
| bool processFile(File file) { | |
| final contents = file.readAsStringSync(); | |
| final dynamic decoded = json.decode(contents); | |
| final sorted = sortJson(decoded, file.path); | |
| const encoder = JsonEncoder.withIndent(' '); | |
| final sortedJson = encoder.convert(sorted); // Already has no trailing newline | |
| final newContents = '$sortedJson\n'; | |
| final newContentNormalized = newContents.replaceAll('\r\n', '\n'); | |
| final oldContentNormalized = contents.replaceAll('\r\n', '\n'); | |
| if (newContentNormalized == oldContentNormalized) { | |
| return false; | |
| } | |
| file.writeAsStringSync(newContents); | |
| print('Normalized: ${file.path}'); | |
| return true; | |
| } | |
| const List<String> _orderedKeysInSchemas = [ | |
| // Schema Identification: Defines the JSON Schema version and identifier. | |
| // Should be at the top. | |
| '\$schema', | |
| '\$id', | |
| // Informational: Keyword for adding comments to the schema. | |
| '\$comment', | |
| // References to other schemas. | |
| '\$ref', | |
| // Schema Metadata: Human-readable information about the schema. | |
| 'title', | |
| 'description', | |
| // Core Types: The basic data types and related keywords. | |
| 'type', | |
| 'enum', | |
| 'const', | |
| // Object Schemas: Keywords for defining and validating JSON objects. | |
| 'properties', | |
| 'required', | |
| 'additionalProperties', | |
| 'patternProperties', | |
| 'unevaluatedProperties', | |
| // Array Schemas: Keywords for defining and validating JSON arrays. | |
| 'items', | |
| 'prefixItems', | |
| 'contains', | |
| 'minContains', | |
| 'maxContains', | |
| // Combining Schemas: Keywords for combining and manipulating schemas. | |
| 'allOf', | |
| 'anyOf', | |
| 'oneOf', | |
| 'not', | |
| // Conditional Application: Keywords for applying schemas conditionally. | |
| 'if', | |
| 'then', | |
| 'else', | |
| 'dependentSchemas', | |
| 'dependentRequired', | |
| // Reusable Definitions: Keywords for defining and referencing reusable schema | |
| // components. | |
| '\$defs', | |
| 'definitions', | |
| // Semantic Validation: Keywords for validating data based on its semantic | |
| // type. | |
| 'format', | |
| 'examples', | |
| 'default', | |
| // Metadata Annotations: Keywords for adding metadata annotations to schemas. | |
| 'readOnly', | |
| 'writeOnly', | |
| 'deprecated', | |
| // Numeric Validation: Keywords for validating numeric data. | |
| 'multipleOf', | |
| 'maximum', | |
| 'exclusiveMaximum', | |
| 'minimum', | |
| 'exclusiveMinimum', | |
| // String Validation: Keywords for validating string data. | |
| 'maxLength', | |
| 'minLength', | |
| 'pattern', | |
| // Array Validation: Keywords for validating array data. | |
| 'maxItems', | |
| 'minItems', | |
| 'uniqueItems', | |
| // Object Validation: Keywords for validating object data. | |
| 'maxProperties', | |
| 'minProperties', | |
| ]; | |
| dynamic sortJson(dynamic data, String filePath) { | |
| if (data is Map<String, Object?>) { | |
| final sortedMap = <String, Object?>{}; | |
| final keys = data.keys.toList(); | |
| final isSchema = filePath.endsWith('schema.json'); | |
| if (isSchema) { | |
| keys.sort((a, b) { | |
| final aIndex = _orderedKeysInSchemas.indexOf(a); | |
| final bIndex = _orderedKeysInSchemas.indexOf(b); | |
| if (aIndex == -1 && bIndex == -1) { | |
| // Both keys are not in _orderedKeys, sort alphabetically. | |
| return a.compareTo(b); | |
| } else if (aIndex == -1) { | |
| // Only b is in _orderedKeys, sort b first | |
| return 1; | |
| } else if (bIndex == -1) { | |
| // Only a is in _orderedKeys, sort a first | |
| return -1; | |
| } else { | |
| return aIndex.compareTo(bIndex); | |
| } | |
| }); | |
| } else { | |
| // Sort keys alphabetically for non-schemas. | |
| keys.sort(); | |
| } | |
| for (final key in keys) { | |
| sortedMap[key] = sortJson(data[key], filePath); | |
| } | |
| return sortedMap; | |
| } | |
| if (data is List) { | |
| return data.map((item) => sortJson(item, filePath)).toList() | |
| ..sort(_compareTwoItems); | |
| } | |
| return data; | |
| } | |
| int _compareTwoItems(dynamic a, dynamic b) { | |
| if (a is Map && b is Map) { | |
| return compareMaps(a, 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.'); | |
| } | |
| int compareMaps(Map<dynamic, dynamic> a, Map<dynamic, dynamic> b) { | |
| final aKeys = a.keys.toList(); | |
| final bKeys = b.keys.toList(); | |
| for (var i = 0; i < aKeys.length && i < bKeys.length; i++) { | |
| final keyComparison = _compareTwoItems(aKeys[i], bKeys[i]); | |
| if (keyComparison != 0) { | |
| return keyComparison; | |
| } | |
| final aValue = a[aKeys[i]]; | |
| final bValue = b[bKeys[i]]; | |
| final valueComparison = _compareTwoItems(aValue, bValue); | |
| if (valueComparison != 0) { | |
| return valueComparison; | |
| } | |
| } | |
| return 0; | |
| } | |
| int compareLists(List<dynamic> a, List<dynamic> b) { | |
| for (var i = 0; i < a.length && i < b.length; i++) { | |
| final comparison = _compareTwoItems(a[i], b[i]); | |
| if (comparison != 0) { | |
| return comparison; | |
| } | |
| } | |
| // If all common elements are equal, the shorter list comes first. | |
| return a.length.compareTo(b.length); | |
| } |