Skip to content

[apex] New Rule: AvoidInterfaceAsMapKeyRule#6493

Merged
adangel merged 6 commits into
pmd:mainfrom
Traction-Rec:feature/6492-apex-interface-key-with-abstract-equals
Apr 24, 2026
Merged

[apex] New Rule: AvoidInterfaceAsMapKeyRule#6493
adangel merged 6 commits into
pmd:mainfrom
Traction-Rec:feature/6492-apex-interface-key-with-abstract-equals

Conversation

@JonnyPower
Copy link
Copy Markdown
Contributor

@JonnyPower JonnyPower commented Mar 8, 2026

Describe the PR

Apex has a dispatch bug in the implementation of Map that can result in difficult errors. This PMD rule looks for the structure that results in Map's bad dispatch, when a Map is using an interface as a key, the rule uses ApexLink to find implementations of the interface, then checking if any implementation is abstract & overrides equals / hashCode - this will result in incorrect dispatching from Map's methods.

This was largely implemented with AI, confirmed resulting jar finds violations against https://github.com/Traction-Rec/map-eq-dispatch-bug but doesn't trigger violation just because an interface is being used.

Ran this rule against relatively large internal project and it completed succesfully.

Related issues

Ready?

  • Added unit tests for fixed bug/feature
  • Passing all unit tests
  • Complete build ./mvnw clean verify passes (checked automatically by github actions)
  • Added (in-code) documentation (if needed)

- rely on ApexLink for cross file analysis

Closes pmd#6492
@JonnyPower JonnyPower force-pushed the feature/6492-apex-interface-key-with-abstract-equals branch from c40f35c to 58bf4fd Compare March 9, 2026 00:18
@JonnyPower JonnyPower marked this pull request as draft March 9, 2026 20:35
- discovered that this dispatch bug happens even if abstract class
isn't implementing the equals/hashCode methods
@JonnyPower JonnyPower force-pushed the feature/6492-apex-interface-key-with-abstract-equals branch from 9928701 to 90be883 Compare March 9, 2026 21:27
@JonnyPower JonnyPower marked this pull request as ready for review March 9, 2026 21:33
@JonnyPower JonnyPower changed the title [apex] feat: add new rule that avoids incorrect Map behavior [apex] New Rule: AvoidInterfaceAsMapKeyRule Mar 9, 2026
- anyTypeMatches does seem useful, but I ended up not using it, so makes
 sense to remove
@adangel adangel added the a:new-rule Proposal to add a new built-in rule label Mar 24, 2026
@adangel adangel added this to the 7.24.0 milestone Mar 24, 2026
@adangel adangel self-requested a review April 17, 2026 09:58
Copy link
Copy Markdown
Member

@adangel adangel left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks!

I'll fix the issues myself.

* to the root of your SFDX project (where {@code sfdx-project.json} lives). Without it the
* rule produces no violations.
*
* @see <a href="https://github.com/pmd/pmd/issues/6492">Issue 6492</a>
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
* @see <a href="https://github.com/pmd/pmd/issues/6492">Issue 6492</a>
* @see <a href="https://github.com/pmd/pmd/issues/6492">[apex] New rule: Prevent use of interface -> abstract class with equals/hashCode as key in Map #6492</a>
* @since 7.24.0

* Returns an unmodifiable list of all type summaries in the org.
* Returns an empty list when multifile analysis is unavailable.
* This enables rules to perform complex cross-type analysis.
*/
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
*/
* @since 7.24.0
*/

return multifileAnalysis.getFileIssues(fileId.getAbsolutePath());
}

public @NonNull ApexMultifileAnalysis getMultifileAnalysis() {
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Instead of this method, I'd return the type summaries directly. For now at least, this fits more the style - like the above method getGlobalIssues.

@Override
public Object visit(ASTApexFile node, Object data) {
ApexMultifileAnalysis mfa = node.getMultifileAnalysis();
if (mfa.isFailed()) {
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think, this warning should be implemented in ApexMultifileAnalysis directly

private static final Logger LOG = LoggerFactory.getLogger(AvoidInterfaceAsMapKeyRule.class);
private static boolean warnedAboutMissingOrg = false;

// Cached index - computed once per PMD run, reused across all file visits
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Note: This is likely once per thread - if PMD is run multithreaded, each rule is instantiated once per thread.
If we want to cache it once per PMD run, then we would need to move it into ApexMultifileAnalysis. But that is maybe something for later, when we know, what we need. I could imagine, the TypeHierarchyIndex is maybe not rule specific and ApexMultifileAnalysis could have method like getSuperType(type) -> Type, Type.getMethods(), ... something like this.

typesBySimpleName.computeIfAbsent(simpleName, k -> new HashSet<>()).add(summary);
typesByFullName.put(fullName, summary);

if ("interface".equalsIgnoreCase(nature)) {
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It probably makes sense to extract these natures as constants

Comment on lines +120 to +124
In Apex, when a Map uses an interface as key and an abstract class implements that interface
and defines equals/hashCode, methods like containsKey do not dispatch to the correct implementation.
This rule reports Map declarations (fields, variables, parameters) whose key type is an interface
that has at least one abstract implementing class defining equals or hashCode (requires multi-file
analysis provided by apex-link).
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
In Apex, when a Map uses an interface as key and an abstract class implements that interface
and defines equals/hashCode, methods like containsKey do not dispatch to the correct implementation.
This rule reports Map declarations (fields, variables, parameters) whose key type is an interface
that has at least one abstract implementing class defining equals or hashCode (requires multi-file
analysis provided by apex-link).
In Apex, when a Map uses an interface as key and an abstract class implements that interface
and defines equals/hashCode, methods like containsKey do not dispatch to the correct implementation. This results in potentially duplicated map entries or not being able to get entries by key.
For example (`IKey` is an interface and `StringKey` implements IKey and equals/hashCode):
```apex
Map<IKey, String> m = new Map<IKey, String>();
m.put(new StringKey('hello'), 'value');
Boolean exists = m.containsKey(new StringKey('hello')); // might return false
m.put(new StringKey('hello'), 'other value'); // might add a second entry instead of replacing the existing one
```
This rule reports Map declarations (fields, variables, parameters) whose key type is an interface that has at least one abstract implementing class defining equals or hashCode.
[Apex Language Server](https://github.com/apex-dev-tools/apex-ls) is used to make this possible and this needs
additional configuration. The environment variable `PMD_APEX_ROOT_DIRECTORY` needs to be set prior to executing
PMD. With this variable the root directory of the Salesforce metadata, where `sfdx-project.json` resides, is
specified. Apex Language Server can then load all the types in the project and figure out the type hierarchy.

assertEquals(lineNumber, violation.getBeginLine());
}

private Report runRule(Path testProjectDir) throws IOException {
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks like this is the same as in UnusedMethodTest - maybe it's time to provide test framework support?

Comment on lines +56 to +58
for (RuleViolation v : report.getViolations()) {
assertEquals("MapUser.cls", v.getFileId().getFileName());
}
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is not necessary, filename is asserted with assertViolation as well

adangel added a commit that referenced this pull request Apr 24, 2026
@adangel adangel merged commit 52d3b34 into pmd:main Apr 24, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

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

Projects

None yet

Development

Successfully merging this pull request may close these issues.

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

2 participants