TNG / ArchUnit

A Java architecture test library, to specify and assert architecture rules in plain Java
http://archunit.org
Apache License 2.0
3.17k stars 287 forks source link

Is there a way to know if JavaCodeUnit is a switch/case statement? #1242

Closed goblinbr closed 7 months ago

goblinbr commented 7 months ago

I want to check if there's any call to hashCode(), but the problem is that JVM calls hashCode() at a switch statement and I want to ignore this case.

Example:

package com.github.goblinbr.archunitswitchtest;

import java.util.HashMap;
import java.util.Map;

public class UseHashCode {
    public String useHashCode() {
        Map<Integer, String> map = new HashMap<>();
        map.put("A".hashCode(), "AAAA");
        map.put("B".hashCode(), "BBBB");

        return map.get("A".hashCode());
    }

    public String useSwitch() {
        String str = "A";
        switch (str) {
            case "A":
                return "AAAA";
            case "B":
                return "BBBB";
        }
        return "";
    }
}

package com.github.goblinbr.archunitswitchtest;

import com.tngtech.archunit.core.domain.JavaClass;
import com.tngtech.archunit.core.domain.JavaClasses;
import com.tngtech.archunit.core.domain.JavaCodeUnit;
import com.tngtech.archunit.core.importer.ClassFileImporter;
import com.tngtech.archunit.lang.ArchCondition;
import com.tngtech.archunit.lang.ConditionEvents;
import com.tngtech.archunit.lang.SimpleConditionEvent;
import org.junit.Test;

import java.util.stream.Stream;

import static com.tngtech.archunit.lang.syntax.ArchRuleDefinition.noClasses;

public class UseHashCodeTest {
    @Test
    public void checkHashCodeCall() {
        JavaClasses classes = new ClassFileImporter().importPackages("com.github.goblinbr.archunitswitchtest");

        noClasses()
                .should(new ArchCondition<JavaClass>("") {
                    @Override
                    public void check(JavaClass javaClass, ConditionEvents events) {
                        Stream.concat(javaClass.getMethodCallsFromSelf().stream(), javaClass.getMethodReferencesFromSelf().stream())
                                .filter(jmc -> !jmc.getOrigin().getName().equals("hashCode") &&
                                        jmc.getTarget().getName().equals("hashCode") &&
                                        jmc.getTarget().getParameterTypes().isEmpty() &&
                                        !isSwitchCase(jmc.getOrigin())
                                )
                                .forEach(jmc -> events.add(SimpleConditionEvent.satisfied(javaClass, jmc.getDescription())));
                    }

                    private boolean isSwitchCase(JavaCodeUnit jcu) {
                        // TODO: Check if the method call is inside a switch case
                        return false;
                    }
                })
                .as("No class should call hashCode()")
                .check(classes);
    }
}
hankem commented 7 months ago

I don't think that ArchUnit (currently) recognizes switch statements, sorry!

hankem commented 7 months ago

By the way: JavaCodeUnit is the base class of JavaMethod, JavaConstructor and JavaStaticInitializer (see also Domain model), so you could check whether a code unit has switch statements, but IMO not whether a code unit is a switch statement.

goblinbr commented 7 months ago

I will close this issue because I found a way to "solve"

When it's a switch statement java also calls equals method at the same code line, so I just ignore cases where there's also a equals call at the same line. It may ignore some cases like "aaa.hashCode() == bbb.hashCode() && aaa.equals(ccc)" but it's good enough for me.

package com.github.goblinbr.archunitswitchtest;

import com.tngtech.archunit.core.domain.*;
import com.tngtech.archunit.core.importer.ClassFileImporter;
import com.tngtech.archunit.lang.ArchCondition;
import com.tngtech.archunit.lang.ConditionEvents;
import com.tngtech.archunit.lang.SimpleConditionEvent;
import org.junit.Test;

import java.util.stream.Stream;

import static com.tngtech.archunit.lang.syntax.ArchRuleDefinition.noClasses;

public class UseHashCodeTest {
    @Test
    public void checkHashCodeCall() {
        JavaClasses classes = new ClassFileImporter().importPackages("com.github.goblinbr.archunitswitchtest");

        noClasses()
                .should(new ArchCondition<JavaClass>("") {
                    @Override
                    public void check(JavaClass javaClass, ConditionEvents events) {
                        Stream.concat(javaClass.getMethodCallsFromSelf().stream(), javaClass.getMethodReferencesFromSelf().stream())
                                .filter(jmc -> !jmc.getOrigin().getName().equals("hashCode") &&
                                        jmc.getTarget().getName().equals("hashCode") &&
                                        jmc.getTarget().getParameterTypes().isEmpty() &&
                                        !isSwitchCase(jmc)
                                )
                                .forEach(jmc -> events.add(SimpleConditionEvent.satisfied(javaClass, jmc.getDescription())));
                    }

                    private boolean isSwitchCase(JavaCodeUnitAccess<? extends AccessTarget.CodeUnitAccessTarget> jmc) {
                        if (jmc.getTarget().getFullName().equals("java.lang.String.hashCode()")) {
                            return jmc.getOrigin().getMethodCallsFromSelf().stream()
                                    .anyMatch(e -> e.getLineNumber() == jmc.getLineNumber() &&
                                            e.getTarget().getFullName().equals("java.lang.String.equals(java.lang.Object)"));
                        }
                        return false;
                    }
                })
                .as("No class should call hashCode()")
                .check(classes);
    }
}
hankem commented 7 months ago

Sounds like a pragmatic hack workaround! 😅