From 62bb68464f6a0b2be0ea71b04a73f74fefb68852 Mon Sep 17 00:00:00 2001 From: soumyadip sarkar Date: Wed, 2 Sep 2026 12:02:12 +0530 Subject: [PATCH 1/2] fix(apex): match qualified and generic method return types (#3217) `method_re` matched the return type with `[\w<>\[\]]+`, so any signature whose return type carried a dot, a comma, or a space never matched and the method was dropped with no node, no edges, and no warning. That covers the two most common shapes in real Apex: `Database.QueryLocator` (every `Database.Batchable.start()`) and `Map` (the `@AuraEnabled` controller convention), plus nested generics like `List>`. Widen the return type to a single `_TYPE` pattern that admits a namespace-qualified name and generic arguments. Whitespace is admitted only adjacent to a comma, so a statement such as `insert new Account(...)` or `Integer a = 1, b = compute();` still cannot be read as a declaration - extraction of the existing fixtures is byte-identical. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_01UEfivKVYzq1e5E6hJHiuKi --- graphify/extractors/apex.py | 8 +++++++- tests/test_languages.py | 37 +++++++++++++++++++++++++++++++++++++ 2 files changed, 44 insertions(+), 1 deletion(-) diff --git a/graphify/extractors/apex.py b/graphify/extractors/apex.py index 928923a640..af558aa701 100644 --- a/graphify/extractors/apex.py +++ b/graphify/extractors/apex.py @@ -73,8 +73,14 @@ def add_edge(src: str, tgt: str, relation: str, line: int, r"^\s*trigger\s+(\w+)\s+on\s+(\w+)\s*\(", _re.IGNORECASE, ) + # An Apex return type is not one bare word: it can be namespace-qualified + # (`Database.QueryLocator`) and can carry generic arguments holding commas + # and spaces (`Map`, `List>`). Whitespace is + # admitted only next to a comma, so a statement such as + # `insert new Account(...)` still cannot be read as a declaration (#3217). + _TYPE = r"[\w.<>\[\]]+(?:\s*,\s*[\w.<>\[\]]+)*" 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+)?{_TYPE}\s+(\w+)\s*\([^)]*\)\s*(?:throws\s+\w+\s*)?\{{?", _re.IGNORECASE, ) annotation_re = _re.compile(r"@(\w+)", _re.IGNORECASE) diff --git a/tests/test_languages.py b/tests/test_languages.py index 46dae524c2..7e520e386f 100644 --- a/tests/test_languages.py +++ b/tests/test_languages.py @@ -3399,6 +3399,43 @@ 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_qualified_and_generic_return_types(tmp_path): + source = tmp_path / "Repro.cls" + source.write_text( + "public with sharing class Repro {\n" + " public static String simpleReturn() { return ''; }\n" + " public static Map commaGeneric() { return null; }\n" + " public Database.QueryLocator dottedReturn(Database.BatchableContext bc) { return null; }\n" + " private static List> nestedGeneric() { return null; }\n" + " global Set setReturn() { return null; }\n" + "}\n" + ) + result = extract_apex(source) + labels = _labels(result) + assert ".simpleReturn()" in labels + assert ".commaGeneric()" in labels + assert ".dottedReturn()" in labels + assert ".nestedGeneric()" in labels + assert ".setReturn()" in labels + +def test_apex_statements_are_not_read_as_methods(tmp_path): + source = tmp_path / "Neg.cls" + source.write_text( + "public class Neg {\n" + " public void real() {\n" + " insert new Account(Name = 'x');\n" + " System.assertEquals(1, ids.size());\n" + " Map m = new Map();\n" + " results.put('a', compute(x));\n" + " this.helper(1, 2);\n" + " Integer a = 1, b = compute();\n" + " }\n" + "}\n" + ) + result = extract_apex(source) + methods = {l for l in _labels(result) if l.startswith(".")} + assert methods == {".real()"} + def test_apex_contains_and_method_relations(): r = extract_apex(FIXTURES / "sample.cls") relations = _relations(r) From 00e70fc2a1f9ba5937911eb0c4e1837a57df7ba9 Mon Sep 17 00:00:00 2001 From: soumyadip sarkar Date: Wed, 2 Sep 2026 12:21:47 +0530 Subject: [PATCH 2/2] fix(apex): admit whitespace around generic angle brackets in a return type Review follow-up. The first pass admitted whitespace only next to a comma, so a return type spaced around the angle brackets themselves - all legal Apex - was still dropped: public Map spaceBeforeAngle() # dropped public List< Account > spacesInside() # dropped public List < Map< String, Id > > roomy() # dropped Read the type as segments joined by the type punctuators `<`, `>` and `,`, with whitespace allowed only adjacent to one of them and never between two bare words. That separator rule is what still bars an ordinary statement from being read as a declaration, so a bare `<`/`>` used as a comparison (`if (a > b) { doIt(x); }`, `while (i < list.size()) { next(); }`) fabricates nothing; the negative test grew those cases plus a generic cast, and extraction of the existing fixtures is unchanged. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_01UEfivKVYzq1e5E6hJHiuKi --- graphify/extractors/apex.py | 11 +++++++---- tests/test_languages.py | 13 +++++++++++++ 2 files changed, 20 insertions(+), 4 deletions(-) diff --git a/graphify/extractors/apex.py b/graphify/extractors/apex.py index af558aa701..9b66d5a722 100644 --- a/graphify/extractors/apex.py +++ b/graphify/extractors/apex.py @@ -75,10 +75,13 @@ def add_edge(src: str, tgt: str, relation: str, line: int, ) # An Apex return type is not one bare word: it can be namespace-qualified # (`Database.QueryLocator`) and can carry generic arguments holding commas - # and spaces (`Map`, `List>`). Whitespace is - # admitted only next to a comma, so a statement such as - # `insert new Account(...)` still cannot be read as a declaration (#3217). - _TYPE = r"[\w.<>\[\]]+(?:\s*,\s*[\w.<>\[\]]+)*" + # and spaces (`Map`, `List>`). Apex also + # permits whitespace around the angle brackets themselves (`Map `, + # `List< Account >`), so the type is read as segments joined by the type + # punctuators `<`, `>` and `,`, with whitespace allowed only ADJACENT to one + # of them - never between two bare words. That is what keeps a statement such + # as `insert new Account(...)` from being read as a declaration (#3217). + _TYPE = r"[\w.\[\]]+(?:\s*[<>,]\s*[\w.\[\]]*)*" method_re = _re.compile( rf"^{_ANNOTATION}\s*{_ACCESS}{_MOD}\s*(?:static\s+)?{_TYPE}\s+(\w+)\s*\([^)]*\)\s*(?:throws\s+\w+\s*)?\{{?", _re.IGNORECASE, diff --git a/tests/test_languages.py b/tests/test_languages.py index 7e520e386f..c6f6efc290 100644 --- a/tests/test_languages.py +++ b/tests/test_languages.py @@ -3408,6 +3408,11 @@ def test_apex_method_qualified_and_generic_return_types(tmp_path): " public Database.QueryLocator dottedReturn(Database.BatchableContext bc) { return null; }\n" " private static List> nestedGeneric() { return null; }\n" " global Set setReturn() { return null; }\n" + " public String[] arrayReturn() { return null; }\n" + # Apex permits whitespace around the angle brackets themselves + " public Map spaceBeforeAngle() { return null; }\n" + " public List< Account > spacesInside() { return null; }\n" + " public List < Map< String, Id > > roomy() { return null; }\n" "}\n" ) result = extract_apex(source) @@ -3417,6 +3422,10 @@ def test_apex_method_qualified_and_generic_return_types(tmp_path): assert ".dottedReturn()" in labels assert ".nestedGeneric()" in labels assert ".setReturn()" in labels + assert ".arrayReturn()" in labels + assert ".spaceBeforeAngle()" in labels + assert ".spacesInside()" in labels + assert ".roomy()" in labels def test_apex_statements_are_not_read_as_methods(tmp_path): source = tmp_path / "Neg.cls" @@ -3429,6 +3438,10 @@ def test_apex_statements_are_not_read_as_methods(tmp_path): " results.put('a', compute(x));\n" " this.helper(1, 2);\n" " Integer a = 1, b = compute();\n" + # a bare `<` / `>` is a comparison, not a generic argument list + " if (a > b) { doIt(x); }\n" + " while (i < list.size()) { next(); }\n" + " String s = (Map) JSON.deserializeUntyped(raw);\n" " }\n" "}\n" )