[apex] New Rule: AvoidInterfaceAsMapKeyRule#6493
Conversation
- rely on ApexLink for cross file analysis Closes pmd#6492
c40f35c to
58bf4fd
Compare
- discovered that this dispatch bug happens even if abstract class isn't implementing the equals/hashCode methods
9928701 to
90be883
Compare
- anyTypeMatches does seem useful, but I ended up not using it, so makes sense to remove
adangel
left a comment
There was a problem hiding this comment.
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> |
There was a problem hiding this comment.
| * @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. | ||
| */ |
There was a problem hiding this comment.
| */ | |
| * @since 7.24.0 | |
| */ |
| return multifileAnalysis.getFileIssues(fileId.getAbsolutePath()); | ||
| } | ||
|
|
||
| public @NonNull ApexMultifileAnalysis getMultifileAnalysis() { |
There was a problem hiding this comment.
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()) { |
There was a problem hiding this comment.
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 |
There was a problem hiding this comment.
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)) { |
There was a problem hiding this comment.
It probably makes sense to extract these natures as constants
| 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). |
There was a problem hiding this comment.
| 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 { |
There was a problem hiding this comment.
Looks like this is the same as in UnusedMethodTest - maybe it's time to provide test framework support?
| for (RuleViolation v : report.getViolations()) { | ||
| assertEquals("MapUser.cls", v.getFileId().getFileName()); | ||
| } |
There was a problem hiding this comment.
This is not necessary, filename is asserted with assertViolation as well
Describe the PR
Apex has a dispatch bug in the implementation of
Mapthat can result in difficult errors. This PMD rule looks for the structure that results inMap's bad dispatch, when a Map is using an interface as a key, the rule usesApexLinkto 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?
./mvnw clean verifypasses (checked automatically by github actions)