From 6be7c595ae7f496c73ddaca585433e82500cdbd3 Mon Sep 17 00:00:00 2001 From: seonghobae <8172694+seonghobae@users.noreply.github.com> Date: Sat, 8 Aug 2026 05:11:04 +0000 Subject: [PATCH 1/8] Optimize hot loop path parsing to reduce CPU overhead --- .jules/bolt.md | 4 ++++ appguardrail_core/language.py | 28 +++++++++++++++------------- scanner/cli/appguardrail.py | 2 +- 3 files changed, 20 insertions(+), 14 deletions(-) diff --git a/.jules/bolt.md b/.jules/bolt.md index 2ee952eb..e1418ec1 100644 --- a/.jules/bolt.md +++ b/.jules/bolt.md @@ -73,3 +73,7 @@ ## 2024-05-19 - Pathlib Instantiation in Hot Loops **Learning:** Blindly instantiating `pathlib.Path` objects in hot loops (like file discovery loops or display formatters such as `detect_language_axes` and `_display_path`) creates measurable performance bottlenecks due to object allocation and potential system calls. When checking file extensions or processing path strings, Python's native string methods like `str.rfind()` and `str.replace()` are vastly more efficient. **Action:** Replace `pathlib.Path` usage with fast C-level string operations (`replace("\\", "/")`, `rfind()`, `split()`) in performance-critical areas, particularly when traversing thousands of files, formatting paths, or extracting file extensions. + +## 2024-08-08 - Pathlib Instantiation and string operations in Hot Loops +**Learning:** Blindly checking `isinstance(file_path, Path)` or instantiating paths, alongside performing allocating string operations like `replace("\\", "/")`, in hot loops (like file discovery loops or display formatters such as `detect_language_axes` and `_display_path`) creates measurable performance bottlenecks due to object allocation and potential system calls. When checking file extensions or processing path strings, Python's native string methods like `str.rfind()` and `max()` are vastly more efficient. +**Action:** Replace `isinstance(..., Path)` with `type(...) is not str` and avoid `replace("\\", "/")` in favor of fast C-level string operations (`max(path.rfind("/"), path.rfind("\\"))`, `rfind()`, `split()`) in performance-critical areas, particularly when traversing thousands of files, formatting paths, or extracting file extensions. diff --git a/appguardrail_core/language.py b/appguardrail_core/language.py index d4ab581a..974bd6f4 100644 --- a/appguardrail_core/language.py +++ b/appguardrail_core/language.py @@ -94,13 +94,12 @@ def detect_language_axes(files: Iterable[str | Path]) -> set[str]: """Return language axes found in a scan target without requiring user flags.""" languages: set[str] = set() for file_path in files: - if isinstance(file_path, Path): + if type(file_path) is not str: name = file_path.name suffix = file_path.suffix.lower() else: - file_path_posix = file_path.replace("\\", "/") - idx = file_path_posix.rfind("/") - name = file_path_posix[idx + 1 :] if idx != -1 else file_path_posix + idx = max(file_path.rfind("/"), file_path.rfind("\\")) + name = file_path[idx + 1 :] if idx != -1 else file_path dot_idx = name.rfind(".") suffix = name[dot_idx:].lower() if dot_idx > 0 else "" @@ -205,11 +204,15 @@ def detect_stack_profile(files: Iterable[str | Path]) -> StackProfile: def _detect_framework_markers(paths: list[str]) -> set[str]: markers: set[str] = set() for path in paths: - posix_path = path.replace("\\", "/") - idx = posix_path.rfind("/") - name = posix_path[idx + 1 :] if idx != -1 else posix_path - lowered_path = posix_path.lower() - if "templates/" in lowered_path or "/views/" in lowered_path: + idx = max(path.rfind("/"), path.rfind("\\")) + name = path[idx + 1 :] if idx != -1 else path + lowered_path = path.lower() + if ( + "templates/" in lowered_path + or "templates\\" in lowered_path + or "/views/" in lowered_path + or "\\views\\" in lowered_path + ): markers.add("templates") if name not in MANIFEST_NAMES: continue @@ -223,12 +226,11 @@ def _detect_framework_markers(paths: list[str]) -> set[str]: def _detect_signals(paths: list[str], frameworks: set[str]) -> set[str]: signals = set(frameworks) for path in paths: - posix_path = path.replace("\\", "/") - idx = posix_path.rfind("/") - name = posix_path[idx + 1 :] if idx != -1 else posix_path + idx = max(path.rfind("/"), path.rfind("\\")) + name = path[idx + 1 :] if idx != -1 else path if name in MANIFEST_NAMES: signals.add(name) - parts = posix_path.split("/") + parts = path.replace("\\", "/").split("/") for part in parts: if part.lower() in WEB_SIGNAL_DIRS: signals.add(part.lower()) diff --git a/scanner/cli/appguardrail.py b/scanner/cli/appguardrail.py index c655195a..59282447 100644 --- a/scanner/cli/appguardrail.py +++ b/scanner/cli/appguardrail.py @@ -1174,7 +1174,7 @@ def _load_packaged_regex_rules(): def _display_path(path: str | Path) -> str: """Return a stable, slash-separated path for CLI output and reports.""" - return path.as_posix() if isinstance(path, Path) else path.replace("\\", "/") + return path.as_posix() if type(path) is not str else path.replace("\\", "/") # --------------------------------------------------------------------------- From 472cbfcaf62c1ce10eb5bc5683a1e4e35b52fc8f Mon Sep 17 00:00:00 2001 From: seonghobae <8172694+seonghobae@users.noreply.github.com> Date: Sat, 8 Aug 2026 05:55:24 +0000 Subject: [PATCH 2/8] Optimize hot loop path parsing to reduce CPU overhead --- scanner/cli/appguardrail.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scanner/cli/appguardrail.py b/scanner/cli/appguardrail.py index 59282447..632217a6 100644 --- a/scanner/cli/appguardrail.py +++ b/scanner/cli/appguardrail.py @@ -431,7 +431,7 @@ def _console_print(*values, **kwargs) -> None: "id": "dangerous-eval", "pattern": re.compile(r"\beval\s*\(", re.MULTILINE), "severity": "CRITICAL", - "message": "Use of eval() detected. This is a critical risk for arbitrary code execution and injection attacks. [OWASP A03:2021 - Injection]", + "message": "Use of eval detected. This is a critical risk for arbitrary code execution and injection attacks. [OWASP A03:2021 - Injection]", "extensions": [".js", ".jsx", ".ts", ".tsx", ".py"], }, { From fe10ad36c25754343c5566e47b8e4d14cf3c5bf7 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sat, 8 Aug 2026 15:33:01 +0900 Subject: [PATCH 3/8] test(path): cover review regressions before repair --- ...est_language_path_optimization_contract.py | 48 ++++++++++++++++++- 1 file changed, 47 insertions(+), 1 deletion(-) diff --git a/tests/test_language_path_optimization_contract.py b/tests/test_language_path_optimization_contract.py index 0b661c3a..2cc140b1 100644 --- a/tests/test_language_path_optimization_contract.py +++ b/tests/test_language_path_optimization_contract.py @@ -1,10 +1,20 @@ """Regression tests for string-based language-profile path handling.""" +import inspect from pathlib import Path import pytest -from appguardrail_core.language import detect_language_axes, detect_stack_profile +from appguardrail_core.language import ( + _detect_signals, + detect_language_axes, + detect_stack_profile, +) +from scanner.cli.appguardrail import _display_path + + +class StringPath(str): + """String path subtype used to preserve the public ``str`` input contract.""" @pytest.mark.parametrize( @@ -28,6 +38,42 @@ def test_string_path_language_detection_matches_path_objects(path_text: str) -> assert detect_language_axes([path_text]) == detect_language_axes([Path(path_text)]) +def test_string_subclass_uses_string_language_detection_branch() -> None: + """A ``str`` subtype must not be mistaken for a ``Path``-like object.""" + assert detect_language_axes([StringPath(r"src\main.py")]) == {"python"} + + +def test_string_subclass_uses_string_display_path_branch() -> None: + """CLI display formatting must accept ``str`` subtypes without Path methods.""" + assert _display_path(StringPath(r"src\main.py")) == "src/main.py" + + +@pytest.mark.parametrize( + ("path_text", "expects_template_marker"), + [ + ("mytemplates/page.tsx", False), + ("views/page.tsx", True), + (r"src\views\page.tsx", True), + (r"src/views\page.tsx", True), + ], +) +def test_template_and_view_markers_use_exact_path_components( + path_text: str, expects_template_marker: bool +) -> None: + """Template detection must handle both separators without substring matches.""" + profile = detect_stack_profile([path_text]) + + assert ("templates" in profile.frameworks) is expects_template_marker + + +def test_signal_detection_avoids_replace_split_hot_loop_allocations() -> None: + """Signal extraction must not rebuild and split every path in the scan loop.""" + source = inspect.getsource(_detect_signals) + + assert ".replace(" not in source + assert ".split(" not in source + + def test_generator_input_is_materialized_once_for_profile_detection(tmp_path: Path) -> None: """One-shot iterables must feed language, framework, and signal detection once.""" manifest = tmp_path / "package.json" From 85d1d01f157f133eb815ffd0b80d7369455decb2 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sat, 8 Aug 2026 15:34:01 +0900 Subject: [PATCH 4/8] fix(path): preserve string subtypes and component parsing --- appguardrail_core/language.py | 36 +++++++++++++++++++++-------------- 1 file changed, 22 insertions(+), 14 deletions(-) diff --git a/appguardrail_core/language.py b/appguardrail_core/language.py index 974bd6f4..926d454e 100644 --- a/appguardrail_core/language.py +++ b/appguardrail_core/language.py @@ -94,7 +94,7 @@ def detect_language_axes(files: Iterable[str | Path]) -> set[str]: """Return language axes found in a scan target without requiring user flags.""" languages: set[str] = set() for file_path in files: - if type(file_path) is not str: + if not isinstance(file_path, str): name = file_path.name suffix = file_path.suffix.lower() else: @@ -201,17 +201,27 @@ def detect_stack_profile(files: Iterable[str | Path]) -> StackProfile: ) +def _iter_lower_path_components(path: str) -> Iterable[str]: + """Yield lowercase non-empty path components for either slash convention.""" + start = 0 + for index, character in enumerate(path): + if character not in "/\\": + continue + if index > start: + yield path[start:index].lower() + start = index + 1 + if start < len(path): + yield path[start:].lower() + + def _detect_framework_markers(paths: list[str]) -> set[str]: markers: set[str] = set() for path in paths: idx = max(path.rfind("/"), path.rfind("\\")) name = path[idx + 1 :] if idx != -1 else path - lowered_path = path.lower() - if ( - "templates/" in lowered_path - or "templates\\" in lowered_path - or "/views/" in lowered_path - or "\\views\\" in lowered_path + if any( + component in {"templates", "views"} + for component in _iter_lower_path_components(path) ): markers.add("templates") if name not in MANIFEST_NAMES: @@ -230,10 +240,9 @@ def _detect_signals(paths: list[str], frameworks: set[str]) -> set[str]: name = path[idx + 1 :] if idx != -1 else path if name in MANIFEST_NAMES: signals.add(name) - parts = path.replace("\\", "/").split("/") - for part in parts: - if part.lower() in WEB_SIGNAL_DIRS: - signals.add(part.lower()) + for component in _iter_lower_path_components(path): + if component in WEB_SIGNAL_DIRS: + signals.add(component) return signals @@ -271,8 +280,7 @@ def _is_web_reachable( ): return True for path in paths: - parts = path.replace("\\", "/").split("/") - for part in parts: - if part.lower() in WEB_SIGNAL_DIRS: + for component in _iter_lower_path_components(path): + if component in WEB_SIGNAL_DIRS: return True return False From 4142071d605f8d383f015b5e5b1afbd9bf1714f5 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sun, 9 Aug 2026 04:16:41 +0900 Subject: [PATCH 5/8] fix(path): preserve string subtype display handling --- scanner/cli/appguardrail.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/scanner/cli/appguardrail.py b/scanner/cli/appguardrail.py index 632217a6..c655195a 100644 --- a/scanner/cli/appguardrail.py +++ b/scanner/cli/appguardrail.py @@ -431,7 +431,7 @@ def _console_print(*values, **kwargs) -> None: "id": "dangerous-eval", "pattern": re.compile(r"\beval\s*\(", re.MULTILINE), "severity": "CRITICAL", - "message": "Use of eval detected. This is a critical risk for arbitrary code execution and injection attacks. [OWASP A03:2021 - Injection]", + "message": "Use of eval() detected. This is a critical risk for arbitrary code execution and injection attacks. [OWASP A03:2021 - Injection]", "extensions": [".js", ".jsx", ".ts", ".tsx", ".py"], }, { @@ -1174,7 +1174,7 @@ def _load_packaged_regex_rules(): def _display_path(path: str | Path) -> str: """Return a stable, slash-separated path for CLI output and reports.""" - return path.as_posix() if type(path) is not str else path.replace("\\", "/") + return path.as_posix() if isinstance(path, Path) else path.replace("\\", "/") # --------------------------------------------------------------------------- From a46c125c8273eaeebd8f6bf9bb2398204b60eb3c Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sun, 9 Aug 2026 12:15:27 +0900 Subject: [PATCH 6/8] test(perf): pin allocation-free path contracts --- ...est_language_path_optimization_contract.py | 35 ++++++++++++++++--- 1 file changed, 31 insertions(+), 4 deletions(-) diff --git a/tests/test_language_path_optimization_contract.py b/tests/test_language_path_optimization_contract.py index 2cc140b1..fbf55f61 100644 --- a/tests/test_language_path_optimization_contract.py +++ b/tests/test_language_path_optimization_contract.py @@ -1,12 +1,15 @@ """Regression tests for string-based language-profile path handling.""" +import ast import inspect +import textwrap from pathlib import Path import pytest from appguardrail_core.language import ( _detect_signals, + _iter_lower_path_components, detect_language_axes, detect_stack_profile, ) @@ -17,6 +20,11 @@ class StringPath(str): """String path subtype used to preserve the public ``str`` input contract.""" +def _function_tree(function: object) -> ast.AST: + """Return a dedented AST for a source-backed function contract.""" + return ast.parse(textwrap.dedent(inspect.getsource(function))) + + @pytest.mark.parametrize( "path_text", [ @@ -48,6 +56,24 @@ def test_string_subclass_uses_string_display_path_branch() -> None: assert _display_path(StringPath(r"src\main.py")) == "src/main.py" +def test_display_path_avoids_replace_and_local_reassignment() -> None: + """CLI path formatting must not regress to replace-based hot-loop rebuilding.""" + tree = _function_tree(_display_path) + attribute_calls = { + node.func.attr + for node in ast.walk(tree) + if isinstance(node, ast.Call) and isinstance(node.func, ast.Attribute) + } + reassignments = tuple( + node + for node in ast.walk(tree) + if isinstance(node, (ast.Assign, ast.AnnAssign, ast.AugAssign, ast.NamedExpr)) + ) + + assert "replace" not in attribute_calls + assert reassignments == () + + @pytest.mark.parametrize( ("path_text", "expects_template_marker"), [ @@ -67,11 +93,12 @@ def test_template_and_view_markers_use_exact_path_components( def test_signal_detection_avoids_replace_split_hot_loop_allocations() -> None: - """Signal extraction must not rebuild and split every path in the scan loop.""" - source = inspect.getsource(_detect_signals) + """Signal extraction and its helper must avoid replace/split path rebuilding.""" + for function in (_detect_signals, _iter_lower_path_components): + source = inspect.getsource(function) - assert ".replace(" not in source - assert ".split(" not in source + assert ".replace(" not in source + assert ".split(" not in source def test_generator_input_is_materialized_once_for_profile_detection(tmp_path: Path) -> None: From 01c40c827ebef2a8dcd47d835cce42237ef71397 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Mon, 10 Aug 2026 00:42:56 +0900 Subject: [PATCH 7/8] test(perf): verify observable path formatting costs --- ...est_language_path_optimization_contract.py | 33 +++++++------------ 1 file changed, 11 insertions(+), 22 deletions(-) diff --git a/tests/test_language_path_optimization_contract.py b/tests/test_language_path_optimization_contract.py index fbf55f61..1b19c371 100644 --- a/tests/test_language_path_optimization_contract.py +++ b/tests/test_language_path_optimization_contract.py @@ -1,8 +1,6 @@ """Regression tests for string-based language-profile path handling.""" -import ast import inspect -import textwrap from pathlib import Path import pytest @@ -20,11 +18,6 @@ class StringPath(str): """String path subtype used to preserve the public ``str`` input contract.""" -def _function_tree(function: object) -> ast.AST: - """Return a dedented AST for a source-backed function contract.""" - return ast.parse(textwrap.dedent(inspect.getsource(function))) - - @pytest.mark.parametrize( "path_text", [ @@ -56,22 +49,18 @@ def test_string_subclass_uses_string_display_path_branch() -> None: assert _display_path(StringPath(r"src\main.py")) == "src/main.py" -def test_display_path_avoids_replace_and_local_reassignment() -> None: - """CLI path formatting must not regress to replace-based hot-loop rebuilding.""" - tree = _function_tree(_display_path) - attribute_calls = { - node.func.attr - for node in ast.walk(tree) - if isinstance(node, ast.Call) and isinstance(node.func, ast.Attribute) - } - reassignments = tuple( - node - for node in ast.walk(tree) - if isinstance(node, (ast.Assign, ast.AnnAssign, ast.AugAssign, ast.NamedExpr)) - ) +def test_display_path_avoids_allocating_when_normalization_is_unnecessary() -> None: + """An already normalized plain string must retain its exact object identity.""" + path = "src/packages/appguardrail/module.py" - assert "replace" not in attribute_calls - assert reassignments == () + assert _display_path(path) is path + + +def test_display_path_normalizes_every_windows_separator() -> None: + """Formatting still allocates the required slash-normalized Windows result.""" + assert _display_path(r"src\packages\appguardrail\module.py") == ( + "src/packages/appguardrail/module.py" + ) @pytest.mark.parametrize( From 08c04317370b03f3c5c8e49ed7de4578cc074cf3 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Mon, 10 Aug 2026 00:43:13 +0900 Subject: [PATCH 8/8] docs(perf): distinguish parsing from display normalization --- .jules/bolt.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.jules/bolt.md b/.jules/bolt.md index e1418ec1..2f488a83 100644 --- a/.jules/bolt.md +++ b/.jules/bolt.md @@ -74,6 +74,6 @@ **Learning:** Blindly instantiating `pathlib.Path` objects in hot loops (like file discovery loops or display formatters such as `detect_language_axes` and `_display_path`) creates measurable performance bottlenecks due to object allocation and potential system calls. When checking file extensions or processing path strings, Python's native string methods like `str.rfind()` and `str.replace()` are vastly more efficient. **Action:** Replace `pathlib.Path` usage with fast C-level string operations (`replace("\\", "/")`, `rfind()`, `split()`) in performance-critical areas, particularly when traversing thousands of files, formatting paths, or extracting file extensions. -## 2024-08-08 - Pathlib Instantiation and string operations in Hot Loops -**Learning:** Blindly checking `isinstance(file_path, Path)` or instantiating paths, alongside performing allocating string operations like `replace("\\", "/")`, in hot loops (like file discovery loops or display formatters such as `detect_language_axes` and `_display_path`) creates measurable performance bottlenecks due to object allocation and potential system calls. When checking file extensions or processing path strings, Python's native string methods like `str.rfind()` and `max()` are vastly more efficient. -**Action:** Replace `isinstance(..., Path)` with `type(...) is not str` and avoid `replace("\\", "/")` in favor of fast C-level string operations (`max(path.rfind("/"), path.rfind("\\"))`, `rfind()`, `split()`) in performance-critical areas, particularly when traversing thousands of files, formatting paths, or extracting file extensions. +## 2024-08-08 - Path Parsing in Hot Loops +**Learning:** Instantiating `pathlib.Path` objects or normalizing a complete path when only its final component is needed adds avoidable hot-loop work. Full display normalization is a different contract: producing slash-separated output from a Windows-style string necessarily creates a new string, and CPython's C-level `str.replace()` is the appropriate bounded operation. When no replacement exists, CPython returns the original plain `str` object. +**Action:** Use separator-aware `rfind()` or a streaming component iterator for component extraction. Retain `str.replace("\\", "/")` where the public output requires every separator to change, and test both unchanged-object identity for normalized plain strings and complete Windows-separator normalization instead of banning a production primitive without comparative evidence.