From 76515436b86fd8cebb04249e410f1ddd36d497aa Mon Sep 17 00:00:00 2001 From: Joe Esquibel Date: Wed, 23 Sep 2026 14:08:36 -0400 Subject: [PATCH 1/7] calls_out: keywords, annotations and tcl SQL are not calls (#3359) Contract C2/C1/C7 (docs/calls_out_rule_contract.md, classes K/A/S): - _calls_out_ignore keyword additions for 24 languages (no built-ins touched, _CALLS_OUT_GLOBAL_IGNORE untouched) - CALLS_OUT_C_STYLE_NO_ANNOTATION ((? --- gitgalaxy/core/detector.py | 25 +- .../language_standards/_shared_patterns.py | 9 + .../language_standards/languages/ada.py | 12 + .../languages/agc_assembly.py | 4 + .../language_standards/languages/cobol.py | 14 +- .../language_standards/languages/cpp.py | 15 ++ .../language_standards/languages/csharp.py | 29 ++ .../language_standards/languages/dart.py | 13 +- .../languages/embedded_python.py | 19 ++ .../language_standards/languages/go.py | 9 + .../language_standards/languages/groovy.py | 11 +- .../language_standards/languages/java.py | 11 +- .../language_standards/languages/kotlin.py | 13 +- .../language_standards/languages/livecode.py | 25 ++ .../language_standards/languages/lua.py | 10 + .../language_standards/languages/perl.py | 11 + .../language_standards/languages/php.py | 17 ++ .../languages/powershell.py | 34 ++- .../language_standards/languages/python.py | 19 ++ .../language_standards/languages/rust.py | 19 ++ .../language_standards/languages/scala.py | 15 +- .../language_standards/languages/scheme.py | 27 ++ .../language_standards/languages/solidity.py | 9 + .../language_standards/languages/swift.py | 12 +- .../language_standards/languages/tcl.py | 16 +- .../languages/typescript.py | 11 + .../language_standards/languages/zig.py | 17 ++ gitgalaxy/standards/signal_contracts.py | 2 +- .../languages/test_calls_out_contract_3327.py | 12 - .../languages/test_calls_out_keywords_3359.py | 251 ++++++++++++++++++ 30 files changed, 656 insertions(+), 35 deletions(-) create mode 100644 tests/extraction/languages/test_calls_out_keywords_3359.py diff --git a/gitgalaxy/core/detector.py b/gitgalaxy/core/detector.py index 8ecf89e8e8..0cd0f7523f 100644 --- a/gitgalaxy/core/detector.py +++ b/gitgalaxy/core/detector.py @@ -44,7 +44,10 @@ COMPILED_HANDSHAKE_REGISTRY, HTML_NONEXECUTABLE_SCRIPT_TAG, ) -from gitgalaxy.standards.language_standards._shared_patterns import CALLS_OUT_C_STYLE +from gitgalaxy.standards.language_standards._shared_patterns import ( + CALLS_OUT_C_STYLE, + CALLS_OUT_C_STYLE_NO_ANNOTATION, +) HAS_TIKTOKEN = False try: @@ -879,7 +882,8 @@ def _cpp_class_has_body(code_stream: str, scan_start: int) -> bool: # cannot distinguish from user calls. Case-sensitive on purpose: languages # whose keywords are case-insensitive (fortran, abap, pli, rexx, db2_sql, ada) # declare their own lowercase words via the per-language `_calls_out_ignore` -# rule, which is compared casefolded at the filter site. +# rule, which is compared casefolded at the filter site for a language declared +# `identifier_case: insensitive`, and exactly for every other language (#3359). # #3329: member-access separators a call's qualifier is joined by -- `a.b()`, # `p->f()`, `Ns::f()`, `a?.b()`. Longest first, so `->`/`::`/`?.` win over `.`. _QUALIFIER_SEPARATORS = ("->", "::", "?.", ".") @@ -8835,7 +8839,7 @@ def _calculate_block_metrics( if invocation_pattern: # Apply literal shield to avoid capturing words inside strings safe_block = self._apply_literal_shield(block, self.primary_lang_id) - if invocation_pattern is CALLS_OUT_C_STYLE: + if invocation_pattern is CALLS_OUT_C_STYLE or invocation_pattern is CALLS_OUT_C_STYLE_NO_ANNOTATION: for m in invocation_pattern.finditer(safe_block): callee = m.group(1) raw_calls.append(callee) @@ -8847,9 +8851,16 @@ def _calculate_block_metrics( raw_calls = invocation_pattern.findall(safe_block) # Per-language additions to the global ignore set (Epic #3264 Phase 3). - # Authored lowercase in the profile and compared casefolded, so - # case-insensitive languages filter their keywords in any spelling. + # Authored lowercase in the profile. A case-insensitive language + # (`identifier_case: insensitive`) compares casefolded, so it filters its + # keywords in any spelling; #3359: a case-sensitive one compares exactly, + # so a keyword never swallows a real callee spelled with capitals (go's + # `func` keyword vs a `Func()` method, rust's `let` vs an `Expr::Let(` + # variant, C#'s `this(` chaining vs a `factory.This()` call). lang_ignore = rules.get("_calls_out_ignore") or frozenset() + _fold_ignore = ( + self.languages.get(self.primary_lang_id, {}).get("identifier_case") == IDENTIFIER_CASE_INSENSITIVE + ) # Deduplicate and filter (excluding the function calling itself recursively). # #3292: a `positional` language's units cannot be invoked by name, so a # callee spelled like the unit is never recursion -- `//IEFBR14 EXEC @@ -8864,7 +8875,9 @@ def _calculate_block_metrics( dict.fromkeys( c for c in raw_calls - if c not in _CALLS_OUT_GLOBAL_IGNORE and c.casefold() not in lang_ignore and c != self_name + if c not in _CALLS_OUT_GLOBAL_IGNORE + and (c.casefold() if _fold_ignore else c) not in lang_ignore + and c != self_name ) ) diff --git a/gitgalaxy/standards/language_standards/_shared_patterns.py b/gitgalaxy/standards/language_standards/_shared_patterns.py index 5721cc116f..d537ab1c79 100644 --- a/gitgalaxy/standards/language_standards/_shared_patterns.py +++ b/gitgalaxy/standards/language_standards/_shared_patterns.py @@ -147,6 +147,15 @@ # C-Family / Algol-Family (name followed by optional space and open parenthesis) CALLS_OUT_C_STYLE = re.compile(r"\b([a-zA-Z_]\w*)\s*\(") +# #3359 (contract C1): the C-style pattern for languages whose `@Name(...)` is a +# metadata annotation -- a declaration, never an invocation (java, kotlin, swift, +# dart, groovy, scala). NOT for python/typescript/javascript, where a decorator +# factory `@retry(3)` really is a call to `retry`. A dotted annotation +# (`@javax.annotation.Generated(`) still leaks its last segment, a known gap: +# the lookbehind is fixed-width on purpose (Rule 5). detector.py treats this +# pattern exactly like CALLS_OUT_C_STYLE (qualifier capture included). +CALLS_OUT_C_STYLE_NO_ANNOTATION = re.compile(r"(? -- an unconditional transfer of control. # First target only for `GO TO A B C DEPENDING ON X` (one occurrence on # the whole crucible). WHENEVER ... GO TO (embedded SQL) is excluded: that diff --git a/gitgalaxy/standards/language_standards/languages/cpp.py b/gitgalaxy/standards/language_standards/languages/cpp.py index e35e99def5..4dd8507ef7 100644 --- a/gitgalaxy/standards/language_standards/languages/cpp.py +++ b/gitgalaxy/standards/language_standards/languages/cpp.py @@ -61,6 +61,21 @@ "rules": { # Epic #3264: Explicitly declare the structural invocation paradigm "calls_out": CALLS_OUT_C_STYLE, + # #3359 (contract C2): keywords and special forms, never calls + "_calls_out_ignore": frozenset( + { + "static_assert", + "operator", + "constexpr", + "alignas", + "noexcept", + "requires", + "typeid", + "co_await", + "co_yield", + "co_return", + } + ), # 1. branch (Control Flow / Branching) # Control flow jumps. Includes modern coroutine jumps (co_yield, co_await). # EXCLUDES exceptions (bailout_hits). diff --git a/gitgalaxy/standards/language_standards/languages/csharp.py b/gitgalaxy/standards/language_standards/languages/csharp.py index 47e5bb8acd..20bf3e51cd 100644 --- a/gitgalaxy/standards/language_standards/languages/csharp.py +++ b/gitgalaxy/standards/language_standards/languages/csharp.py @@ -56,6 +56,35 @@ "rules": { # Epic #3264: Explicitly declare the structural invocation paradigm "calls_out": CALLS_OUT_C_STYLE, + # #3359 (contract C2): keywords and special forms, never calls + "_calls_out_ignore": frozenset( + { + "foreach", + "nameof", + "default", + "var", + "is", + "not", + "or", + "and", + "when", + "lock", + "this", + "base", + "new", + "static", + "internal", + "private", + "protected", + "public", + "in", + "out", + "ref", + "fixed", + "checked", + "unchecked", + } + ), # --- PHASE 1: LOGIC TOPOLOGY & STRUCTURE --- # 1. branch (Control Flow / Branching) # Decisions and logical jumps. EXCLUDES throw (bailout_hits). diff --git a/gitgalaxy/standards/language_standards/languages/dart.py b/gitgalaxy/standards/language_standards/languages/dart.py index f050227178..cad56731e6 100644 --- a/gitgalaxy/standards/language_standards/languages/dart.py +++ b/gitgalaxy/standards/language_standards/languages/dart.py @@ -11,7 +11,7 @@ import re from typing import Any -from .._shared_patterns import CALLS_OUT_C_STYLE, GLOBAL_FRAGILE_DEBT, GLOBAL_PLANNED_DEBT +from .._shared_patterns import CALLS_OUT_C_STYLE_NO_ANNOTATION, GLOBAL_FRAGILE_DEBT, GLOBAL_PLANNED_DEBT DEFINITION: dict[str, Any] = { "_meta": { @@ -40,7 +40,16 @@ "lexical_family": "standard_block", "rules": { # Epic #3264: Explicitly declare the structural invocation paradigm - "calls_out": CALLS_OUT_C_STYLE, + "calls_out": CALLS_OUT_C_STYLE_NO_ANNOTATION, # #3359: `@Name(` is an annotation (C1) + # #3359 (contract C2): keywords and special forms, never calls + "_calls_out_ignore": frozenset( + { + "when", + "final", + "var", + "Function", # the function-TYPE keyword: `void Function(int)` + } + ), # --- PHASE 1: LOGIC TOPOLOGY & STRUCTURE --- # 1. branch: decisions that split flow. Includes modern pattern guards (when) and null-coalescing. "branch": re.compile( diff --git a/gitgalaxy/standards/language_standards/languages/embedded_python.py b/gitgalaxy/standards/language_standards/languages/embedded_python.py index 4dc57d7e51..f287527e3b 100644 --- a/gitgalaxy/standards/language_standards/languages/embedded_python.py +++ b/gitgalaxy/standards/language_standards/languages/embedded_python.py @@ -59,6 +59,25 @@ "rules": { # Epic #3264: Explicitly declare the structural invocation paradigm "calls_out": CALLS_OUT_C_STYLE, + # #3359 (contract C2): keywords and special forms, never calls + "_calls_out_ignore": frozenset( + { + "in", + "not", + "and", + "or", + "elif", + "is", + "yield", + "lambda", + "del", + "with", + "raise", + "from", + "as", + "else", + } + ), # --- PHASE 1: LOGIC TOPOLOGY & STRUCTURE --- # 1. branch (Control Flow / Branching) # Decisions and logical jumps. EXCLUDES raise (bailout_hits). diff --git a/gitgalaxy/standards/language_standards/languages/go.py b/gitgalaxy/standards/language_standards/languages/go.py index 290957b85b..88bc30dd99 100644 --- a/gitgalaxy/standards/language_standards/languages/go.py +++ b/gitgalaxy/standards/language_standards/languages/go.py @@ -37,6 +37,15 @@ "rules": { # Epic #3264: Explicitly declare the structural invocation paradigm "calls_out": CALLS_OUT_C_STYLE, + # #3359 (contract C2): keywords and special forms, never calls + "_calls_out_ignore": frozenset( + { + "func", + "var", + "const", + "type", + } + ), # --- PHASE 1: LOGIC TOPOLOGY & STRUCTURE --- # 1. branch (Control Flow / Branching) # Includes select/case and range-based loops. EXCLUDES panic (bailout_hits). diff --git a/gitgalaxy/standards/language_standards/languages/groovy.py b/gitgalaxy/standards/language_standards/languages/groovy.py index b650b15ea4..a2d3e12945 100644 --- a/gitgalaxy/standards/language_standards/languages/groovy.py +++ b/gitgalaxy/standards/language_standards/languages/groovy.py @@ -11,7 +11,7 @@ import re from typing import Any -from .._shared_patterns import CALLS_OUT_C_STYLE, GLOBAL_FRAGILE_DEBT, GLOBAL_PLANNED_DEBT +from .._shared_patterns import CALLS_OUT_C_STYLE_NO_ANNOTATION, GLOBAL_FRAGILE_DEBT, GLOBAL_PLANNED_DEBT DEFINITION: dict[str, Any] = { "_meta": { @@ -38,7 +38,14 @@ "lexical_family": "standard_block", "rules": { # Epic #3264: Explicitly declare the structural invocation paradigm - "calls_out": CALLS_OUT_C_STYLE, + "calls_out": CALLS_OUT_C_STYLE_NO_ANNOTATION, # #3359: `@Name(` is an annotation (C1) + # #3359 (contract C2): keywords and special forms, never calls + "_calls_out_ignore": frozenset( + { + "this", + "in", + } + ), # --- PHASE 1: LOGIC TOPOLOGY & STRUCTURE --- # 1. branch (Control Flow / Branching) "branch": re.compile(r"\b(if|else|switch|case|default|for|while|in)\b|\?"), diff --git a/gitgalaxy/standards/language_standards/languages/java.py b/gitgalaxy/standards/language_standards/languages/java.py index 854b8c9d47..0c152906c6 100644 --- a/gitgalaxy/standards/language_standards/languages/java.py +++ b/gitgalaxy/standards/language_standards/languages/java.py @@ -11,7 +11,7 @@ import re from typing import Any -from .._shared_patterns import CALLS_OUT_C_STYLE, GLOBAL_FRAGILE_DEBT, GLOBAL_PLANNED_DEBT +from .._shared_patterns import CALLS_OUT_C_STYLE_NO_ANNOTATION, GLOBAL_FRAGILE_DEBT, GLOBAL_PLANNED_DEBT DEFINITION: dict[str, Any] = { "_meta": { @@ -45,7 +45,14 @@ "lexical_family": "standard_block", "rules": { # Epic #3264: Explicitly declare the structural invocation paradigm - "calls_out": CALLS_OUT_C_STYLE, + "calls_out": CALLS_OUT_C_STYLE_NO_ANNOTATION, # #3359: `@Name(` is an annotation (C1) + # #3359 (contract C2): keywords and special forms, never calls + "_calls_out_ignore": frozenset( + { + "this", + "synchronized", + } + ), # --- PHASE 1: LOGIC TOPOLOGY & STRUCTURE --- # 1. branch (Control Flow / Branching) # Includes modern switch expressions (yield) and pattern guards (when). diff --git a/gitgalaxy/standards/language_standards/languages/kotlin.py b/gitgalaxy/standards/language_standards/languages/kotlin.py index c33c95a608..63b640411f 100644 --- a/gitgalaxy/standards/language_standards/languages/kotlin.py +++ b/gitgalaxy/standards/language_standards/languages/kotlin.py @@ -11,7 +11,7 @@ import re from typing import Any -from .._shared_patterns import CALLS_OUT_C_STYLE, GLOBAL_FRAGILE_DEBT, GLOBAL_PLANNED_DEBT +from .._shared_patterns import CALLS_OUT_C_STYLE_NO_ANNOTATION, GLOBAL_FRAGILE_DEBT, GLOBAL_PLANNED_DEBT DEFINITION: dict[str, Any] = { "_meta": { @@ -39,7 +39,16 @@ "lexical_family": "standard_block", "rules": { # Epic #3264: Explicitly declare the structural invocation paradigm - "calls_out": CALLS_OUT_C_STYLE, + "calls_out": CALLS_OUT_C_STYLE_NO_ANNOTATION, # #3359: `@Name(` is an annotation (C1) + # #3359 (contract C2): keywords and special forms, never calls + "_calls_out_ignore": frozenset( + { + "when", + "this", + "val", + "var", + } + ), # --- PHASE 1: LOGIC TOPOLOGY & STRUCTURE --- # 1. branch (Control Flow / Branching) # Decisions and logical jumps. Includes modern 'when' and Elvis operator. diff --git a/gitgalaxy/standards/language_standards/languages/livecode.py b/gitgalaxy/standards/language_standards/languages/livecode.py index 1292fdc051..aa43a03324 100644 --- a/gitgalaxy/standards/language_standards/languages/livecode.py +++ b/gitgalaxy/standards/language_standards/languages/livecode.py @@ -60,6 +60,31 @@ "get", "put", "global", + # #3359 (contract C2): keywords and special forms, never calls + "private", + "public", + "command", + "variable", + "case", + "break", + "module", + "handler", + "default", + "constant", + "unsafe", + "use", + "next", + "begin", + "returns", + "syntax", + "foreign", + "script", + "the", + "there", + "not", + "library", + "widget", + "metadata", } ), # --- PHASE 1: LOGIC TOPOLOGY & STRUCTURE --- diff --git a/gitgalaxy/standards/language_standards/languages/lua.py b/gitgalaxy/standards/language_standards/languages/lua.py index 552795ae46..b6b3717b25 100644 --- a/gitgalaxy/standards/language_standards/languages/lua.py +++ b/gitgalaxy/standards/language_standards/languages/lua.py @@ -53,6 +53,16 @@ "rawlen", "next", "select", + # #3359 (contract C2): keywords and special forms, never calls + "not", + "or", + "and", + "elseif", + "then", + "until", + "in", + "else", + "local", } ), # --- PHASE 1: LOGIC TOPOLOGY & STRUCTURE --- diff --git a/gitgalaxy/standards/language_standards/languages/perl.py b/gitgalaxy/standards/language_standards/languages/perl.py index 395f0c9805..f4572ed41e 100644 --- a/gitgalaxy/standards/language_standards/languages/perl.py +++ b/gitgalaxy/standards/language_standards/languages/perl.py @@ -70,6 +70,17 @@ "chomp", "die", "warn", + # #3359 (contract C2): keywords and special forms, never calls + "qw", + "q", + "qq", + "qr", + "qx", + "and", + "or", + "not", + "until", + "sub", } ), # --- PHASE 1: LOGIC TOPOLOGY & STRUCTURE --- diff --git a/gitgalaxy/standards/language_standards/languages/php.py b/gitgalaxy/standards/language_standards/languages/php.py index af84e6f3a4..460ebf2a57 100644 --- a/gitgalaxy/standards/language_standards/languages/php.py +++ b/gitgalaxy/standards/language_standards/languages/php.py @@ -62,6 +62,23 @@ "rules": { # Epic #3264: Explicitly declare the structural invocation paradigm "calls_out": CALLS_OUT_C_STYLE, + # #3359 (contract C2): keywords and special forms, never calls + "_calls_out_ignore": frozenset( + { + "foreach", + "isset", + "empty", + "elseif", + "array", + "unset", + "use", + "fn", + "die", + "exit", + "list", + "declare", + } + ), # --- PHASE 1: LOGIC TOPOLOGY & STRUCTURE --- # 1. branch (Control Flow / Branching) # Control flow. Includes modern match expression. EXCLUDES throw (bailout_hits). diff --git a/gitgalaxy/standards/language_standards/languages/powershell.py b/gitgalaxy/standards/language_standards/languages/powershell.py index a27a4ffedc..b0d86039df 100644 --- a/gitgalaxy/standards/language_standards/languages/powershell.py +++ b/gitgalaxy/standards/language_standards/languages/powershell.py @@ -51,7 +51,39 @@ # Epic #3264: Explicitly declare the structural invocation paradigm "calls_out": CALLS_OUT_COMMAND_POSITION, "_calls_out_ignore": frozenset( - {"param", "begin", "process", "end", "elseif", "else", "foreach", "do", "until", "trap"} + { + "param", + "begin", + "process", + "end", + "elseif", + "else", + "foreach", + "do", + "until", + "trap", + # #3359 (contract C2): keywords and special forms, never calls + "break", + "continue", + "exit", + "default", + "function", + "filter", + "return", + "throw", + "static", + "enum", + "class", + "dynamicparam", + "try", + "catch", + "finally", + "if", + "for", + "while", + "switch", + "hidden", + } ), # --- PHASE 1: LOGIC TOPOLOGY & STRUCTURE --- # branch: decisions that split flow. Includes ternary operators (?) and null-coalescing (??). diff --git a/gitgalaxy/standards/language_standards/languages/python.py b/gitgalaxy/standards/language_standards/languages/python.py index 5e6cefde94..8172dc1e89 100644 --- a/gitgalaxy/standards/language_standards/languages/python.py +++ b/gitgalaxy/standards/language_standards/languages/python.py @@ -83,6 +83,25 @@ "rules": { # Epic #3264: Explicitly declare the structural invocation paradigm "calls_out": CALLS_OUT_C_STYLE, + # #3359 (contract C2): keywords and special forms, never calls + "_calls_out_ignore": frozenset( + { + "in", + "not", + "and", + "or", + "elif", + "is", + "yield", + "lambda", + "del", + "with", + "raise", + "from", + "as", + "else", + } + ), # --- PHASE 1: LOGIC TOPOLOGY & STRUCTURE --- # 1. branch (Control Flow / Branching) # Includes match/case (3.10+) and logical short-circuits. EXCLUDES exceptions. diff --git a/gitgalaxy/standards/language_standards/languages/rust.py b/gitgalaxy/standards/language_standards/languages/rust.py index 79df9261a4..1e25cdacd8 100644 --- a/gitgalaxy/standards/language_standards/languages/rust.py +++ b/gitgalaxy/standards/language_standards/languages/rust.py @@ -43,6 +43,25 @@ "rules": { # Epic #3264: Explicitly declare the structural invocation paradigm "calls_out": CALLS_OUT_C_STYLE, + # #3359 (contract C2): keywords and special forms, never calls + "_calls_out_ignore": frozenset( + { + "pub", + "let", + "fn", + "match", + "mut", + "in", + "ref", + "move", + "unsafe", + "dyn", + "as", + "else", + "impl", + "where", + } + ), # --- PHASE 1: LOGIC TOPOLOGY & STRUCTURE --- # 1. branch (Control Flow / Branching) # Decisions and logical jumps. EXCLUDES panic!/throw (bailout_hits). diff --git a/gitgalaxy/standards/language_standards/languages/scala.py b/gitgalaxy/standards/language_standards/languages/scala.py index a9bff3fd0d..d72eb7659f 100644 --- a/gitgalaxy/standards/language_standards/languages/scala.py +++ b/gitgalaxy/standards/language_standards/languages/scala.py @@ -11,7 +11,7 @@ import re from typing import Any -from .._shared_patterns import CALLS_OUT_C_STYLE, GLOBAL_FRAGILE_DEBT, GLOBAL_PLANNED_DEBT +from .._shared_patterns import CALLS_OUT_C_STYLE_NO_ANNOTATION, GLOBAL_FRAGILE_DEBT, GLOBAL_PLANNED_DEBT DEFINITION: dict[str, Any] = { "_meta": { @@ -41,7 +41,18 @@ "lexical_family": "recursive_block", "rules": { # Epic #3264: Explicitly declare the structural invocation paradigm - "calls_out": CALLS_OUT_C_STYLE, + "calls_out": CALLS_OUT_C_STYLE_NO_ANNOTATION, # #3359: `@Name(` is an annotation (C1) + # #3359 (contract C2): keywords and special forms, never calls + "_calls_out_ignore": frozenset( + { + "case", + "val", + "var", + "else", + "yield", + "implicit", + } + ), # --- PHASE 1: LOGIC TOPOLOGY & STRUCTURE --- # 1. branch: decisions that split flow. Includes Scala 3 if-then and match-case. "branch": re.compile( diff --git a/gitgalaxy/standards/language_standards/languages/scheme.py b/gitgalaxy/standards/language_standards/languages/scheme.py index 41efd344d9..57b0cba3e8 100644 --- a/gitgalaxy/standards/language_standards/languages/scheme.py +++ b/gitgalaxy/standards/language_standards/languages/scheme.py @@ -67,6 +67,33 @@ "define-syntax", "syntax-rules", "set!", + # #3359 (contract C2): keywords and special forms, never calls + "let*", + "letrec", + "letrec*", + "let-values", + "let*-values", + "define-values", + "define-record-type", + "case-lambda", + "syntax-case", + "with-syntax", + "let-syntax", + "letrec-syntax", + "parameterize", + "fluid-let", + "guard", + "quasiquote", + "unquote", + "unquote-splicing", + "delay-force", + "fields", + "mutable", + "immutable", + "sealed", + "opaque", + "protocol", + "nongenerative", } ), # --- PHASE 1: LOGIC TOPOLOGY & STRUCTURE --- diff --git a/gitgalaxy/standards/language_standards/languages/solidity.py b/gitgalaxy/standards/language_standards/languages/solidity.py index 16f4dd0789..4135a14d3d 100644 --- a/gitgalaxy/standards/language_standards/languages/solidity.py +++ b/gitgalaxy/standards/language_standards/languages/solidity.py @@ -40,6 +40,15 @@ "rules": { # Epic #3264: Explicitly declare the structural invocation paradigm "calls_out": CALLS_OUT_C_STYLE, + # #3359 (contract C2): keywords and special forms, never calls + "_calls_out_ignore": frozenset( + { + "returns", + "assembly", + "override", + "mapping", + } + ), # --- PHASE 1: LOGIC TOPOLOGY & STRUCTURE --- # 1. branch: Decisions that split flow. Includes Solidity 0.6+ try/catch. # #2545: `return` removed -- was phantom-counting every early-return function as a diff --git a/gitgalaxy/standards/language_standards/languages/swift.py b/gitgalaxy/standards/language_standards/languages/swift.py index b4d27956d8..c3919a00f2 100644 --- a/gitgalaxy/standards/language_standards/languages/swift.py +++ b/gitgalaxy/standards/language_standards/languages/swift.py @@ -11,7 +11,7 @@ import re from typing import Any -from .._shared_patterns import CALLS_OUT_C_STYLE, GLOBAL_FRAGILE_DEBT, GLOBAL_PLANNED_DEBT +from .._shared_patterns import CALLS_OUT_C_STYLE_NO_ANNOTATION, GLOBAL_FRAGILE_DEBT, GLOBAL_PLANNED_DEBT DEFINITION: dict[str, Any] = { "_meta": { @@ -34,7 +34,15 @@ "lexical_family": "recursive_block", "rules": { # Epic #3264: Explicitly declare the structural invocation paradigm - "calls_out": CALLS_OUT_C_STYLE, + "calls_out": CALLS_OUT_C_STYLE_NO_ANNOTATION, # #3359: `@Name(` is an annotation (C1) + # #3359 (contract C2): keywords and special forms, never calls + "_calls_out_ignore": frozenset( + { + "case", + "let", + "var", + } + ), # --- PHASE 1: LOGIC TOPOLOGY & STRUCTURE --- # 1. branch (Control Flow / Branching) # Decisions and logical jumps. Includes modern typed throws (throws(Error)). diff --git a/gitgalaxy/standards/language_standards/languages/tcl.py b/gitgalaxy/standards/language_standards/languages/tcl.py index 563aee312e..be6a83030a 100644 --- a/gitgalaxy/standards/language_standards/languages/tcl.py +++ b/gitgalaxy/standards/language_standards/languages/tcl.py @@ -11,7 +11,7 @@ import re from typing import Any -from .._shared_patterns import CALLS_OUT_COMMAND_POSITION, GLOBAL_FRAGILE_DEBT, GLOBAL_PLANNED_DEBT +from .._shared_patterns import GLOBAL_FRAGILE_DEBT, GLOBAL_PLANNED_DEBT DEFINITION: dict[str, Any] = { "_meta": { @@ -34,7 +34,19 @@ "lexical_family": "line_exclusive", "rules": { # Epic #3264: Explicitly declare the structural invocation paradigm - "calls_out": CALLS_OUT_COMMAND_POSITION, + # #3359 (contract C7): command position, minus the upper-case SQL keywords + # that start the lines of a brace-quoted query (`db eval { SELECT ...\n + # WHERE ... }`). Tcl cannot shield braces, which are also code bodies, so + # the SQL words are excluded by name. Case-SENSITIVE on purpose: the + # `_calls_out_ignore` set below is casefolded, and `set`/`update` are real + # Tcl commands. The lookahead is one bounded alternation (Rule 5). + "calls_out": re.compile( + r"(?m)^[ \t]*(?!(?:SELECT|INSERT|UPDATE|DELETE|REPLACE|FROM|WHERE|SET|VALUES|INTO|AND|OR|NOT" + r"|UNION|WITH|AS|ON|USING|JOIN|INNER|OUTER|LEFT|CROSS|NATURAL|ORDER|GROUP|HAVING|LIMIT|OFFSET" + r"|CREATE|DROP|ALTER|TABLE|INDEX|VIEW|TRIGGER|BEGIN|COMMIT|ROLLBACK|PRAGMA|COALESCE|CASE|WHEN" + r"|THEN|ELSE|END|EXCEPT|INTERSECT|RETURNING|DISTINCT|ALL|IN|IS|NULL|LIKE|BETWEEN)(?![\w:-]))" + r"([A-Za-z_][\w:-]*)\b" + ), "_calls_out_ignore": frozenset( { "proc", diff --git a/gitgalaxy/standards/language_standards/languages/typescript.py b/gitgalaxy/standards/language_standards/languages/typescript.py index 707b485077..e7c247ae4a 100644 --- a/gitgalaxy/standards/language_standards/languages/typescript.py +++ b/gitgalaxy/standards/language_standards/languages/typescript.py @@ -60,6 +60,17 @@ "rules": { # Epic #3264: Explicitly declare the structural invocation paradigm "calls_out": CALLS_OUT_C_STYLE, + # #3359 (contract C2): keywords and special forms, never calls + "_calls_out_ignore": frozenset( + { + "async", + "as", + "new", + "void", + "instanceof", + "keyof", + } + ), # --- PHASE 1: LOGIC TOPOLOGY & STRUCTURE --- # --- PHASE 1: LOGIC TOPOLOGY & STRUCTURE --- # 1. branch (Control Flow / Branching) diff --git a/gitgalaxy/standards/language_standards/languages/zig.py b/gitgalaxy/standards/language_standards/languages/zig.py index 8aaa161a09..f626c2eeed 100644 --- a/gitgalaxy/standards/language_standards/languages/zig.py +++ b/gitgalaxy/standards/language_standards/languages/zig.py @@ -34,6 +34,23 @@ "rules": { # Epic #3264: Explicitly declare the structural invocation paradigm "calls_out": CALLS_OUT_C_STYLE, + # #3359 (contract C2): keywords and special forms, never calls + "_calls_out_ignore": frozenset( + { + "and", + "or", + "orelse", + "else", + "fn", + "callconv", + "align", + "addrspace", + "linksection", + "union", + "enum", + "struct", + } + ), # --- PHASE 1: LOGIC TOPOLOGY & STRUCTURE --- # 1. branch: decisions that split flow. Includes unique 'orelse' and 'catch' patterns. # #2545: `return` removed -- was phantom-counting every early-return function as a diff --git a/gitgalaxy/standards/signal_contracts.py b/gitgalaxy/standards/signal_contracts.py index 2b9614272b..5b0520b95f 100644 --- a/gitgalaxy/standards/signal_contracts.py +++ b/gitgalaxy/standards/signal_contracts.py @@ -768,7 +768,7 @@ def _c(name, phase, kind, contract, **kw): # tabular constructor, see rows belo "_visibility_export_list": "capture group(s) = a region holding MANY exported names, same census (#2823)", "_args_arrow_count_groups": "args strategy: arrow-function parameter groups", "_transfers_out": "regex whose one group names the target of an unconditional transfer of control (COBOL GO TO) -- recorded per function as transfers_to beside calls_out_to, never in it (calls_out contract C4, #3362)", - "_calls_out_ignore": "frozenset of lowercase callee names UNIONed with the detector's global calls_out ignore set, compared casefolded (case-insensitive languages filter keywords in any spelling) -- Epic #3264 Phase 3 (#3282)", + "_calls_out_ignore": "frozenset of callee names UNIONed with the detector's global calls_out ignore set; a language declared `identifier_case: insensitive` authors them lowercase and compares casefolded (keywords filtered in any spelling), every other language compares exactly (#3359) -- Epic #3264 Phase 3 (#3282)", "_args_bare_body_groups": "args strategy: bare-body parameter groups", "_args_colon_selector_groups": "args strategy: colon-selector parameter groups (objective-c)", "_args_findall_max_groups": "args strategy: take the maximum over findall groups", diff --git a/tests/extraction/languages/test_calls_out_contract_3327.py b/tests/extraction/languages/test_calls_out_contract_3327.py index c07ca0ed88..06e9c4685f 100644 --- a/tests/extraction/languages/test_calls_out_contract_3327.py +++ b/tests/extraction/languages/test_calls_out_contract_3327.py @@ -95,15 +95,3 @@ def test_builtins_are_calls(): def test_nested_declaration_is_not_a_call(): code = "def outer(x):\n def inner(y):\n return y\n return inner\n" assert "inner" not in _calls("python", code)["outer"] - - -@pytest.mark.xfail(strict=True, reason="#3359: a metadata annotation is captured as a call") -def test_annotation_is_not_a_call(): - code = 'class A {\n @SuppressWarnings("x")\n void run(int a) {\n go(a);\n }\n}\n' - assert "SuppressWarnings" not in _calls("java", code)["run"] - - -@pytest.mark.xfail(strict=True, reason="#3359: go's `func` literal keyword is captured as a call") -def test_func_literal_keyword_is_not_a_call(): - code = "package m\nfunc Run(a int) {\n go func() { work(a) }()\n}\n" - assert "func" not in _calls("go", code)["Run"] diff --git a/tests/extraction/languages/test_calls_out_keywords_3359.py b/tests/extraction/languages/test_calls_out_keywords_3359.py new file mode 100644 index 0000000000..50ca76c3ed --- /dev/null +++ b/tests/extraction/languages/test_calls_out_keywords_3359.py @@ -0,0 +1,251 @@ +""" +#3359: the `calls_out` contract's keyword (C2), annotation (C1) and string (C7) classes +(docs/calls_out_rule_contract.md, classes K / A / S), pinned end to end through +`StructuralExtractor.splice()` so the literal shield and the ignore sets are in the path. + +Every case pairs the non-call it must drop with a real call from the same body, so a fix that +over-filters (recall loss) fails as loudly as one that under-filters. +""" + +import pytest + +from gitgalaxy.core.detector import StructuralExtractor +from gitgalaxy.standards.language_standards import LANGUAGE_DEFINITIONS +from gitgalaxy.standards.language_standards._shared_patterns import ( + CALLS_OUT_C_STYLE, + CALLS_OUT_C_STYLE_NO_ANNOTATION, +) + + +def _calls(lang: str, code: str) -> dict[str, list[str]]: + functions = StructuralExtractor(lang, LANGUAGE_DEFINITIONS).splice(code, "")["functions"] + return {f["name"]: f["calls_out_to"] for f in functions} + + +# (lang, code, function, callees that must be kept, non-calls that must be dropped) +_KEYWORD_CASES = [ + ( + "go", + "package m\nfunc Run(a int) {\n var (\n x = 1\n )\n go func() { work(a) }()\n}\n", + "Run", + {"work"}, + {"func", "var"}, + ), + ( + "php", + " $y; }\n" + " $m = array(1);\n $this->match($m);\n}\n", + "run", + {"save", "match"}, + {"foreach", "isset", "empty", "unset", "elseif", "fn", "array"}, + ), + ( + "csharp", + "class A {\n void Run(int[] xs) {\n foreach (var x in xs) { Save(x); }\n" + " var n = nameof(Run);\n var (a, b) = Pair();\n lock (this) { Go(); }\n }\n}\n", + "Run", + {"Save", "Pair", "Go"}, + {"foreach", "nameof", "var", "lock"}, + ), + ( + "rust", + "pub(crate) fn run(v: Vec) {\n let (a, b) = split(v);\n match (a, b) { _ => go() }\n}\n", + "run", + {"split", "go"}, + {"let", "match"}, + ), + ( + "python", + "def run(x, xs):\n if x in (1, 2) and not (x or y):\n go(x)\n elif(x):\n stop()\n", + "run", + {"go", "stop"}, + {"in", "and", "not", "or", "elif"}, + ), + ( + "lua", + "function run(x)\n if not (x) or (y) then\n go(x)\n end\nend\n", + "run", + {"go"}, + {"not", "or"}, + ), + ( + "typescript", + "function run(p: Promise) {\n const f = async () => { await go(); };\n return p as (unknown);\n}\n", + "run", + {"go"}, + {"async", "as"}, + ), + ( + "solidity", + 'contract C {\n function run(uint a) public returns (uint) {\n assembly ("memory-safe") { }\n' + " return helper(a);\n }\n}\n", + "run", + {"helper"}, + {"returns", "assembly"}, + ), + ( + "scala", + "object O {\n def run(xs: List[Int]): Unit = {\n val (a, b) = xs.splitAt(1)\n" + " xs match { case (h) => go(h) }\n }\n}\n", + "run", + {"splitAt", "go"}, + {"val", "case"}, + ), + ( + "java", + "class A {\n A(int x) {\n this(x, 0);\n init(x);\n }\n}\n", + "A", + {"init"}, + {"this"}, + ), + ( + "perl", + "sub run {\n my @a = qw(a b);\n go(@a) and (stop()) or (halt());\n $dbh->do('x');\n}\n", + "run", + {"go", "stop", "halt", "do"}, + {"qw", "and", "or"}, + ), + ( + "zig", + "fn run(a: u8) callconv(.C) void {\n const E = enum(u8) { x };\n if (a == 1 and (b)) go();\n}\n", + "run", + {"go"}, + {"callconv", "enum", "and"}, + ), + ( + "ada", + "procedure Run is\nbegin\n if not (X) then\n Go (X);\n end if;\nend Run;\n", + "Run", + {"Go"}, + {"not"}, + ), + ( + "cpp", + "void run(int x) {\n static_assert(sizeof(int) == 4);\n if constexpr (true) { go(x); }\n}\n", + "run", + {"go"}, + {"static_assert", "constexpr"}, + ), + ( + "powershell", + "function Run {\n Write-Host 'x'\n Return\n break\n exit 1\n}\n", + "Run", + {"Write-Host"}, + {"Return", "break", "exit"}, + ), + ( + "livecode", + 'command doIt\n private\n variable tX\n answer "x"\n helper tX\nend doIt\n', + "doIt", + {"helper"}, + {"private", "variable"}, + ), + ( + "scheme", + "(define (run x)\n (let* ((a (car x)))\n (let-values (((b c) (split a)))\n (go b))))\n", + "run", + {"car", "split", "go"}, + {"let*", "let-values"}, + ), +] + + +@pytest.mark.parametrize("lang, code, fn, keep, drop", _KEYWORD_CASES, ids=[c[0] for c in _KEYWORD_CASES]) +def test_keyword_is_not_a_call(lang, code, fn, keep, drop): + # C2: a keyword or special form followed by `(` is never a call; the real calls beside it stay. + calls = _calls(lang, code)[fn] + assert keep <= set(calls), calls + assert not drop & set(calls), calls + + +@pytest.mark.parametrize( + "lang, code, fn", + [ + ("java", 'class A {\n @SuppressWarnings("x")\n void run(int a) {\n go(a);\n }\n}\n', "run"), + ("kotlin", "class A {\n @Throws(E::class)\n fun run(a: Int) {\n go(a)\n }\n}\n", "run"), + ("swift", "@available(iOS 13, *)\nfunc run(a: Int) {\n go(a)\n}\n", "run"), + ("dart", "class A {\n @Deprecated('x')\n void run(int a) {\n go(a);\n }\n}\n", "run"), + ("groovy", "class A {\n @Issue('x')\n def run(a) {\n go(a)\n }\n}\n", "run"), + ("scala", 'object O {\n @deprecated("x")\n def run(a: Int): Unit = {\n go(a)\n }\n}\n', "run"), + ], +) +def test_annotation_is_not_a_call(lang, code, fn): + # C1: a metadata annotation is a declaration, even with parentheses. The annotation may sit + # outside the sliced body; either way it must not surface as a callee. + found = _calls(lang, code) + for calls in found.values(): + assert not {"SuppressWarnings", "Throws", "available", "Deprecated", "Issue", "deprecated"} & set(calls) + assert "go" in found[fn] + + +def test_annotation_languages_use_the_no_annotation_pattern(): + for lang in ("java", "kotlin", "swift", "dart", "groovy", "scala"): + assert LANGUAGE_DEFINITIONS[lang]["rules"]["calls_out"] is CALLS_OUT_C_STYLE_NO_ANNOTATION + # Python/TypeScript/JavaScript decorator factories ARE invocations (C1) -- the plain pattern. + for lang in ("python", "typescript", "javascript"): + assert LANGUAGE_DEFINITIONS[lang]["rules"]["calls_out"] is CALLS_OUT_C_STYLE + + +def test_decorator_factory_is_still_a_call(): + # C1: `@retry(3)` invokes `retry`; the annotation rule must not reach python. + code = "class A:\n def run(self):\n @retry(3)\n def inner():\n pass\n go()\n" + assert "retry" in _calls("python", code)["run"] + + +def test_no_annotation_pattern_keeps_qualifiers(): + # #3329: the qualifier rides beside calls_out_to for the annotation-free variant too. + code = "class A {\n void run(Store s) {\n s.save(1);\n }\n}\n" + fn = next(f for f in StructuralExtractor("java", LANGUAGE_DEFINITIONS).splice(code, "")["functions"]) + assert fn["calls_out_qualifiers"].get("save") == ["s"] + + +def test_tcl_brace_quoted_sql_is_not_a_call(): + # C7: SQL keywords that start the lines of a brace-quoted query are string content. + code = ( + "proc run {db} {\n $db eval {\n SELECT a FROM t\n WHERE x = 1\n }\n lappend out x\n cleanup $db\n}\n" + ) + calls = _calls("tcl", code)["run"] + assert {"lappend", "cleanup"} <= set(calls) + assert not {"SELECT", "WHERE"} & set(calls) + + +@pytest.mark.parametrize( + "lang, code, fn, callee", + [ + # A case-sensitive language compares its ignore set exactly: the keyword never + # swallows a real callee spelled with capitals. + ("go", "package m\nfunc Run(v Value) {\n t := v.Type()\n use(t)\n}\n", "Run", "Type"), + ("csharp", "class A {\n void Run() {\n var x = factory.This();\n }\n}\n", "Run", "This"), + ("perl", "sub run {\n my $self = shift;\n $self->Warn('x');\n}\n", "run", "Warn"), + ], +) +def test_case_sensitive_ignore_keeps_capitalised_callee(lang, code, fn, callee): + assert callee in _calls(lang, code)[fn] + + +def test_case_insensitive_ignore_folds(): + # powershell is `identifier_case: insensitive`: `Return`/`RETURN` are the keyword. + calls = _calls("powershell", "function Run {\n Write-Host 'x'\n RETURN\n}\n")["Run"] + assert "RETURN" not in calls and "Write-Host" in calls + + +def test_cobol_inline_perform_is_not_a_call(): + code = ( + " IDENTIFICATION DIVISION.\n PROGRAM-ID. P.\n PROCEDURE DIVISION.\n" + " MAIN-PARA.\n PERFORM VARYING I FROM 1 BY 1 UNTIL I > 3\n" + " DISPLAY I\n END-PERFORM\n PERFORM UNTIL X = 1\n" + " MOVE 1 TO X\n END-PERFORM\n PERFORM WORK-PARA.\n" + " WORK-PARA.\n DISPLAY 'W'.\n" + ) + calls = _calls("cobol", code)["MAIN-PARA"] + assert "WORK-PARA" in calls + # Inline PERFORM names no paragraph; and `END-PERFORM` ends in the verb, so the next + # statement's first word (`PERFORM`, `MOVE`) must not be read as its target. + assert not {"VARYING", "UNTIL", "PERFORM", "MOVE"} & set(calls) + + +def test_agc_tc_q_is_a_return_not_a_call(): + code = "ROUTINE\t\tTC\tHELPER\n\t\tTC\tQ\n" + calls = [c for v in _calls("agc_assembly", code).values() for c in v] + assert "Q" not in calls From a768a077b0a2f2900f8c6a26f80d401f35274d8b Mon Sep 17 00:00:00 2001 From: Joe Esquibel Date: Wed, 23 Sep 2026 14:11:39 -0400 Subject: [PATCH 2/7] calls_out(perl): keep qx() -- it runs a command (#3359) Co-Authored-By: Claude Opus 5.5 (1M context) --- gitgalaxy/standards/language_standards/languages/perl.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/gitgalaxy/standards/language_standards/languages/perl.py b/gitgalaxy/standards/language_standards/languages/perl.py index f4572ed41e..3d6830626f 100644 --- a/gitgalaxy/standards/language_standards/languages/perl.py +++ b/gitgalaxy/standards/language_standards/languages/perl.py @@ -70,12 +70,13 @@ "chomp", "die", "warn", - # #3359 (contract C2): keywords and special forms, never calls + # #3359 (contract C2/C7): keywords and quote-like operators, never + # calls. `qx(...)` is deliberately absent: it runs a shell command, + # an invocation like `system` (the rosetta oracle plants it as one). "qw", "q", "qq", "qr", - "qx", "and", "or", "not", From 94b3d8500e76ce694f4ab9589ce20e5e1493cc58 Mon Sep 17 00:00:00 2001 From: Joe Esquibel Date: Wed, 23 Sep 2026 14:14:53 -0400 Subject: [PATCH 3/7] docs: record the #3359 resolution in the calls_out contract Co-Authored-By: Claude Opus 5.5 (1M context) --- docs/calls_out_rule_contract.md | 28 ++++++++++++++++++++++++++++ 1 file changed, 28 insertions(+) diff --git a/docs/calls_out_rule_contract.md b/docs/calls_out_rule_contract.md index f0d43ebbc4..ac4223bb53 100644 --- a/docs/calls_out_rule_contract.md +++ b/docs/calls_out_rule_contract.md @@ -175,6 +175,34 @@ Summary: 37 of 65 languages agree outside the global **B**. The rest are **K** ( **D** (8), **S** (1: tcl), **G** (1: cobol), **I** (1: matlab) and **R** (1: haskell). Every disagreement is precision (a non-call emitted), except **B**, which is recall. +## #3359 resolution (classes K, A, S) + +Fixed by #3359, re-censused on the same crucible plus keyword-rosetta: 4,300+ non-call names +left `calls_out_to` across 25 languages, and every one was checked to be a keyword, special +form, annotation or string word (no real callee dropped). + +- **K:** keyword entries added to each language's `_calls_out_ignore` (built-ins and + `_CALLS_OUT_GLOBAL_IGNORE` untouched; #3361 owns those). The per-language set is now compared + **exactly** in a case-sensitive language and casefolded only for `identifier_case: + insensitive`, so a keyword never swallows a capitalised callee (go `v.Type()`, C# + `factory.This()`, perl `$self->Warn(`). +- **A:** `CALLS_OUT_C_STYLE_NO_ANNOTATION` (`(? TIMES`. Not keywords on inspection, so kept as calls: perl `do`/`then` +(every crucible hit is `$dbh->do(` / `->then(`), php `match` (`$this->match(`), tcl `default` +(a macports command), perl `qx(` (runs a shell command). + ## How the census was taken A scratch probe (not committed; same shape as `tests/tools/wrapper_probe.py`) ran the From ef25e2190dd8b63041d80a12310744e85cf35d71 Mon Sep 17 00:00:00 2001 From: Joe Esquibel Date: Wed, 23 Sep 2026 14:25:00 -0400 Subject: [PATCH 4/7] test: calls_out ignore folds only for identifier_case: insensitive (#3359) Co-Authored-By: Claude Opus 5.5 (1M context) --- tests/core_engine/test_detector.py | 35 ++++++++++++++++++------------ 1 file changed, 21 insertions(+), 14 deletions(-) diff --git a/tests/core_engine/test_detector.py b/tests/core_engine/test_detector.py index 46eadefccc..1d20f2a9d7 100644 --- a/tests/core_engine/test_detector.py +++ b/tests/core_engine/test_detector.py @@ -1110,13 +1110,15 @@ def test_detector_classification_and_wiring(): def test_detector_calls_out_language_ignore_union(): """ Proves the per-language `_calls_out_ignore` rule (Epic #3264 Phase 3) is - unioned with the global ignore set and compared casefolded: a language - authoring lowercase words filters them in any spelling (case-insensitive - languages get correct behavior for free), while the global set keeps - filtering exactly as before. + unioned with the global ignore set. A language declared + `identifier_case: insensitive` compares it casefolded (lowercase words filter + in any spelling); #3359: every other language compares it exactly, so a + keyword never swallows a capitalised callee. The global set keeps filtering + exactly as before. """ - defs = { - "fortranish": { + + def defs(identifier_case): + definition = { "lexical_family": "single_line_only", "rules": { "func_start": re.compile(r"^[ \t]*def\s+([a-zA-Z_][a-zA-Z0-9_]*)\s*\(", re.M), @@ -1124,16 +1126,21 @@ def test_detector_calls_out_language_ignore_union(): "_calls_out_ignore": frozenset({"open"}), }, } - } - opt_detector = StructuralExtractor("fortranish", defs) - code = "def dispatch(unit):\n OPEN(unit)\n Open(unit)\n print(unit)\n db_insert(unit)\n" + if identifier_case: + definition["identifier_case"] = identifier_case + return {"fortranish": definition} - result = opt_detector.splice(code, "") - func = result["functions"][0] + code = "def dispatch(unit):\n OPEN(unit)\n Open(unit)\n open(unit)\n print(unit)\n db_insert(unit)\n" + + folded = StructuralExtractor("fortranish", defs("insensitive")).splice(code, "")["functions"][0] + assert folded["calls_out_to"] == ["db_insert"], ( + "_calls_out_ignore must filter casefolded (OPEN/Open/open) in a case-insensitive language " + f"and the global set must keep filtering (print); got {folded['calls_out_to']}" + ) - assert func["calls_out_to"] == ["db_insert"], ( - "_calls_out_ignore must filter casefolded (OPEN/Open) and the global " - f"set must keep filtering (print); got {func['calls_out_to']}" + exact = StructuralExtractor("fortranish", defs(None)).splice(code, "")["functions"][0] + assert exact["calls_out_to"] == ["OPEN", "Open", "db_insert"], ( + f"a case-sensitive language filters only the exact spelling; got {exact['calls_out_to']}" ) From 07990d2925d5f2f6a70c787515b4d26cb95c54ac Mon Sep 17 00:00:00 2001 From: Joe Esquibel Date: Wed, 23 Sep 2026 14:46:15 -0400 Subject: [PATCH 5/7] docs(tcl): the SQL exclusion's reason under exact-case ignore sets (#3359) Co-Authored-By: Claude Opus 5.5 (1M context) --- docs/calls_out_rule_contract.md | 6 +++--- gitgalaxy/standards/language_standards/languages/tcl.py | 5 ++--- 2 files changed, 5 insertions(+), 6 deletions(-) diff --git a/docs/calls_out_rule_contract.md b/docs/calls_out_rule_contract.md index ac4223bb53..03cb1f3099 100644 --- a/docs/calls_out_rule_contract.md +++ b/docs/calls_out_rule_contract.md @@ -188,10 +188,10 @@ form, annotation or string word (no real callee dropped). `factory.This()`, perl `$self->Warn(`). - **A:** `CALLS_OUT_C_STYLE_NO_ANNOTATION` (`(? Date: Wed, 23 Sep 2026 14:58:42 -0400 Subject: [PATCH 6/7] call_graph_accuracy: score the #3327 contract on the tree-sitter side too (#3359) tree-sitter parses php isset/empty/unset/die/exit and C# nameof as call nodes; the contract says they are keywords (C2), and the engine now drops them. Exclude them from the tree-sitter side so the gate does not read following the contract as lost recall. Baseline regenerated: precision up in go (90.3 -> 99.6), php (88.1 -> 96.3), csharp (89.5 -> 93.8), python, rust, java, cpp, typescript; recall unchanged except php 98.3 -> 98.2 (the excluded names left the matched set). Co-Authored-By: Claude Opus 5.5 (1M context) --- tests/call_graph_accuracy_baseline.json | 42 ++++++++++++------------- tests/tools/call_graph_accuracy.py | 13 +++++++- 2 files changed, 33 insertions(+), 22 deletions(-) diff --git a/tests/call_graph_accuracy_baseline.json b/tests/call_graph_accuracy_baseline.json index 0c11858917..a710708949 100644 --- a/tests/call_graph_accuracy_baseline.json +++ b/tests/call_graph_accuracy_baseline.json @@ -10,37 +10,37 @@ }, "cpp": { "fn": 513, - "fp": 1045, + "fp": 1014, "matched_functions": 1280, "parse_failures": 0, - "precision_pct": 87.4, + "precision_pct": 87.7, "recall_pct": 93.4, "tp": 7257 }, "csharp": { - "fn": 76, - "fp": 306, + "fn": 73, + "fp": 171, "matched_functions": 650, "parse_failures": 0, - "precision_pct": 89.5, + "precision_pct": 93.8, "recall_pct": 97.2, - "tp": 2612 + "tp": 2575 }, "go": { "fn": 515, - "fp": 652, + "fp": 24, "matched_functions": 1294, "parse_failures": 0, - "precision_pct": 90.3, + "precision_pct": 99.6, "recall_pct": 92.2, "tp": 6061 }, "java": { "fn": 60, - "fp": 60, + "fp": 18, "matched_functions": 735, "parse_failures": 0, - "precision_pct": 96.9, + "precision_pct": 99.1, "recall_pct": 96.9, "tp": 1886 }, @@ -54,20 +54,20 @@ "tp": 2028 }, "php": { - "fn": 97, - "fp": 766, + "fn": 95, + "fp": 206, "matched_functions": 1548, "parse_failures": 0, - "precision_pct": 88.1, - "recall_pct": 98.3, - "tp": 5647 + "precision_pct": 96.3, + "recall_pct": 98.2, + "tp": 5316 }, "python": { "fn": 1161, - "fp": 861, + "fp": 666, "matched_functions": 4045, "parse_failures": 0, - "precision_pct": 92.1, + "precision_pct": 93.8, "recall_pct": 89.7, "tp": 10069 }, @@ -82,19 +82,19 @@ }, "rust": { "fn": 1042, - "fp": 1092, + "fp": 944, "matched_functions": 1774, "parse_failures": 0, - "precision_pct": 84.9, + "precision_pct": 86.7, "recall_pct": 85.5, "tp": 6150 }, "typescript": { "fn": 546, - "fp": 1253, + "fp": 1215, "matched_functions": 2847, "parse_failures": 0, - "precision_pct": 80.7, + "precision_pct": 81.2, "recall_pct": 90.6, "tp": 5241 } diff --git a/tests/tools/call_graph_accuracy.py b/tests/tools/call_graph_accuracy.py index 8f58e2974f..a679ccf52a 100644 --- a/tests/tools/call_graph_accuracy.py +++ b/tests/tools/call_graph_accuracy.py @@ -73,6 +73,16 @@ _QUALIFIED_TYPES = frozenset({"scoped_identifier", "qualified_identifier", "scoped_type_identifier"}) _LINE_SLACK = 3 # decorators/annotations/doc comments can move a start line +# #3359: names tree-sitter parses as a call node but the #3327 contract says are +# keywords, never calls (C2) -- language constructs and operator keywords the +# engine's `_calls_out_ignore` now drops. Removed from the tree-sitter side so +# both sides score the same contract instead of the engine losing "recall" for +# following it. Built-ins are NOT here: they are calls (decision 1, #3361). +CONTRACT_NON_CALLS: dict[str, frozenset[str]] = { + "php": frozenset({"isset", "empty", "unset", "die", "exit", "array", "list"}), + "csharp": frozenset({"nameof"}), +} + # Gate tolerance, in percentage points, before --ci calls a drop a regression. TOLERANCE_PP = 0.5 @@ -119,6 +129,7 @@ def ts_functions(source: bytes, lang: str, audit: Any) -> list[tuple[str, int, s spec = audit.NODE_MAPS[lang] tree = tree_sitter_language_pack.get_parser(spec["ts_lang"]).parse(source) func_types = spec["func_node_types"] + non_calls = CONTRACT_NON_CALLS.get(lang, frozenset()) out: list[tuple[str, int, set[str]]] = [] stack: list[tuple[Any, list[set[str]]]] = [(tree.root_node, [])] # iterative walk; `owners` is the chain of enclosing function call-sets @@ -132,7 +143,7 @@ def ts_functions(source: bytes, lang: str, audit: Any) -> list[tuple[str, int, s owners = [*owners, calls] elif node.type in CALL_NODE_TYPES and owners: name = _callee(node) - if name: + if name and name not in non_calls: owners[-1].add(name) stack.extend((child, owners) for child in reversed(node.children)) return out From a09aada972ea46cf3ef61e853a1ffdbab0e99806 Mon Sep 17 00:00:00 2001 From: Joe Esquibel Date: Wed, 23 Sep 2026 15:12:30 -0400 Subject: [PATCH 7/7] call_graph_accuracy: regenerate baseline on #3359 + #3360 (#3359) Precision up (python 93.8 -> 96.2, typescript 81.2 -> 82.6, ruby 89.8 -> 91.3, rust 86.7 -> 87.1, javascript, csharp, java, php); recall unchanged. Co-Authored-By: Claude Opus 5.5 (1M context) --- tests/call_graph_accuracy_baseline.json | 28 ++++++++++++------------- 1 file changed, 14 insertions(+), 14 deletions(-) diff --git a/tests/call_graph_accuracy_baseline.json b/tests/call_graph_accuracy_baseline.json index a710708949..918658679e 100644 --- a/tests/call_graph_accuracy_baseline.json +++ b/tests/call_graph_accuracy_baseline.json @@ -19,10 +19,10 @@ }, "csharp": { "fn": 73, - "fp": 171, + "fp": 166, "matched_functions": 650, "parse_failures": 0, - "precision_pct": 93.8, + "precision_pct": 93.9, "recall_pct": 97.2, "tp": 2575 }, @@ -37,7 +37,7 @@ }, "java": { "fn": 60, - "fp": 18, + "fp": 17, "matched_functions": 735, "parse_failures": 0, "precision_pct": 99.1, @@ -46,16 +46,16 @@ }, "javascript": { "fn": 120, - "fp": 559, + "fp": 546, "matched_functions": 619, "parse_failures": 0, - "precision_pct": 78.4, + "precision_pct": 78.8, "recall_pct": 94.4, "tp": 2028 }, "php": { "fn": 95, - "fp": 206, + "fp": 205, "matched_functions": 1548, "parse_failures": 0, "precision_pct": 96.3, @@ -64,37 +64,37 @@ }, "python": { "fn": 1161, - "fp": 666, + "fp": 396, "matched_functions": 4045, "parse_failures": 0, - "precision_pct": 93.8, + "precision_pct": 96.2, "recall_pct": 89.7, "tp": 10069 }, "ruby": { "fn": 660, - "fp": 38, + "fp": 32, "matched_functions": 338, "parse_failures": 0, - "precision_pct": 89.8, + "precision_pct": 91.3, "recall_pct": 33.7, "tp": 336 }, "rust": { "fn": 1042, - "fp": 944, + "fp": 912, "matched_functions": 1774, "parse_failures": 0, - "precision_pct": 86.7, + "precision_pct": 87.1, "recall_pct": 85.5, "tp": 6150 }, "typescript": { "fn": 546, - "fp": 1215, + "fp": 1101, "matched_functions": 2847, "parse_failures": 0, - "precision_pct": 81.2, + "precision_pct": 82.6, "recall_pct": 90.6, "tp": 5241 }