Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 10 additions & 1 deletion graphify/extractors/apex.py
Original file line number Diff line number Diff line change
Expand Up @@ -73,8 +73,17 @@ 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<String, Object>`, `List<Map<String, Id>>`). Apex also
# permits whitespace around the angle brackets themselves (`Map <String, Id>`,
# `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+)?[\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)
Expand Down
50 changes: 50 additions & 0 deletions tests/test_languages.py
Original file line number Diff line number Diff line change
Expand Up @@ -3399,6 +3399,56 @@ 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<String, Object> commaGeneric() { return null; }\n"
" public Database.QueryLocator dottedReturn(Database.BatchableContext bc) { return null; }\n"
" private static List<Map<String, Id>> nestedGeneric() { return null; }\n"
" global Set<Id> setReturn() { return null; }\n"
" public String[] arrayReturn() { return null; }\n"
# Apex permits whitespace around the angle brackets themselves
" public Map <String, Object> 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)
labels = _labels(result)
assert ".simpleReturn()" in labels
assert ".commaGeneric()" in labels
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"
source.write_text(
"public class Neg {\n"
" public void real() {\n"
" insert new Account(Name = 'x');\n"
" System.assertEquals(1, ids.size());\n"
" Map<String, Object> m = new Map<String, Object>();\n"
" 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<String, Object>) JSON.deserializeUntyped(raw);\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)
Expand Down