diff --git a/gitgalaxy/standards/language_standards.py b/gitgalaxy/standards/language_standards.py index 3a8f828f..bcf8bd05 100644 --- a/gitgalaxy/standards/language_standards.py +++ b/gitgalaxy/standards/language_standards.py @@ -11961,11 +11961,11 @@ class PrismConfigSchema(TypedDict): # 4. func_start (Executable Logic Anchors) # MUST HAVE EXACTLY ONE CAPTURE GROUP. # Captures standard procs and namespaced procs (e.g., `proc ::my::func`). - "func_start": re.compile(r"^[ \t]*proc[ \t]+([a-zA-Z0-9_:]+)(?=[ \t]*\{|[ \t\n]|$)", re.M), + "func_start": re.compile(r"^[ \t]*proc[ \t\n]+([a-zA-Z0-9_:]+)(?=[ \t]*\{|[ \t\n]|$)", re.M), # 5. class_start (Object / Entity Declarations) # Captures TclOO, Snit, and Itcl class definitions. "class_start": re.compile( - r"^[ \t]*(?:oo::class[ \t]+create|snit::type|itcl::class)[ \t]+([a-zA-Z0-9_:]+)(?=[ \t]*\{|[ \t\n]|$)", + r"^[ \t]*(?:oo::class[ \t\n]+create|snit::type|itcl::class)[ \t\n]+([a-zA-Z0-9_:]+)(?=[ \t]*\{|[ \t\n]|$)", re.M, ), # --- PHASE 2: RISK & STRUCTURAL INTEGRITY --- @@ -12036,6 +12036,11 @@ class PrismConfigSchema(TypedDict): # 24. import (Dependency Inclusions) # Package and module loading. "import": re.compile(r"^[ \t]*(?:package[ \t]+require|source|load)\b", re.M), + "dependency_injection": None, + "_dependency_capture": re.compile( + r"^[ \t]*(?:package[ \t\n]+require|source|load)[ \t\n]+(?:-exact[ \t\n]+)?(?:\{?[\"']?)([^\"'\s#{}]+)", + re.M, + ), # 25. ownership (Authorship Metadata) "ownership": re.compile( r"^[ \t]*#[ \t]*(?:Author|Created by|Maintainer|Copyright):\s+(.*)", @@ -12061,7 +12066,6 @@ class PrismConfigSchema(TypedDict): # 32. events (Event Emitters / Pub-Sub) # Tcl event bindings and file event handlers. "events": re.compile(r"\b(?:bind|fileevent|vwait|trace[ \t]+add)\b"), - "dependency_injection": None, "macros": None, "pointers": None, "memory_alloc": None, diff --git a/tests/extraction/languages/test_tcl.py b/tests/extraction/languages/test_tcl.py new file mode 100644 index 00000000..55423d7a --- /dev/null +++ b/tests/extraction/languages/test_tcl.py @@ -0,0 +1,163 @@ +""" +Tcl extraction hardening (epic #848). See +tests/extraction/how_to_harden_extraction.md for the methodology. + +Covers all four extraction gauntlets for tcl in one file: func_start, +args, class_start, _dependency_capture. +""" + +import sys +from pathlib import Path +from typing import Any + +import pytest + +from gitgalaxy.standards.language_standards import LANGUAGE_DEFINITIONS + +_EXTRACTION_DIR = str(Path(__file__).resolve().parent.parent) +if _EXTRACTION_DIR not in sys.path: + sys.path.insert(0, _EXTRACTION_DIR) + +from _extraction_harness import ( # noqa: E402 + assert_invalid_no_match, + assert_pathological_dependency_match, + assert_pathological_match, + assert_valid_dependency_match, + assert_valid_match, +) + +TCL_RULES = LANGUAGE_DEFINITIONS["tcl"]["rules"] + +FUNCTION_CASES: dict[str, Any] = { + "valid": [ + ("proc TargetFunc {", "TargetFunc"), + ("proc TargetFunc {a b} {", "TargetFunc"), + ("proc ::namespace::TargetFunc {args} {", "TargetFunc"), + ], + "invalid": [ + "set TargetFunc", + "if {$TargetFunc}", + "TargetFunc a b", + "if {$a == $b} {", + "set proc TargetFunc", + 'puts "proc TargetFunc {"', + ], + "pathological": [ + ("proc \t TargetFunc \t {", "TargetFunc"), + ("proc TargetFunc {", "TargetFunc"), + ("proc \n ::namespace::TargetFunc \n { \n a \n b \n } \n {", "TargetFunc"), + ], +} + +ARGS_CASES: dict[str, Any] = { + "valid": [ + ("proc TargetFunc {a b} {", "a b"), + ("proc ::namespace::TargetFunc {args} {", "args"), + ("proc TargetFunc {{a 1} b} {", "{a 1} b"), + ], + "invalid": [ + "TargetFunc a b", + "if {$a == $b} {", + ], + "pathological": [ + ("proc \n ::namespace::TargetFunc \n { \n a \n b \n } \n {", " \n a \n b \n "), + ("proc TargetFunc \n { \n {a 1} \n {b 2} \n } \n {", " \n {a 1} \n {b 2} \n "), + ("proc TargetFunc { a b } {", " a b "), + ], +} + +CLASS_CASES: dict[str, Any] = { + "valid": [ + ("oo::class create TargetClass {", "TargetClass"), + ("snit::type TargetClass {", "TargetClass"), + ("itcl::class TargetClass {", "TargetClass"), + ], + "invalid": ["set TargetClass", "if {$TargetClass}", "TargetClass create foo"], + "pathological": [ + ("oo::class \t create \t TargetClass \t {", "TargetClass"), + ("oo::class \n create \n TargetClass \n {", "TargetClass"), + ("snit::type \n TargetClass \n {", "TargetClass"), + ], +} + +DEPENDENCY_CASES: dict[str, Any] = { + "valid": [ + ("package require TargetPkg", "TargetPkg"), + ("source TargetPkg.tcl", "TargetPkg.tcl"), + ("load TargetPkg.so", "TargetPkg.so"), + ("package require -exact TargetPkg 1.0", "TargetPkg"), + ('source "TargetPkg.tcl"', "TargetPkg.tcl"), + ], + "invalid": [ + "puts TargetPkg", + "set package TargetPkg", + 'puts "package require TargetPkg"', + ], + "pathological": [ + ("package require TargetPkg", "TargetPkg"), + ("source TargetPkg.tcl", "TargetPkg.tcl"), + ("package \t require \t TargetPkg", "TargetPkg"), + ], +} + + +class TestTclExtraction: + # ------------------------------------------------------------------------- + # func_start + # ------------------------------------------------------------------------- + @pytest.mark.parametrize("payload,expected", FUNCTION_CASES["valid"]) + def test_func_start_valid(self, payload, expected): + assert_valid_match(TCL_RULES["func_start"], payload, expected, "tcl") + + @pytest.mark.parametrize("payload", FUNCTION_CASES["invalid"]) + def test_func_start_invalid(self, payload): + assert_invalid_no_match(TCL_RULES["func_start"], payload, "tcl") + + @pytest.mark.parametrize("payload,expected", FUNCTION_CASES["pathological"]) + def test_func_start_pathological(self, payload, expected): + assert_pathological_match(TCL_RULES["func_start"], payload, expected, "tcl") + + # ------------------------------------------------------------------------- + # args + # ------------------------------------------------------------------------- + @pytest.mark.parametrize("payload,expected", ARGS_CASES["valid"]) + def test_args_valid(self, payload, expected): + assert_valid_match(TCL_RULES["args"], payload, expected, "tcl") + + @pytest.mark.parametrize("payload", ARGS_CASES["invalid"]) + def test_args_invalid(self, payload): + assert_invalid_no_match(TCL_RULES["args"], payload, "tcl") + + @pytest.mark.parametrize("payload,expected", ARGS_CASES["pathological"]) + def test_args_pathological(self, payload, expected): + assert_pathological_match(TCL_RULES["args"], payload, expected, "tcl") + + # ------------------------------------------------------------------------- + # class_start + # ------------------------------------------------------------------------- + @pytest.mark.parametrize("payload,expected", CLASS_CASES["valid"]) + def test_class_start_valid(self, payload, expected): + assert_valid_match(TCL_RULES["class_start"], payload, expected, "tcl") + + @pytest.mark.parametrize("payload", CLASS_CASES["invalid"]) + def test_class_start_invalid(self, payload): + assert_invalid_no_match(TCL_RULES["class_start"], payload, "tcl") + + @pytest.mark.parametrize("payload,expected", CLASS_CASES["pathological"]) + def test_class_start_pathological(self, payload, expected): + assert_pathological_match(TCL_RULES["class_start"], payload, expected, "tcl") + + # ------------------------------------------------------------------------- + # _dependency_capture + # ------------------------------------------------------------------------- + @pytest.mark.parametrize("payload,expected", DEPENDENCY_CASES["valid"]) + def test_dependency_valid(self, payload, expected): + assert_valid_dependency_match(TCL_RULES["_dependency_capture"], payload, expected, "tcl") + + @pytest.mark.parametrize("payload", DEPENDENCY_CASES["invalid"]) + def test_dependency_invalid(self, payload): + assert_invalid_no_match(TCL_RULES["_dependency_capture"], payload, "tcl") + + @pytest.mark.parametrize("payload,expected", DEPENDENCY_CASES["pathological"]) + def test_dependency_pathological(self, payload, expected): + assert_pathological_dependency_match(TCL_RULES["_dependency_capture"], payload, expected, "tcl") diff --git a/tests/extraction/test_args_extraction_strict.py b/tests/extraction/test_args_extraction_strict.py index a98d92cc..efe73296 100644 --- a/tests/extraction/test_args_extraction_strict.py +++ b/tests/extraction/test_args_extraction_strict.py @@ -70,17 +70,6 @@ ) ], }, - "tcl": { - "valid": [ - ("proc TargetFunc {a b} {", "TargetFunc"), - ("proc ::namespace::TargetFunc {args} {", "TargetFunc"), - ], - "invalid": ["TargetFunc a b", "if {$a == $b} {"], - "pathological": [ - # Vertical Tcl procs - ("proc \n ::namespace::TargetFunc \n { \n a \n b \n } \n {", "TargetFunc") - ], - }, "scheme": { "valid": [ ("(define (TargetFunc a b)", "TargetFunc"), diff --git a/tests/extraction/test_function_extraction_strict.py b/tests/extraction/test_function_extraction_strict.py index cbcb4ec7..a8cd6a02 100644 --- a/tests/extraction/test_function_extraction_strict.py +++ b/tests/extraction/test_function_extraction_strict.py @@ -179,11 +179,6 @@ "invalid": ["
", ""], "pathological": [("