I'm having a method that accepts, for example, two long primitives. How can I add checks to prevent the accidental swapping of parameters?

Something like this should be detected.

    public void startSomethingElse(@AccountIdParam long accountId, @BalanceParam long balance) {
        System.out.println("Account " + accountId + " balance " + balance);
    }

    public void startOfSomething() {
        @AccountIdParam long accountId = 1;
        @BalanceParam long balance = 100;
        startSomethingElse(balance, accountId);
    }

Using CheckerFramework, I could create custom qualifiers like this:

@SubtypeOf({})
@Target({ElementType.TYPE_USE, ElementType.TYPE_PARAMETER})
public @interface AccountIdParam {}

@SubtypeOf({})
@Target({ElementType.TYPE_USE, ElementType.TYPE_PARAMETER})
public @interface BalanceParam {}

But that won't work without creating hierarchy so I need to do something like this which also wouldn't work because of two leaf nodes:

@SubtypeOf({Unqualified.class})
@Target({ElementType.TYPE_USE, ElementType.TYPE_PARAMETER})
public @interface AccountIdParam {}

@SubtypeOf({Unqualified.class})
@Target({ElementType.TYPE_USE, ElementType.TYPE_PARAMETER})
public @interface BalanceParam {}

@DefaultQualifierInHierarchy
@Target({ElementType.TYPE_USE, ElementType.TYPE_PARAMETER})
@SubtypeOf({})
public @interface Unqualified {}

How would you do it?

Couple of constraints:

  • I need to use primitives so subclassing Java objects isn't a option.

  • I need to have compile time check, not run time.

5 Replies 5

https://checkerframework.org/manual/#creating-bottom-and-top-qualifier says, "there must be a top qualifier that is a supertype of all other qualifiers, and there must be a bottom qualifier that is a subtype of all other qualifiers".

You can add a @ParamBottom qualifier:

@Target({ElementType.TYPE_USE, ElementType.TYPE_PARAMETER})
@SubtypeOf({AccountIdParam.class, BalanceParam.class})
public @interface FormatBottom {}

By the way, it is better style to name your top qualifier something like @ParamUnknown rather than just @Unqualified, which doesn't indicate what hierarchy it belongs to. You will also want to set default, restrict the qualifier to ints, etc.; all of these things can be done with meta-annotations written on your @interface declarations.

Sounds like you're looking for Error Prone's https://errorprone.info/bugpattern/ArgumentSelectionDefectChecker, which would be much simpler than the approaches you seem to be looking at.

Another alternative is to wrap your primitives in a class, i.e. AccountNumber. Then you will be using a type instead and the whole problem of order of parameters is gone.

Thank you @mernst!

I got this to work:

void startSomethingElse(@AccountIdParam long accountId, @BalanceParam long balance) {
    System.out.println("Account " + accountId + " balance " + balance);
}

void startOfSomething() {
    /*
     * either use:
     * @AccountIdParam long accountId = (@AccountIdParam long) 1;
     * or
     * @SuppressWarnings("assignment")
     */
    @SuppressWarnings("assignment")
    @AccountIdParam long accountId = 1;
    @SuppressWarnings("assignment")
    @BalanceParam long balance = 100;
    startSomethingElse(balance, accountId);
}

And now I have these qualifiers:

@SubtypeOf({ParamUnknown.class})
@Target({ElementType.TYPE_USE, ElementType.TYPE_PARAMETER})
public @interface AccountIdParam {}

@SubtypeOf({ParamUnknown.class})
@Target({ElementType.TYPE_USE, ElementType.TYPE_PARAMETER})
public @interface BalanceParam {}

@Target({ElementType.TYPE_USE, ElementType.TYPE_PARAMETER})
@SubtypeOf({AccountIdParam.class, BalanceParam.class})
public @interface ParamBottom {}

@DefaultQualifierInHierarchy
@Target({ElementType.TYPE_USE, ElementType.TYPE_PARAMETER})
@SubtypeOf({})
public @interface ParamUnknown {}

If my number of qualifiers grows, this line on ParamBottom where I list them will become cumbersome. Any advice on that?

@Louis Wasserman

I've looked at ErrorProne ArgumentSelectionDefectChecker, but it does not give me any warning for this.

void startSomethingElse(long accountId, long balance) {
    System.out.println("Account " + accountId + " balance " + balance);
}

void startOfSomething() {
    long accountId = 1;
    long balance = 100;
    startSomethingElse(balance, accountId);
}

However, it works for this. I guess it needs custom annotations for its heuristic. These are my CheckerFramework annotations :)

void startSomethingElse(@AccountIdParam long accountId, @BalanceParam long balance) {
    System.out.println("Account " + accountId + " balance " + balance);
}

void startOfSomething() {
    long accountId = 1;
    long balance = 100;
    startSomethingElse(balance, accountId);
}

What am I doing wrong? Thanks!

Your Reply

By clicking “Post Your Reply”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.