Skip to content
Merged
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
24 changes: 22 additions & 2 deletions gitgalaxy/standards/language_standards/_shared_patterns.py
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,26 @@
r"للقيام به|لاحقا|يجب عمله" # Arabic
r")"
)
GLOBAL_PLANNED_DEBT = re.compile(f"{_SPACED_PLANNED}|{_DENSE_PLANNED}", re.I)
# #2537: `-` is a regex word boundary, so the bare `\b(...)\b` alternations
# matched debt keywords EMBEDDED INSIDE hyphenated code identifiers -- COBOL
# data items (`HACK-LEVEL`, `BUG-COUNT`), COBOL/Lisp-family paragraph and
# symbol names (`PROBE-TODO`, `probe-todo`), css classes (`.bug-icon`) --
# inflating tech-debt scoring from ordinary code in exactly the hyphenated-
# identifier ecosystems (COBOL/JCL, Lisp, css) where debt measurement matters
# most. Python's `HACK_LEVEL` was inert only by tokenization luck (`_` is a
# word char, so `\bHACK\b` can't fire mid-identifier).
# THE GUARD: refuse a match glued to a hyphen-plus-alphanumeric on either
# side -- the shape of an identifier CONTINUING through the hyphen. Real
# comment markers keep counting, including hyphen-adjacent ones whose
# neighbor char is NOT alphanumeric: `-- TODO x` (Ada/Haskell/SQL comments),
# a glued `--TODO`, or a trailing `TODO--` (the char beside the hyphen is
# another `-`, not a letter/digit). Deliberately scoped to the SPACED
# (Latin/Cyrillic) alternation only: the DENSE CJK/RTL alternation has no
# hyphenated-identifier idiom to guard against.
_HYPHEN_IDENT_PRE = r"(?<![A-Za-z0-9]-)"
_HYPHEN_IDENT_POST = r"(?!-[A-Za-z0-9])"

GLOBAL_PLANNED_DEBT = re.compile(f"{_HYPHEN_IDENT_PRE}{_SPACED_PLANNED}{_HYPHEN_IDENT_POST}|{_DENSE_PLANNED}", re.I)


# --- 2. FRAGILE DEBT (Hacks, FIXMEs, Code Smells) ---
Expand All @@ -70,7 +89,8 @@
r"مؤقت|إصلاح|ترقيع" # Arabic (Tarqie = Patching/Hacking)
r")"
)
GLOBAL_FRAGILE_DEBT = re.compile(f"{_SPACED_FRAGILE}|{_DENSE_FRAGILE}", re.I)
# #2537: same hyphenated-identifier guard as GLOBAL_PLANNED_DEBT above.
GLOBAL_FRAGILE_DEBT = re.compile(f"{_HYPHEN_IDENT_PRE}{_SPACED_FRAGILE}{_HYPHEN_IDENT_POST}|{_DENSE_FRAGILE}", re.I)


