Skip to content

Apex: method_re drops methods with commas/dots in return type (Map<String, Object>, Database.QueryLocator) #3217

Description

@Fmahdavi

Summary

The Apex extractor's method_re cannot match method signatures whose return type contains a comma, a space, or a dot. Every such method is silently dropped from the graph — no node, no edges, no warning.

The return type is matched by a single character class:

method_re = _re.compile(
    rf"^{_ANNOTATION}\s*{_ACCESS}{_MOD}\s*(?:static\s+)?[\w<>\[\]]+\s+(\w+)\s*\([^)]*\)\s*(?:throws\s+\w+\s*)?\{{?",
    _re.IGNORECASE,
)

[\w<>\[\]]+ permits word chars and angle/square brackets only, so:

  • Map<String, Object> — comma + space → never matches
  • Database.QueryLocator — dot → never matches
  • List<Map<String, Id>> — → never matches

These are extremely common in real Apex: every Database.Batchable implementation's start() returns Database.QueryLocator, and @AuraEnabled controller methods conventionally return Map<String, Object>.

Affected versions

Present since Apex support landed (7467c1b, 2026-06-07). Verified byte-identical in v0.8.39 and v0.9.53 / HEAD (33362d9), now in graphify/extractors/apex.py.

Minimal repro

Repro.cls:

public with sharing class Repro {
    public static String simpleReturn() { return ''; }
    public static Map<String, Object> commaGeneric() { return null; }
    public Database.QueryLocator dottedReturn(Database.BatchableContext bc) { return null; }
    private static List<Map<String, Id>> nestedGeneric() { return null; }
}
from graphify.extract import extract_apex
res = extract_apex(Path("Repro.cls"))
print(sorted(n["label"] for n in res["nodes"] if n["label"].startswith(".")))

Observed (v0.8.39 and v0.9.53):

['.simpleReturn()']

Expected: all four methods.

Real-world impact

On a 455-file production Salesforce codebase, this dropped 512 method signatures (~25% of the API surface), including every batch-class start() entry point and every Map<String, Object> controller method.

Suggested fix

Replace the return-type token with a base token plus an optional generic group that admits commas/spaces/dots (and one nested level):

_RETTYPE = r"[\w.\[\]]+(?:\s*<[^;{}()=]*>)?"
method_re = _re.compile(
    rf"^{_ANNOTATION}\s*{_ACCESS}{_MOD}\s*(?:static\s+)?{_RETTYPE}\s+(\w+)\s*\([^)]*\)\s*(?:throws\s+\w+\s*)?\{{?",
    _re.IGNORECASE,
)

Excluding ;{}()= inside <...> keeps declarations-with-initializers (Map<Id, X> m = new Map<Id, X>();) and control-flow lines from matching. Scanned against the 455-file corpus above: the old pattern's match set is strictly contained in the new one (0 regressions), and all 512 newly matched lines are genuine method signatures.

Happy to submit a PR with this change plus a regression test.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions