From 7bb1b08e905bd500bc74562f829ed60a6f0a285d Mon Sep 17 00:00:00 2001 From: Frank Mahdavi Date: Sun, 30 Aug 2026 10:54:26 -0700 Subject: [PATCH] fix(apex): match commas, spaces, and dots in method return types The return type in method_re was a single character class [\w<>\[\]]+, which cannot match Map (comma, space), Database.QueryLocator (dot), or List> (nested generic). Every method with such a return type was silently dropped from the graph -- including Database.Batchable start() entry points and @AuraEnabled Map controller methods. Replace it with a base token plus an optional <...> generic group. The group excludes ;{}()= so declarations with initializers (Map m = new Map();) and control-flow lines still cannot match. On a 455-file production Apex codebase this recovers 512 method signatures with zero regressions (the old pattern's match set is strictly contained in the new one). Fixes #3217 Co-Authored-By: Claude Fable 5 --- graphify/extractors/apex.py | 7 ++++++- tests/fixtures/sample.cls | 12 ++++++++++++ tests/test_languages.py | 12 ++++++++++++ 3 files changed, 30 insertions(+), 1 deletion(-) diff --git a/graphify/extractors/apex.py b/graphify/extractors/apex.py index 928923a640..b2ea9e162c 100644 --- a/graphify/extractors/apex.py +++ b/graphify/extractors/apex.py @@ -73,8 +73,13 @@ def add_edge(src: str, tgt: str, relation: str, line: int, r"^\s*trigger\s+(\w+)\s+on\s+(\w+)\s*\(", _re.IGNORECASE, ) + # Return type: base token (dotted names, arrays) plus an optional <...> + # generic group that may hold commas, spaces, and one nested level. A bare + # character class cannot match "Map" (comma, space) or + # "Database.QueryLocator" (dot), silently dropping those methods. + _RETTYPE = r"[\w.\[\]]+(?:\s*<[^;{}()=]*>)?" method_re = _re.compile( - rf"^{_ANNOTATION}\s*{_ACCESS}{_MOD}\s*(?:static\s+)?[\w<>\[\]]+\s+(\w+)\s*\([^)]*\)\s*(?:throws\s+\w+\s*)?\{{?", + rf"^{_ANNOTATION}\s*{_ACCESS}{_MOD}\s*(?:static\s+)?{_RETTYPE}\s+(\w+)\s*\([^)]*\)\s*(?:throws\s+\w+\s*)?\{{?", _re.IGNORECASE, ) annotation_re = _re.compile(r"@(\w+)", _re.IGNORECASE) diff --git a/tests/fixtures/sample.cls b/tests/fixtures/sample.cls index 156ddc7e20..ce952d4b1e 100644 --- a/tests/fixtures/sample.cls +++ b/tests/fixtures/sample.cls @@ -41,6 +41,18 @@ public with sharing class AccountService { delete old; } + public static Map getAccountSummary(Id accountId) { + return new Map{ 'id' => accountId }; + } + + public Database.QueryLocator start(Database.BatchableContext bc) { + return Database.getQueryLocator('SELECT Id FROM Account'); + } + + private static List> buildNameIndex() { + return new List>(); + } + @isTest static void testGetAccounts() { List result = getAccounts('Customer'); diff --git a/tests/test_languages.py b/tests/test_languages.py index 46dae524c2..932b5fd01f 100644 --- a/tests/test_languages.py +++ b/tests/test_languages.py @@ -3399,6 +3399,18 @@ def test_apex_method_extraction(): assert any("createAccounts" in l for l in labels) assert any("deleteOldAccounts" in l for l in labels) + +def test_apex_method_generic_and_dotted_return_types(): + # Regression: return types containing commas/spaces (Map), + # dots (Database.QueryLocator), or nested generics (List>) + # were silently dropped because the return type was matched by a single + # character class with no comma, space, or dot. + r = extract_apex(FIXTURES / "sample.cls") + labels = _labels(r) + assert any("getAccountSummary" in l for l in labels) + assert ".start()" in labels + assert any("buildNameIndex" in l for l in labels) + def test_apex_contains_and_method_relations(): r = extract_apex(FIXTURES / "sample.cls") relations = _relations(r)