Search code examples
javaarchunit

How to assert HashMap keys to be Comparable with ArchUnit?


Background: https://dev.to/carey/java-map-keys-should-always-be-comparable-2c1b

What I want to achieve:

  1. Find code that uses HashMap.
  2. Figure out the type of the HashMaps key.
  3. Check if the key type implements Comparable interface.
  4. (optional) Check if that type resides in a certain package.

I a stuck at step 2

noClasses().that()
        .containAnyFieldsThat(have(rawType(HashMap.class)))
        .should()...

Any ideas how I can get the type of a generic class? Thanks for support.


Solution

  • The crucial part of your ArchRule (which can be directly based on fields() or codeUnits()) can be expressed with custom ArchConditions:

    @ArchTest
    static final ArchRule fields_of_type_HashMap_should_have_Comparable_key = fields()
        .that().haveRawType(HashMap.class)
        .should(haveComparableFirstTypeParameter());
    
    @ArchTest
    static final ArchRule code_units_should_have_parameters_of_type_HashMap_with_Comparable_key = codeUnits()
        .should(new ArchCondition<JavaCodeUnit>("have parameters of type HashMap with Comparable key") {
            @Override
            public void check(JavaCodeUnit javaCodeUnit, ConditionEvents events) {
                javaCodeUnit.getParameters().forEach(parameter -> {
                    if (parameter.getRawType().isEquivalentTo(HashMap.class)) {
                        haveComparableFirstTypeParameter().check(parameter, events);
                    }
                });
            }
        });
    
    @ArchTest
    static final ArchRule methods_with_return_type_HashMap_should_have_return_types_with_Comparable_key = methods()
        .that().haveRawReturnType(HashMap.class)
        .should(new ArchCondition<JavaMethod>("have return type with Comparable key") {
            @Override
            public void check(JavaMethod method, ConditionEvents events) {
                class ReturnType implements HasType, HasDescription {
                    @Override
                    public JavaType getType() { return method.getReturnType(); }
                    @Override
                    public JavaClass getRawType() { return method.getRawReturnType(); }
                    @Override
                    public String getDescription() { return "Return type <" + getType().getName() + "> of " + method.getDescription(); }
                }
                haveComparableFirstTypeParameter().check(new ReturnType(), events);
            }
        });
    
    private static <T extends HasType & HasDescription> ArchCondition<T> haveComparableFirstTypeParameter() {
        return new ArchCondition<T>("have Comparable first type parameter") {
            @Override
            public void check(T typed, ConditionEvents events) {
                JavaType fieldType = typed.getType();
                if (fieldType instanceof JavaParameterizedType) {
                    JavaType keyType = ((JavaParameterizedType) fieldType).getActualTypeArguments().get(0);
                    boolean satisfied = keyType.toErasure().getAllRawInterfaces().stream()
                            .anyMatch(rawInterface -> rawInterface.isEquivalentTo(Comparable.class));
                    String message = String.format("%s has a first type parameter %s that %s Comparable",
                            typed.getDescription(), keyType.getName(), satisfied ? "is" : "is not");
                    events.add(new SimpleConditionEvent(typed, satisfied, message));
                } else {
                    events.add(SimpleConditionEvent.violated(typed, typed.getDescription() + " is not parameterized"));
                }
            }
        };
    }