# --- 3. AI / LLM & ML SDK DETECTION (split by SIGNAL_SCHEMA category) ---
Expand Down
60 changes: 60 additions & 0 deletions tests/core_engine/test_language_standards_strict.py
Original file line number Diff line number Diff line change
Expand Up @@ -205,3 +205,63 @@ def test_spec_exposure_adjacent_quantifier_redos_sweep(language, old_pattern_tex
assert spec_exposure.search(extra_positive), (
f"{language}: lost its own extra alternative ({extra_positive!r}) while bounding the fix"
)


# ==============================================================================
# TEST 9: HYPHENATED-IDENTIFIER DEBT LEAK (#2537)
# The two GLOBAL debt patterns are shared by every language block, so this
# cross-language regression lives here rather than in any one per-language
# strict file (cobol/scheme carry their own end-to-end-shaped repros too).
# ==============================================================================
def test_global_debt_patterns_ignore_hyphenated_identifiers():
"""#2537: `-` is a regex word boundary, so the unguarded `\\b(...)\\b`
alternations matched debt keywords EMBEDDED INSIDE hyphenated code
identifiers (COBOL `HACK-LEVEL`, Lisp `probe-todo`, css `.bug-icon`),
inflating tech-debt scoring from ordinary code. The guard must refuse a
match glued to hyphen-plus-alphanumeric on either side while keeping
every real comment-marker shape -- including hyphen-ADJACENT ones whose
neighbor char is not alphanumeric (`-- TODO`, `--TODO`, `TODO--`)."""
from gitgalaxy.standards.language_standards._shared_patterns import (
GLOBAL_FRAGILE_DEBT,
GLOBAL_PLANNED_DEBT,
)

# Identifier-embedded shapes: must NOT count.
for pattern, text in (
(GLOBAL_FRAGILE_DEBT, "77 HACK-LEVEL PIC 9."), # COBOL data item
(GLOBAL_FRAGILE_DEBT, "DISPLAY HACK-LEVEL."), # ...and its reference
(GLOBAL_FRAGILE_DEBT, "MOVE 0 TO WS-BUG-COUNT."), # both-sides glued
(GLOBAL_FRAGILE_DEBT, ".bug-icon { color: red; }"), # css class
(GLOBAL_PLANNED_DEBT, " PROBE-TODO."), # COBOL paragraph name
(GLOBAL_PLANNED_DEBT, "(define (probe-todo plan)"), # Lisp-family symbol
(GLOBAL_PLANNED_DEBT, "see the todo-list section"), # kebab prose
):
assert not pattern.search(text), f"identifier-embedded debt keyword counted: {text!r}"

# Real debt markers: must still count, exactly once each.
for pattern, text in (
(GLOBAL_FRAGILE_DEBT, " * HACK: shortcut kept deliberately"), # COBOL comment
(GLOBAL_FRAGILE_DEBT, ";; HACK: shortcut"), # Lisp comment
(GLOBAL_FRAGILE_DEBT, "# FIXME handle overflow"),
(GLOBAL_PLANNED_DEBT, " * TODO: fill in the probe body later"),
(GLOBAL_PLANNED_DEBT, "-- TODO wire this up"), # Ada/Haskell/SQL comment
(GLOBAL_PLANNED_DEBT, "--TODO glued to the comment marker"),
(GLOBAL_PLANNED_DEBT, "TODO-- reversed gluing"),
(GLOBAL_PLANNED_DEBT, "@todo document this"),
(GLOBAL_PLANNED_DEBT, "待办事项"), # DENSE (CJK) path unguarded
):
assert len(pattern.findall(text)) == 1, f"real debt marker lost or duplicated: {text!r}"

# The guards add fixed-width lookarounds only (no new quantifiers) -- prove
# both patterns stay immune to a hyphen-heavy adversarial payload anyway.
from pathlib import Path as _P
import sys as _sys

_langs_dir = str(_P(__file__).resolve().parent.parent / "extraction" / "languages")
if _langs_dir not in _sys.path:
_sys.path.insert(0, _langs_dir)
from _strict_harness import assert_redos_immune as _immune # type: ignore

payload = "TOD-" * 25000 + "TODO-"
_immune(GLOBAL_PLANNED_DEBT, payload, timeout_sec=3.0)
_immune(GLOBAL_FRAGILE_DEBT, "HAC-" * 25000 + "HACK-", timeout_sec=3.0)
35 changes: 27 additions & 8 deletions tests/extraction/languages/test_cobol_strict.py
Original file line number Diff line number Diff line change
Expand Up @@ -110,36 +110,29 @@ def test_cobol_ghost_satellite_prevention():
("regex_execution", "INSPECT X TALLYING Y.", "MOVE X TO Y."),
("time_date_logic", "ACCEPT WS-DATE FROM DATE.", "MOVE X TO Y."),
("ipc_rpc_bridges", "CALL 'SUBPROG' USING X.", "MOVE X TO Y."),

# --- DEEP ADVERSARIAL CASES ---
# args: multiline parsing and commas
("args", "USING \n BY REFERENCE WS-A \n BY CONTENT WS-B", "MOVE X TO Y."),
("args", "RETURNING \n WS-STATUS", "MOVE X TO Y."),
("args", "USING WS-A, WS-B, WS-C", "MOVE X TO Y."),
("args", "USING \n WS-A \n WS-B \n WS-C", "MOVE X TO Y."),
("args", "USING BY VALUE WS-A", "MOVE X TO Y."),

# branch: edge cases, multi-word, line-wrapped
("branch", "DEPENDING \n ON", "MOVE X TO Y."),
("branch", "ON \n SIZE ERROR", "MOVE X TO Y."),
("branch", "INVALID \n KEY", "MOVE X TO Y."),
("branch", "AT \n END", "MOVE X TO Y."),
("branch", "NOT AT END", None),

# func_start: reserved words shouldn't match, edge margins
("func_start", " MY-FUNC SECTION 12.", " DIVISION."),
("func_start", " 1234-VALID-PARA.", " SECTION."),
("func_start", " SOME-PARA.", " PROCEDURE DIVISION."),
("func_start", "000100 VALID-FUNC-WITH-MARGIN.", "000100 WORKING-STORAGE SECTION."),
("func_start", " -VALID-WITH-DASH.", " *COMMENT-SHOULD-NOT-MATCH."),

# class_start: trailing clauses, optional names
("class_start", " PROGRAM-ID. MYPROG IS INITIAL.", " 100-PROCESS-RECORDS SECTION."),
("class_start", " CLASS-ID. FOO INHERITS BASE.", " 01 WS-DATA."),


("class_start", "000100 PROGRAM-ID. P1.", " PROCEDURE DIVISION."),

# structural_boundaries:
("structural_boundaries", "PROCEDURE DIVISION", "MOVE X TO Y."),
("structural_boundaries", "XML PARSE", "MOVE X TO Y."),
Expand All @@ -149,7 +142,6 @@ def test_cobol_ghost_satellite_prevention():
]



