Skip to content

[apex] New rule: Prevent use of interface -> abstract class with equals/hashCode as key in Map #6492

@JonnyPower

Description

@JonnyPower

Proposed Rule Name: AvoidInterfaceAsMapKey

Proposed Category: Error Prone

Description: In apex, when a Map uses an interface as key, where equals / hashCode are implemented by an abstract class implementation of the interface, methods on Map such as containsKey do not dispatch to the correct equals/hashCode implementation.

Involves many classes which might make a more accurate rule implementation difficult, easiest would be to prevent usage of interfaces in key, but that would likely be too broad. An ideal implementation would probably be difficult - where an interface is used as key & there is an implementation of that interface that defines equals/abstract.

Code Sample: A minimal reproduction of the apex bug can be seen here: https://github.com/Traction-Rec/map-eq-dispatch-bug/blob/main/force-app/main/default/classes/MapEqualsDispatchBugTest.cls

public interface IKey {
    Boolean equals(Object obj);
    Integer hashCode();
    String toString();
}

public abstract class AbstractKey implements IKey {
    protected abstract String getKeyValue();

    public Boolean equals(Object obj) {
        System.debug('AbstractKey.equals() called - dispatch worked!');
        if (obj instanceof AbstractKey) {
            return ((AbstractKey) obj).getKeyValue() == this.getKeyValue();
        }
        return false;
    }

    public Integer hashCode() {
        System.debug('AbstractKey.hashCode() called - dispatch worked!');
        String v = getKeyValue();
        return v != null ? System.hashCode(v) : 0;
    }

    public override String toString() {
        return getKeyValue();
    }
}

public class StringKey extends AbstractKey {
    private String value;

    public StringKey(String value) {
        this.value = value;
    }

    protected override String getKeyValue() {
        return value;
    }
}

@IsTest
private class MapEqualsDispatchBugTest {
    @IsTest
    static void containsKeyWithAbstractMiddle() {
        Map<IKey, String> m = new Map<IKey, String>(); // Rule would throw, violation is use of this interface
        IKey k1 = new StringKey('hello');
        m.put(k1, 'value');
        IKey k2 = new StringKey('hello');
        System.assertEquals(k1.hashCode(), k2.hashCode());
        for (IKey existing : m.keySet()) {
            System.assert(k2.equals(existing), 'k2.equals(existing)');
            System.assert(existing.equals(k2), 'existing.equals(k2)');
        }
        System.assert(m.containsKey(k2), 'Hypothesis: containsKey with interface→abstract→concrete (StringKey)'); // This incorrectly returns false
    }
}

Possible Properties:

I don't think properties are necessary / useful

Metadata

Metadata

Assignees

No one assigned

    Labels

    a:new-ruleProposal to add a new built-in rule

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions