From ec3827ba252e44fe9132c733e50ed5c3fcaa68fa Mon Sep 17 00:00:00 2001 From: dondonz <13839920+dondonz@users.noreply.github.com> Date: Sat, 16 Aug 2025 08:25:48 +1000 Subject: [PATCH 1/3] Add Archunit test to enforce no Map.of --- .../graphql/archunit/MapOfUsageTest.groovy | 36 +++++++++++++++++++ 1 file changed, 36 insertions(+) create mode 100644 src/test/groovy/graphql/archunit/MapOfUsageTest.groovy diff --git a/src/test/groovy/graphql/archunit/MapOfUsageTest.groovy b/src/test/groovy/graphql/archunit/MapOfUsageTest.groovy new file mode 100644 index 0000000000..5a77aa3c89 --- /dev/null +++ b/src/test/groovy/graphql/archunit/MapOfUsageTest.groovy @@ -0,0 +1,36 @@ +package graphql.archunit + +import com.tngtech.archunit.core.domain.JavaClasses +import com.tngtech.archunit.core.importer.ClassFileImporter +import com.tngtech.archunit.core.importer.ImportOption +import com.tngtech.archunit.lang.ArchRule +import com.tngtech.archunit.lang.EvaluationResult +import com.tngtech.archunit.lang.syntax.ArchRuleDefinition +import spock.lang.Specification + +class MapOfUsageTest extends Specification { + + private static final JavaClasses importedClasses = new ClassFileImporter() + .withImportOption(ImportOption.Predefined.DO_NOT_INCLUDE_TESTS) + .importPackages("graphql") + + def "should not use Map.of()"() { + given: + ArchRule mapOfRule = ArchRuleDefinition.noClasses() + .should() + .callMethod(Map.class, "of") + .because("Map.of() does not guarantee insertion order. Use LinkedHashMap instead for consistent serialization order.") + + when: + EvaluationResult result = mapOfRule.evaluate(importedClasses) + + then: + if (result.hasViolation()) { + println "Map.of() usage detected. Please use LinkedHashMap instead for consistent serialization order:" + result.getFailureReport().getDetails().each { violation -> + println "- ${violation}" + } + } + !result.hasViolation() + } +} \ No newline at end of file From de40420e231b941486fefd9300db24c092bc6994 Mon Sep 17 00:00:00 2001 From: dondonz <13839920+dondonz@users.noreply.github.com> Date: Sat, 16 Aug 2025 08:56:36 +1000 Subject: [PATCH 2/3] Add LinkedHashMap factory --- .../graphql/util/LinkedHashMapFactory.java | 344 ++++++++++++++++++ .../graphql/archunit/MapOfUsageTest.groovy | 4 +- 2 files changed, 346 insertions(+), 2 deletions(-) create mode 100644 src/main/java/graphql/util/LinkedHashMapFactory.java diff --git a/src/main/java/graphql/util/LinkedHashMapFactory.java b/src/main/java/graphql/util/LinkedHashMapFactory.java new file mode 100644 index 0000000000..b9cc20ef75 --- /dev/null +++ b/src/main/java/graphql/util/LinkedHashMapFactory.java @@ -0,0 +1,344 @@ +package graphql.util; + +import graphql.Internal; + +import java.util.LinkedHashMap; +import java.util.Map; + +/** + * Factory class for creating LinkedHashMap instances with insertion order preservation. + * Use this instead of Map.of() to ensure consistent serialization order. + *

+ * This class provides static factory methods similar to Map.of() but returns mutable LinkedHashMap + * instances that preserve insertion order, which is important for consistent serialization. + */ +@Internal +public final class LinkedHashMapFactory { + + private LinkedHashMapFactory() { + // utility class + } + + /** + * Returns an empty LinkedHashMap. + * + * @param the key type + * @param the value type + * @return an empty LinkedHashMap + */ + public static Map of() { + return new LinkedHashMap<>(); + } + + /** + * Returns a LinkedHashMap containing a single mapping. + * + * @param the key type + * @param the value type + * @param k1 the mapping's key + * @param v1 the mapping's value + * @return a LinkedHashMap containing the specified mapping + */ + public static Map of(K k1, V v1) { + Map map = new LinkedHashMap<>(1); + map.put(k1, v1); + return map; + } + + /** + * Returns a LinkedHashMap containing two mappings. + * + * @param the key type + * @param the value type + * @param k1 the first mapping's key + * @param v1 the first mapping's value + * @param k2 the second mapping's key + * @param v2 the second mapping's value + * @return a LinkedHashMap containing the specified mappings + */ + public static Map of(K k1, V v1, K k2, V v2) { + Map map = new LinkedHashMap<>(2); + map.put(k1, v1); + map.put(k2, v2); + return map; + } + + /** + * Returns a LinkedHashMap containing three mappings. + * + * @param the key type + * @param the value type + * @param k1 the first mapping's key + * @param v1 the first mapping's value + * @param k2 the second mapping's key + * @param v2 the second mapping's value + * @param k3 the third mapping's key + * @param v3 the third mapping's value + * @return a LinkedHashMap containing the specified mappings + */ + public static Map of(K k1, V v1, K k2, V v2, K k3, V v3) { + Map map = new LinkedHashMap<>(3); + map.put(k1, v1); + map.put(k2, v2); + map.put(k3, v3); + return map; + } + + /** + * Returns a LinkedHashMap containing four mappings. + * + * @param the key type + * @param the value type + * @param k1 the first mapping's key + * @param v1 the first mapping's value + * @param k2 the second mapping's key + * @param v2 the second mapping's value + * @param k3 the third mapping's key + * @param v3 the third mapping's value + * @param k4 the fourth mapping's key + * @param v4 the fourth mapping's value + * @return a LinkedHashMap containing the specified mappings + */ + public static Map of(K k1, V v1, K k2, V v2, K k3, V v3, K k4, V v4) { + Map map = new LinkedHashMap<>(4); + map.put(k1, v1); + map.put(k2, v2); + map.put(k3, v3); + map.put(k4, v4); + return map; + } + + /** + * Returns a LinkedHashMap containing five mappings. + * + * @param the key type + * @param the value type + * @param k1 the first mapping's key + * @param v1 the first mapping's value + * @param k2 the second mapping's key + * @param v2 the second mapping's value + * @param k3 the third mapping's key + * @param v3 the third mapping's value + * @param k4 the fourth mapping's key + * @param v4 the fourth mapping's value + * @param k5 the fifth mapping's key + * @param v5 the fifth mapping's value + * @return a LinkedHashMap containing the specified mappings + */ + public static Map of(K k1, V v1, K k2, V v2, K k3, V v3, K k4, V v4, K k5, V v5) { + Map map = new LinkedHashMap<>(5); + map.put(k1, v1); + map.put(k2, v2); + map.put(k3, v3); + map.put(k4, v4); + map.put(k5, v5); + return map; + } + + /** + * Returns a LinkedHashMap containing six mappings. + * + * @param the key type + * @param the value type + * @param k1 the first mapping's key + * @param v1 the first mapping's value + * @param k2 the second mapping's key + * @param v2 the second mapping's value + * @param k3 the third mapping's key + * @param v3 the third mapping's value + * @param k4 the fourth mapping's key + * @param v4 the fourth mapping's value + * @param k5 the fifth mapping's key + * @param v5 the fifth mapping's value + * @param k6 the sixth mapping's key + * @param v6 the sixth mapping's value + * @return a LinkedHashMap containing the specified mappings + */ + public static Map of(K k1, V v1, K k2, V v2, K k3, V v3, K k4, V v4, K k5, V v5, K k6, V v6) { + Map map = new LinkedHashMap<>(6); + map.put(k1, v1); + map.put(k2, v2); + map.put(k3, v3); + map.put(k4, v4); + map.put(k5, v5); + map.put(k6, v6); + return map; + } + + /** + * Returns a LinkedHashMap containing seven mappings. + * + * @param the key type + * @param the value type + * @param k1 the first mapping's key + * @param v1 the first mapping's value + * @param k2 the second mapping's key + * @param v2 the second mapping's value + * @param k3 the third mapping's key + * @param v3 the third mapping's value + * @param k4 the fourth mapping's key + * @param v4 the fourth mapping's value + * @param k5 the fifth mapping's key + * @param v5 the fifth mapping's value + * @param k6 the sixth mapping's key + * @param v6 the sixth mapping's value + * @param k7 the seventh mapping's key + * @param v7 the seventh mapping's value + * @return a LinkedHashMap containing the specified mappings + */ + public static Map of(K k1, V v1, K k2, V v2, K k3, V v3, K k4, V v4, K k5, V v5, K k6, V v6, K k7, V v7) { + Map map = new LinkedHashMap<>(7); + map.put(k1, v1); + map.put(k2, v2); + map.put(k3, v3); + map.put(k4, v4); + map.put(k5, v5); + map.put(k6, v6); + map.put(k7, v7); + return map; + } + + /** + * Returns a LinkedHashMap containing eight mappings. + * + * @param the key type + * @param the value type + * @param k1 the first mapping's key + * @param v1 the first mapping's value + * @param k2 the second mapping's key + * @param v2 the second mapping's value + * @param k3 the third mapping's key + * @param v3 the third mapping's value + * @param k4 the fourth mapping's key + * @param v4 the fourth mapping's value + * @param k5 the fifth mapping's key + * @param v5 the fifth mapping's value + * @param k6 the sixth mapping's key + * @param v6 the sixth mapping's value + * @param k7 the seventh mapping's key + * @param v7 the seventh mapping's value + * @param k8 the eighth mapping's key + * @param v8 the eighth mapping's value + * @return a LinkedHashMap containing the specified mappings + */ + public static Map of(K k1, V v1, K k2, V v2, K k3, V v3, K k4, V v4, K k5, V v5, K k6, V v6, K k7, V v7, K k8, V v8) { + Map map = new LinkedHashMap<>(8); + map.put(k1, v1); + map.put(k2, v2); + map.put(k3, v3); + map.put(k4, v4); + map.put(k5, v5); + map.put(k6, v6); + map.put(k7, v7); + map.put(k8, v8); + return map; + } + + /** + * Returns a LinkedHashMap containing nine mappings. + * + * @param the key type + * @param the value type + * @param k1 the first mapping's key + * @param v1 the first mapping's value + * @param k2 the second mapping's key + * @param v2 the second mapping's value + * @param k3 the third mapping's key + * @param v3 the third mapping's value + * @param k4 the fourth mapping's key + * @param v4 the fourth mapping's value + * @param k5 the fifth mapping's key + * @param v5 the fifth mapping's value + * @param k6 the sixth mapping's key + * @param v6 the sixth mapping's value + * @param k7 the seventh mapping's key + * @param v7 the seventh mapping's value + * @param k8 the eighth mapping's key + * @param v8 the eighth mapping's value + * @param k9 the ninth mapping's key + * @param v9 the ninth mapping's value + * @return a LinkedHashMap containing the specified mappings + */ + public static Map of(K k1, V v1, K k2, V v2, K k3, V v3, K k4, V v4, K k5, V v5, K k6, V v6, K k7, V v7, K k8, V v8, K k9, V v9) { + Map map = new LinkedHashMap<>(9); + map.put(k1, v1); + map.put(k2, v2); + map.put(k3, v3); + map.put(k4, v4); + map.put(k5, v5); + map.put(k6, v6); + map.put(k7, v7); + map.put(k8, v8); + map.put(k9, v9); + return map; + } + + /** + * Returns a LinkedHashMap containing ten mappings. + * + * @param the key type + * @param the value type + * @param k1 the first mapping's key + * @param v1 the first mapping's value + * @param k2 the second mapping's key + * @param v2 the second mapping's value + * @param k3 the third mapping's key + * @param v3 the third mapping's value + * @param k4 the fourth mapping's key + * @param v4 the fourth mapping's value + * @param k5 the fifth mapping's key + * @param v5 the fifth mapping's value + * @param k6 the sixth mapping's key + * @param v6 the sixth mapping's value + * @param k7 the seventh mapping's key + * @param v7 the seventh mapping's value + * @param k8 the eighth mapping's key + * @param v8 the eighth mapping's value + * @param k9 the ninth mapping's key + * @param v9 the ninth mapping's value + * @param k10 the tenth mapping's key + * @param v10 the tenth mapping's value + * @return a LinkedHashMap containing the specified mappings + */ + public static Map of(K k1, V v1, K k2, V v2, K k3, V v3, K k4, V v4, K k5, V v5, K k6, V v6, K k7, V v7, K k8, V v8, K k9, V v9, K k10, V v10) { + Map map = new LinkedHashMap<>(10); + map.put(k1, v1); + map.put(k2, v2); + map.put(k3, v3); + map.put(k4, v4); + map.put(k5, v5); + map.put(k6, v6); + map.put(k7, v7); + map.put(k8, v8); + map.put(k9, v9); + map.put(k10, v10); + return map; + } + + /** + * Returns a LinkedHashMap containing mappings derived from the given arguments. + *

+ * This method is provided for cases where more than 10 key-value pairs are needed. + * It accepts alternating keys and values. + * + * @param the key type + * @param the value type + * @param keyValues alternating keys and values + * @return a LinkedHashMap containing the specified mappings + * @throws IllegalArgumentException if an odd number of arguments is provided + */ + @SuppressWarnings("unchecked") + public static Map ofEntries(Object... keyValues) { + if (keyValues.length % 2 != 0) { + throw new IllegalArgumentException("keyValues must contain an even number of arguments (key-value pairs)"); + } + + Map map = new LinkedHashMap<>(keyValues.length / 2); + for (int i = 0; i < keyValues.length; i += 2) { + K key = (K) keyValues[i]; + V value = (V) keyValues[i + 1]; + map.put(key, value); + } + return map; + } +} \ No newline at end of file diff --git a/src/test/groovy/graphql/archunit/MapOfUsageTest.groovy b/src/test/groovy/graphql/archunit/MapOfUsageTest.groovy index 5a77aa3c89..9f6a4471c7 100644 --- a/src/test/groovy/graphql/archunit/MapOfUsageTest.groovy +++ b/src/test/groovy/graphql/archunit/MapOfUsageTest.groovy @@ -19,14 +19,14 @@ class MapOfUsageTest extends Specification { ArchRule mapOfRule = ArchRuleDefinition.noClasses() .should() .callMethod(Map.class, "of") - .because("Map.of() does not guarantee insertion order. Use LinkedHashMap instead for consistent serialization order.") + .because("Map.of() does not guarantee insertion order. Use LinkedHashMapFactory.of() instead for consistent serialization order.") when: EvaluationResult result = mapOfRule.evaluate(importedClasses) then: if (result.hasViolation()) { - println "Map.of() usage detected. Please use LinkedHashMap instead for consistent serialization order:" + println "Map.of() usage detected. Please use LinkedHashMapFactory.of() instead for consistent serialization order:" result.getFailureReport().getDetails().each { violation -> println "- ${violation}" } From 11d13578c1b47c5d6273afee193858831c825a39 Mon Sep 17 00:00:00 2001 From: dondonz <13839920+dondonz@users.noreply.github.com> Date: Sat, 16 Aug 2025 08:58:27 +1000 Subject: [PATCH 3/3] Switch existing Map.of usage to new linked hash map --- .../ExecutableNormalizedOperationToAstCompiler.java | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/main/java/graphql/normalized/ExecutableNormalizedOperationToAstCompiler.java b/src/main/java/graphql/normalized/ExecutableNormalizedOperationToAstCompiler.java index 9509d9554b..f71e1a30e8 100644 --- a/src/main/java/graphql/normalized/ExecutableNormalizedOperationToAstCompiler.java +++ b/src/main/java/graphql/normalized/ExecutableNormalizedOperationToAstCompiler.java @@ -24,6 +24,7 @@ import graphql.schema.GraphQLObjectType; import graphql.schema.GraphQLSchema; import graphql.schema.GraphQLUnmodifiedType; +import graphql.util.LinkedHashMapFactory; import org.jspecify.annotations.NonNull; import org.jspecify.annotations.Nullable; @@ -100,7 +101,7 @@ public static CompilerResult compileToDocument(@NonNull GraphQLSchema schema, @Nullable String operationName, @NonNull List topLevelFields, @Nullable VariablePredicate variablePredicate) { - return compileToDocument(schema, operationKind, operationName, topLevelFields, Map.of(), variablePredicate); + return compileToDocument(schema, operationKind, operationName, topLevelFields, LinkedHashMapFactory.of(), variablePredicate); } /** @@ -151,7 +152,7 @@ public static CompilerResult compileToDocumentWithDeferSupport(@NonNull GraphQLS @NonNull List topLevelFields, @Nullable VariablePredicate variablePredicate ) { - return compileToDocumentWithDeferSupport(schema, operationKind, operationName, topLevelFields, Map.of(), variablePredicate); + return compileToDocumentWithDeferSupport(schema, operationKind, operationName, topLevelFields, LinkedHashMapFactory.of(), variablePredicate); } /**