@pytest.mark.parametrize("signature,positive,negative", _COBOL_SIMPLE_CASES)
def test_cobol_signature_positive_and_negative(signature, positive, negative):
pattern = COBOL_RULES[signature]
Expand Down Expand Up @@ -412,3 +404,30 @@ def test_cobol_redos_immunity_sweep():
assert COBOL_RULES["func_start"].search(" 100-PROCESS-RECORDS SECTION.")
assert COBOL_RULES["class_start"].search(" PROGRAM-ID. MYPROG.")
assert COBOL_RULES["time_date_logic"].search("ACCEPT WS-DATE FROM DATE.")


def test_cobol_debt_rules_ignore_hyphenated_identifiers_regression():
"""#2537: cobol's debt rules (the shared GLOBAL patterns) counted debt
keywords embedded inside hyphenated identifiers -- the #1096 control
corpus's `77 HACK-LEVEL PIC 9.` data item recorded fragile_debt from
plain code and the `PROBE-TODO.` paragraph name recorded planned_debt.
Real `* HACK:` / `* TODO:` comment markers must keep counting."""
fragile = COBOL_RULES["fragile_debt"]
planned = COBOL_RULES["planned_debt"]

corpus_shaped = (
" * Keyword Rosetta control shell: cobol / c\n"
" 77 HACK-LEVEL PIC 9.\n"
" PROBE-DEBT.\n"
" * HACK: shortcut kept deliberately for the rosetta corpus\n"
" DISPLAY HACK-LEVEL.\n"
" PROBE-TODO.\n"
" * TODO: fill in the probe body later\n"
" DISPLAY 'PLANNED'.\n"
)
assert len(fragile.findall(corpus_shaped)) == 1, "fragile_debt must count ONLY the * HACK: comment"
assert len(planned.findall(corpus_shaped)) == 1, "planned_debt must count ONLY the * TODO: comment"

