Skip to content

Studying lambda parameters for a forEach method call #1379

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
lasselindqvist opened this issue Nov 12, 2024 · 1 comment
Open

Studying lambda parameters for a forEach method call #1379

lasselindqvist opened this issue Nov 12, 2024 · 1 comment

Comments

@lasselindqvist
Copy link

lasselindqvist commented Nov 12, 2024

I am trying to write a rule such as:

	@ArchTest
	public static final ArchRule ruleSimple = methods().that()
			.areAnnotatedWith(RequestMapping.class)
			.should(new ArchCondition<JavaMethod>("call validator") {
				@Override
				public void check(JavaMethod item, ConditionEvents events) {
					if (!item.isAnnotatedWith(ValidationNotNeeded.class)) {
						List<JavaMethodCall> callsToValidator = item.getMethodCallsFromSelf().stream()
								.filter(call -> call.getTargetOwner().isAssignableFrom(Validator.class))
								.toList();
						if (callsToValidator.isEmpty()) {
							events.add(SimpleConditionEvent.violated(item,"No calls to Validator from " + item.getFullName()));
						}
					}
				}
			});

so as to enforce that in certain places all methods must call a method from Validator class.
This works fine and also works fine even if the calls are inside for loops. But case of forEach, the calls go to forEach method, not the methods that are called via the lambda passed onto it.

So

List<JavaMethodCall> calls = item.getMethodCallsFromSelf().stream()
.filter(jmc -> jmc.getName().equals("forEach"))
.toList();

find the forEach calls just fine. From these JavaMethodCall objects it is also possible to find the parameters passed on to forEach, but those are Consumers, not the lambdas themselves.

Is it even possible with ArchUnit to study what is passed as the Consumer?

As a bonus, this works differently with javac and Eclipse Compiler. When compiled with javac, the calls are found directly, but with Eclipse Compiler, the forEach calls are found.

Workaround:
Simple workaround is to just replace forEach calls with normal loops, but it is not ideal in all cases.

@ljluestc
Copy link

ljluestc commented Apr 5, 2025


import com.tngtech.archunit.core.domain.JavaMethod;
import com.tngtech.archunit.core.domain.JavaMethodCall;
import com.tngtech.archunit.lang.ArchCondition;
import com.tngtech.archunit.lang.ArchRule;
import com.tngtech.archunit.lang.ConditionEvents;
import com.tngtech.archunit.lang.SimpleConditionEvent;
import org.springframework.web.bind.annotation.RequestMapping;

import java.util.List;
import java.util.stream.Collectors;

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

public class ValidatorRule {

    @ArchTest
    public static final ArchRule ruleEnhanced = methods().that()
            .areAnnotatedWith(RequestMapping.class)
            .should(new ArchCondition<JavaMethod>("call validator or properly handle forEach") {
                @Override
                public void check(JavaMethod method, ConditionEvents events) {
                    // Skip if annotated with ValidationNotNeeded
                    if (method.isAnnotatedWith(ValidationNotNeeded.class)) {
                        return;
                    }

                    // Check direct calls to Validator
                    List<JavaMethodCall> directValidatorCalls = method.getMethodCallsFromSelf().stream()
                            .filter(call -> call.getTargetOwner().isAssignableFrom(Validator.class))
                            .collect(Collectors.toList());

                    if (!directValidatorCalls.isEmpty()) {
                        return; // Direct call found, rule satisfied
                    }

                    // Check forEach calls
                    List<JavaMethodCall> forEachCalls = method.getMethodCallsFromSelf().stream()
                            .filter(call -> call.getName().equals("forEach") &&
                                    call.getTargetOwner().isAssignableFrom(List.class))
                            .collect(Collectors.toList());

                    if (forEachCalls.isEmpty()) {
                        // No forEach, no direct calls -> violation
                        events.add(SimpleConditionEvent.violated(method,
                                "No calls to Validator from " + method.getFullName()));
                        return;
                    }

                    // Analyze each forEach call
                    for (JavaMethodCall forEachCall : forEachCalls) {
                        // Ideally, inspect the Consumer's body, but ArchUnit limits this.
                        // Check if the lambda might call Validator via heuristic or convention
                        boolean hasValidatorCall = checkForEachConsumer(forEachCall);
                        if (!hasValidatorCall) {
                            events.add(SimpleConditionEvent.violated(method,
                                    "forEach in " + method.getFullName() + " at line " +
                                            forEachCall.getLineNumber() + " does not call Validator"));
                        }
                    }
                }

                private boolean checkForEachConsumer(JavaMethodCall forEachCall) {
                    // This is a limitation: ArchUnit doesn't directly expose lambda internals.
                    // Heuristic: Check if Validator is referenced in the method scope or assume false
                    // For now, conservatively assume no call unless proven otherwise
                    // Future enhancement: Parse source code or bytecode if available
                    return false; // Forces explicit loop usage or manual verification
                }
            });
}

// Example annotations and classes (for completeness)
@interface ValidationNotNeeded {}

interface Validator {
    void validate(Object o);
}

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

2 participants