diff --git a/gitgalaxy/standards/language_standards.py b/gitgalaxy/standards/language_standards.py index 2c438964c..9797367c3 100644 --- a/gitgalaxy/standards/language_standards.py +++ b/gitgalaxy/standards/language_standards.py @@ -9507,7 +9507,7 @@ class PrismConfigSchema(TypedDict): # args: Captures standard function inputs and return signatures `function [out1, out2] = myFun(in1, in2)`. # CRITICAL GUARDRAIL: Safely bounds `\([^)]*\)` and `\[[^\]]*\]`. "args": re.compile( - r"\bfunction[ \t]+(?:\[[^\]]*\][ \t]*=[ \t]*|[a-zA-Z_]\w*[ \t]*=[ \t]*)?[a-zA-Z_]\w*[ \t]*\([^)]*\)|@[ \t]*\([^)]*\)" + r"\bfunction(?:[ \t]|\.\.\.[^\n]*\n)+(?:\[[^\]]*\](?:[ \t]|\.\.\.[^\n]*\n)*=(?:[ \t]|\.\.\.[^\n]*\n)*|[a-zA-Z_]\w*(?:[ \t]|\.\.\.[^\n]*\n)*=(?:[ \t]|\.\.\.[^\n]*\n)*)?[a-zA-Z_]\w*(?:[ \t]|\.\.\.[^\n]*\n)*\([^)]*\)|@(?:[ \t]|\.\.\.[^\n]*\n)*\([^)]*\)" ), # linear: Structural boundaries defining straight-line execution. # CRITICAL GUARDRAIL: Access modifiers (private, protected) explicitly omitted. @@ -9526,13 +9526,13 @@ class PrismConfigSchema(TypedDict): # the optional `(?:\[[^\]]*\]...)?` output array matcher, allowing the # regex to crawl down to the assignment operator `=` and map the name. # ===================================================================== - r"^[ \t]*(?!(?:if|for|while|switch|catch|classdef)\b)function[ \t\n]+(?:\[[^\]]*\][ \t\n]*=[ \t\n]*|[a-zA-Z_]\w*[ \t\n]*=[ \t\n]*)?([a-zA-Z_]\w*)(?=[ \t\n]*\(|$)", + r"^[ \t]*(?!(?:if|for|while|switch|catch|classdef)\b)function(?:[ \t\n]|\.\.\.[^\n]*\n)+(?:\[[^\]]*\](?:[ \t\n]|\.\.\.[^\n]*\n)*=(?:[ \t\n]|\.\.\.[^\n]*\n)*|[a-zA-Z_]\w*(?:[ \t\n]|\.\.\.[^\n]*\n)*=(?:[ \t\n]|\.\.\.[^\n]*\n)*)?([a-zA-Z_]\w*)(?=(?:[ \t\n]|\.\.\.[^\n]*\n)*\(|$)", re.M, ), # class_start: Defines an object-oriented boundary. # Safely steps over optional class attributes like `classdef (ConstructOnLoad) MyClass` "class_start": re.compile( - r"^[ \t]*classdef(?:[ \t]*\([^)]*\))?[ \t]+([a-zA-Z_]\w*)(?=[ \t\n]|$)", + r"^[ \t]*classdef(?:(?:[ \t]|\.\.\.[^\n]*\n)*\([^)]*\))?(?:[ \t]|\.\.\.[^\n]*\n)+([a-zA-Z_]\w*)(?=(?:[ \t]|\.\.\.[^\n]*\n)|<|$)", re.M, ), # --- PHASE 2: RISK & STRUCTURAL INTEGRITY --- @@ -9630,7 +9630,7 @@ class PrismConfigSchema(TypedDict): ), # import: Namespace/Class loading. "import": re.compile(r"^[ \t]*import[ \t]+[a-zA-Z0-9_.*]+", re.M), - "_dependency_capture": re.compile(r"^[ \t]*import[ \t\n]+([a-zA-Z0-9_.*]+)", re.M), + "_dependency_capture": re.compile(r"^[ \t]*import(?:[ \t]|\.\.\.[^\n]*\n)+([a-zA-Z0-9_.*]+)", re.M), # ownership: Standard MATLAB comment authorship signatures. "ownership": re.compile(r"^[ \t]*%[ \t]*(?:Author|Created by|Copyright)[ \t]*:(.*)", re.M | re.I), # --- PHASE 4: SPECIALIZED SUB-SYSTEMS --- diff --git a/tests/extraction/languages/test_matlab.py b/tests/extraction/languages/test_matlab.py new file mode 100644 index 000000000..609cdaa4b --- /dev/null +++ b/tests/extraction/languages/test_matlab.py @@ -0,0 +1,227 @@ +""" +MATLAB extraction hardening (epic #852). See +tests/extraction/how_to_harden_extraction.md for the methodology. +""" + +import sys +from pathlib import Path + +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 typing import Any # noqa: E402 + +from _extraction_harness import ( # noqa: E402 # type: ignore + assert_invalid_no_match, + assert_pathological_dependency_match, + assert_pathological_match, + assert_valid_dependency_match, + assert_valid_match, +) + +MATLAB_RULES = LANGUAGE_DEFINITIONS["matlab"]["rules"] + +# ============================================================================== +# FUNC_START (func_start) +# ============================================================================== +FUNCTION_CASES: dict[str, Any] = { + "valid": [ + # Modern idiom (carried forward) + ("function [out] = TargetFunc(in)", "TargetFunc"), + ("function TargetFunc()", "TargetFunc"), + # Other idioms + ("function TargetFunc", "TargetFunc"), + ("function [out1, out2] = TargetFunc(in1, in2)", "TargetFunc"), + ("function out = TargetFunc(in)", "TargetFunc"), + ("function [out] = TargetFunc ( in )", "TargetFunc"), # Spacing + # Line continuation + ("function [out] = ...\n TargetFunc(in)", "TargetFunc"), + ("function ...\n TargetFunc(in)", "TargetFunc"), + # Block comments lookalike + ], + "invalid": [ + # Carried forward + "if TargetFunc()", + "classdef TargetFunc", + "TargetFunc = 5", + # Ghost Prevention + "disp('function TargetFunc()')", + "% function TargetFunc()", + # KNOWN LIMITATION (Class 3): Block comments and string literals are unshielded in Mode D + # "%{ \n function TargetFunc() \n %}", + # "x = \"function TargetFunc()\"", + ], + "pathological": [ + # Splitting output arrays across newlines (carried forward) + ( + "function ...\n [ \n out1 \n , \n out2 \n ] ...\n = ...\n TargetFunc ...\n (", + "TargetFunc", + ), + ( + "function...\nout...\n=...\nTargetFunc...\n(", + "TargetFunc", + ), + ( + "function ...\n TargetFunc ...\n (", + "TargetFunc", + ), + ( + "function ...\n [ \n out1 \n , \n out2 \n ] ...\n = ...\n TargetFunc", + "TargetFunc", + ), + ], +} + + +@pytest.mark.parametrize("payload,expected_name", FUNCTION_CASES["valid"]) +def test_matlab_func_start_valid(payload, expected_name): + assert_valid_match(MATLAB_RULES["func_start"], payload, expected_name, "matlab.func_start") + + +@pytest.mark.parametrize("payload", FUNCTION_CASES["invalid"]) +def test_matlab_func_start_invalid(payload): + assert_invalid_no_match(MATLAB_RULES["func_start"], payload, "matlab.func_start") + + +@pytest.mark.parametrize("payload,expected_name", FUNCTION_CASES["pathological"]) +def test_matlab_func_start_pathological(payload, expected_name): + assert_pathological_match(MATLAB_RULES["func_start"], payload, expected_name, "matlab.func_start") + + +# ============================================================================== +# ARGS (args) +# ============================================================================== +ARGS_CASES: dict[str, Any] = { + "valid": [ + ("function [out] = TargetFunc(in1, in2)", None), + ("function TargetFunc(in1)", None), + ("function TargetFunc()", None), + ("@(x, y)", None), # anonymous function + ("function [out] = ...\n TargetFunc(in1, in2)", None), + ], + "invalid": [ + "if TargetFunc(in1, in2)", + "disp(in1, in2)", + ], + "pathological": [ + ( + "function ...\n [ \n out1 \n , \n out2 \n ] ...\n = ...\n TargetFunc ...\n (\n in1 \n , \n in2 \n )", + None, + ), + ( + "function...\nout...\n=...\nTargetFunc...\n(\n in1 \n )", + None, + ), + ( + "@...\n(\nx\n,\ny\n)", + None, + ), + ], +} + + +@pytest.mark.parametrize("payload,expected_name", ARGS_CASES["valid"]) +def test_matlab_args_valid(payload, expected_name): + assert_valid_match(MATLAB_RULES["args"], payload, expected_name, "matlab.args") + + +@pytest.mark.parametrize("payload", ARGS_CASES["invalid"]) +def test_matlab_args_invalid(payload): + assert_invalid_no_match(MATLAB_RULES["args"], payload, "matlab.args") + + +@pytest.mark.parametrize("payload,expected_name", ARGS_CASES["pathological"]) +def test_matlab_args_pathological(payload, expected_name): + assert_pathological_match(MATLAB_RULES["args"], payload, expected_name, "matlab.args") + + +# ============================================================================== +# CLASS_START (class_start) +# ============================================================================== +CLASS_CASES: dict[str, Any] = { + "valid": [ + ("classdef TargetClass", "TargetClass"), + ("classdef (ConstructOnLoad) TargetClass", "TargetClass"), + ("classdef TargetClass < handle", "TargetClass"), + ("classdef (Sealed = true, Hidden = false) TargetClass < handle & matlab.mixin.Copyable", "TargetClass"), + ("classdef TargetClass