# Realistic mainframe identifier shapes from #2537's report.
for text in ("MOVE 1 TO BUG-COUNT.", "05 WS-FIX-FLAG PIC X.", "ADD 1 TO HACK-TOTAL."):
assert not fragile.search(text), f"fragile_debt matched inside identifier: {text!r}"
25 changes: 20 additions & 5 deletions tests/extraction/languages/test_scheme_strict.py
Original file line number Diff line number Diff line change
Expand Up @@ -324,34 +324,31 @@ def test_scheme_redos_immunity_sweep():
assert SCHEME_RULES["class_start"].search("(define-record-type <point> (make-point x y) point?)")
assert SCHEME_RULES["globals"].search("(define default->value 5)")


_SCHEME_DEEP_CASES = [
# branch
("branch", "(\n if a b c)", "xif"),
("branch", "if (a)", "if-var"),
("branch", "(cond\n (else 1))", "conditional"),
("branch", "(when (and a b))", "awhen"),
("branch", "unless", "runless"),

# args
("args", "(define (foo \n x \n y)\n ...)", "(define foo 5)"),
("args", "(define (foo))", "(define (foo"),
("args", "(define (foo . rest) ...)", "(define foo (lambda (x) x))"),
("args", "(define (foo!x y))", "(define foo!x)"),
("args", "(define (a-b-c d e))", "(+ 1 2)"),

# func_start
("func_start", "(define (call/cc-wrapper x) ...)", "(define foo 5)"),
("func_start", "(\n define (foo x))", "(define-syntax foo)"),
("func_start", "(define (* a b) ...)", None),
("func_start", "(define (1+ x) x)", "(define)"),
("func_start", "(define (foo))", "define (foo)"), # space instead of (

("func_start", "(define (foo))", "define (foo)"), # space instead of (
# class_start
("class_start", "(define-record-type point)", "(define-record-type)"),
("class_start", "(\n define-record-type <point>)", "(define (define-record-type x))"),
("class_start", "(define-record-type (point x y))", "(+ 1 2)"),
("class_start", "(define-record-type point\n (make-point))", "define-record-type x"),

# structural_boundaries
("structural_boundaries", "(let ((x 1)) x)", "let-syntax"),
("structural_boundaries", "(let* ((x 1)) x)", "foo-let"),
Expand All @@ -360,10 +357,28 @@ def test_scheme_redos_immunity_sweep():
("structural_boundaries", "(do ((i 0 (+ i 1))) ((= i 5)) i)", "redo"),
]


@pytest.mark.parametrize("signature,positive,negative", _SCHEME_DEEP_CASES)
def test_scheme_deep_cases(signature, positive, negative):
pattern = SCHEME_RULES[signature]
assert pattern is not None
assert pattern.search(positive), f"Deep positive failed for {signature}: {positive!r}"
if negative:
assert not pattern.search(negative), f"Deep negative failed for {signature}: {negative!r}"


def test_scheme_debt_rules_ignore_hyphenated_symbols_regression():
"""#2537: scheme reproduces the hyphenated-identifier debt leak -- a
`(define (probe-todo ...))` symbol recorded planned_debt alongside the
real `;; TODO:` comment (the #1096 control corpus measured planned_debt
2 for one planted marker). Kebab-case is THE Lisp-family naming
convention, so ordinary symbols must never feed debt scoring."""
planned = SCHEME_RULES["planned_debt"]
fragile = SCHEME_RULES["fragile_debt"]

corpus_shaped = "(define (probe-todo plan)\n ;; TODO: fill in the probe body later\n 'planned)\n"
assert len(planned.findall(corpus_shaped)) == 1, "planned_debt must count ONLY the ;; TODO: comment"

for text in ("(fix-me-later x)", "(define bug-tracker '())", "(hack-level 9)"):
assert not fragile.search(text), f"fragile_debt matched inside symbol: {text!r}"
assert not planned.search(text), f"planned_debt matched inside symbol: {text!r}"
Loading
Loading