trailmark 0.2.1__tar.gz → 0.2.2__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {trailmark-0.2.1 → trailmark-0.2.2}/PKG-INFO +5 -1
- {trailmark-0.2.1 → trailmark-0.2.2}/README.md +4 -0
- {trailmark-0.2.1 → trailmark-0.2.2}/pyproject.toml +1 -1
- {trailmark-0.2.1 → trailmark-0.2.2}/src/trailmark/__init__.py +1 -1
- {trailmark-0.2.1 → trailmark-0.2.2}/src/trailmark/analysis/entrypoints.py +12 -4
- {trailmark-0.2.1 → trailmark-0.2.2}/src/trailmark/cli.py +18 -1
- {trailmark-0.2.1 → trailmark-0.2.2}/src/trailmark/parsers/_common.py +45 -4
- {trailmark-0.2.1 → trailmark-0.2.2}/tests/test_cli_parser.py +55 -1
- trailmark-0.2.2/tests/test_common_parser.py +154 -0
- {trailmark-0.2.1 → trailmark-0.2.2}/tests/test_entrypoints.py +43 -4
- {trailmark-0.2.1 → trailmark-0.2.2}/uv.lock +2 -2
- {trailmark-0.2.1 → trailmark-0.2.2}/.github/dependabot.yml +0 -0
- {trailmark-0.2.1 → trailmark-0.2.2}/.github/workflows/ci.yml +0 -0
- {trailmark-0.2.1 → trailmark-0.2.2}/.github/workflows/mutation.yml +0 -0
- {trailmark-0.2.1 → trailmark-0.2.2}/.github/workflows/publish.yml +0 -0
- {trailmark-0.2.1 → trailmark-0.2.2}/.github/workflows/semgrep.yml +0 -0
- {trailmark-0.2.1 → trailmark-0.2.2}/.github/workflows/zizmor.yml +0 -0
- {trailmark-0.2.1 → trailmark-0.2.2}/.gitignore +0 -0
- {trailmark-0.2.1 → trailmark-0.2.2}/AGENTS.md +0 -0
- {trailmark-0.2.1 → trailmark-0.2.2}/CONTRIBUTING.md +0 -0
- {trailmark-0.2.1 → trailmark-0.2.2}/LICENSE +0 -0
- {trailmark-0.2.1 → trailmark-0.2.2}/NOTICE +0 -0
- {trailmark-0.2.1 → trailmark-0.2.2}/docs/entrypoint-patterns.md +0 -0
- {trailmark-0.2.1 → trailmark-0.2.2}/src/trailmark/analysis/__init__.py +0 -0
- {trailmark-0.2.1 → trailmark-0.2.2}/src/trailmark/analysis/augment.py +0 -0
- {trailmark-0.2.1 → trailmark-0.2.2}/src/trailmark/analysis/diff.py +0 -0
- {trailmark-0.2.1 → trailmark-0.2.2}/src/trailmark/analysis/preanalysis.py +0 -0
- {trailmark-0.2.1 → trailmark-0.2.2}/src/trailmark/diagram.py +0 -0
- {trailmark-0.2.1 → trailmark-0.2.2}/src/trailmark/models/__init__.py +0 -0
- {trailmark-0.2.1 → trailmark-0.2.2}/src/trailmark/models/annotations.py +0 -0
- {trailmark-0.2.1 → trailmark-0.2.2}/src/trailmark/models/edges.py +0 -0
- {trailmark-0.2.1 → trailmark-0.2.2}/src/trailmark/models/graph.py +0 -0
- {trailmark-0.2.1 → trailmark-0.2.2}/src/trailmark/models/nodes.py +0 -0
- {trailmark-0.2.1 → trailmark-0.2.2}/src/trailmark/parsers/__init__.py +0 -0
- {trailmark-0.2.1 → trailmark-0.2.2}/src/trailmark/parsers/base.py +0 -0
- {trailmark-0.2.1 → trailmark-0.2.2}/src/trailmark/parsers/c/__init__.py +0 -0
- {trailmark-0.2.1 → trailmark-0.2.2}/src/trailmark/parsers/c/parser.py +0 -0
- {trailmark-0.2.1 → trailmark-0.2.2}/src/trailmark/parsers/cairo/__init__.py +0 -0
- {trailmark-0.2.1 → trailmark-0.2.2}/src/trailmark/parsers/cairo/parser.py +0 -0
- {trailmark-0.2.1 → trailmark-0.2.2}/src/trailmark/parsers/circom/__init__.py +0 -0
- {trailmark-0.2.1 → trailmark-0.2.2}/src/trailmark/parsers/circom/parser.py +0 -0
- {trailmark-0.2.1 → trailmark-0.2.2}/src/trailmark/parsers/cpp/__init__.py +0 -0
- {trailmark-0.2.1 → trailmark-0.2.2}/src/trailmark/parsers/cpp/parser.py +0 -0
- {trailmark-0.2.1 → trailmark-0.2.2}/src/trailmark/parsers/csharp/__init__.py +0 -0
- {trailmark-0.2.1 → trailmark-0.2.2}/src/trailmark/parsers/csharp/parser.py +0 -0
- {trailmark-0.2.1 → trailmark-0.2.2}/src/trailmark/parsers/dart/__init__.py +0 -0
- {trailmark-0.2.1 → trailmark-0.2.2}/src/trailmark/parsers/dart/parser.py +0 -0
- {trailmark-0.2.1 → trailmark-0.2.2}/src/trailmark/parsers/erlang/__init__.py +0 -0
- {trailmark-0.2.1 → trailmark-0.2.2}/src/trailmark/parsers/erlang/parser.py +0 -0
- {trailmark-0.2.1 → trailmark-0.2.2}/src/trailmark/parsers/go/__init__.py +0 -0
- {trailmark-0.2.1 → trailmark-0.2.2}/src/trailmark/parsers/go/parser.py +0 -0
- {trailmark-0.2.1 → trailmark-0.2.2}/src/trailmark/parsers/haskell/__init__.py +0 -0
- {trailmark-0.2.1 → trailmark-0.2.2}/src/trailmark/parsers/haskell/parser.py +0 -0
- {trailmark-0.2.1 → trailmark-0.2.2}/src/trailmark/parsers/java/__init__.py +0 -0
- {trailmark-0.2.1 → trailmark-0.2.2}/src/trailmark/parsers/java/parser.py +0 -0
- {trailmark-0.2.1 → trailmark-0.2.2}/src/trailmark/parsers/javascript/__init__.py +0 -0
- {trailmark-0.2.1 → trailmark-0.2.2}/src/trailmark/parsers/javascript/parser.py +0 -0
- {trailmark-0.2.1 → trailmark-0.2.2}/src/trailmark/parsers/kotlin/__init__.py +0 -0
- {trailmark-0.2.1 → trailmark-0.2.2}/src/trailmark/parsers/kotlin/parser.py +0 -0
- {trailmark-0.2.1 → trailmark-0.2.2}/src/trailmark/parsers/masm/__init__.py +0 -0
- {trailmark-0.2.1 → trailmark-0.2.2}/src/trailmark/parsers/masm/parser.py +0 -0
- {trailmark-0.2.1 → trailmark-0.2.2}/src/trailmark/parsers/objc/__init__.py +0 -0
- {trailmark-0.2.1 → trailmark-0.2.2}/src/trailmark/parsers/objc/parser.py +0 -0
- {trailmark-0.2.1 → trailmark-0.2.2}/src/trailmark/parsers/php/__init__.py +0 -0
- {trailmark-0.2.1 → trailmark-0.2.2}/src/trailmark/parsers/php/parser.py +0 -0
- {trailmark-0.2.1 → trailmark-0.2.2}/src/trailmark/parsers/python/__init__.py +0 -0
- {trailmark-0.2.1 → trailmark-0.2.2}/src/trailmark/parsers/python/parser.py +0 -0
- {trailmark-0.2.1 → trailmark-0.2.2}/src/trailmark/parsers/ruby/__init__.py +0 -0
- {trailmark-0.2.1 → trailmark-0.2.2}/src/trailmark/parsers/ruby/parser.py +0 -0
- {trailmark-0.2.1 → trailmark-0.2.2}/src/trailmark/parsers/rust/__init__.py +0 -0
- {trailmark-0.2.1 → trailmark-0.2.2}/src/trailmark/parsers/rust/parser.py +0 -0
- {trailmark-0.2.1 → trailmark-0.2.2}/src/trailmark/parsers/solidity/__init__.py +0 -0
- {trailmark-0.2.1 → trailmark-0.2.2}/src/trailmark/parsers/solidity/parser.py +0 -0
- {trailmark-0.2.1 → trailmark-0.2.2}/src/trailmark/parsers/swift/__init__.py +0 -0
- {trailmark-0.2.1 → trailmark-0.2.2}/src/trailmark/parsers/swift/parser.py +0 -0
- {trailmark-0.2.1 → trailmark-0.2.2}/src/trailmark/parsers/typescript/__init__.py +0 -0
- {trailmark-0.2.1 → trailmark-0.2.2}/src/trailmark/parsers/typescript/parser.py +0 -0
- {trailmark-0.2.1 → trailmark-0.2.2}/src/trailmark/py.typed +0 -0
- {trailmark-0.2.1 → trailmark-0.2.2}/src/trailmark/query/__init__.py +0 -0
- {trailmark-0.2.1 → trailmark-0.2.2}/src/trailmark/query/api.py +0 -0
- {trailmark-0.2.1 → trailmark-0.2.2}/src/trailmark/storage/__init__.py +0 -0
- {trailmark-0.2.1 → trailmark-0.2.2}/src/trailmark/storage/graph_store.py +0 -0
- {trailmark-0.2.1 → trailmark-0.2.2}/src/trailmark/tree_sitter_custom/__init__.py +0 -0
- {trailmark-0.2.1 → trailmark-0.2.2}/src/trailmark/tree_sitter_custom/circom/LICENSE +0 -0
- {trailmark-0.2.1 → trailmark-0.2.2}/src/trailmark/tree_sitter_custom/circom/__init__.py +0 -0
- {trailmark-0.2.1 → trailmark-0.2.2}/src/trailmark/tree_sitter_custom/circom/binding.c +0 -0
- {trailmark-0.2.1 → trailmark-0.2.2}/src/trailmark/tree_sitter_custom/circom/grammar.js +0 -0
- {trailmark-0.2.1 → trailmark-0.2.2}/src/trailmark/tree_sitter_custom/circom/src/parser.c +0 -0
- {trailmark-0.2.1 → trailmark-0.2.2}/src/trailmark/tree_sitter_custom/circom/src/tree_sitter/parser.h +0 -0
- {trailmark-0.2.1 → trailmark-0.2.2}/src/trailmark/tree_sitter_custom/masm/LICENSE +0 -0
- {trailmark-0.2.1 → trailmark-0.2.2}/src/trailmark/tree_sitter_custom/masm/__init__.py +0 -0
- {trailmark-0.2.1 → trailmark-0.2.2}/src/trailmark/tree_sitter_custom/masm/binding.c +0 -0
- {trailmark-0.2.1 → trailmark-0.2.2}/src/trailmark/tree_sitter_custom/masm/grammar.js +0 -0
- {trailmark-0.2.1 → trailmark-0.2.2}/src/trailmark/tree_sitter_custom/masm/src/parser.c +0 -0
- {trailmark-0.2.1 → trailmark-0.2.2}/src/trailmark/tree_sitter_custom/masm/src/tree_sitter/alloc.h +0 -0
- {trailmark-0.2.1 → trailmark-0.2.2}/src/trailmark/tree_sitter_custom/masm/src/tree_sitter/array.h +0 -0
- {trailmark-0.2.1 → trailmark-0.2.2}/src/trailmark/tree_sitter_custom/masm/src/tree_sitter/parser.h +0 -0
- {trailmark-0.2.1 → trailmark-0.2.2}/tests/__init__.py +0 -0
- {trailmark-0.2.1 → trailmark-0.2.2}/tests/test_augment.py +0 -0
- {trailmark-0.2.1 → trailmark-0.2.2}/tests/test_c_parser.py +0 -0
- {trailmark-0.2.1 → trailmark-0.2.2}/tests/test_cairo_parser.py +0 -0
- {trailmark-0.2.1 → trailmark-0.2.2}/tests/test_circom_grammar.py +0 -0
- {trailmark-0.2.1 → trailmark-0.2.2}/tests/test_circom_parser.py +0 -0
- {trailmark-0.2.1 → trailmark-0.2.2}/tests/test_cli.py +0 -0
- {trailmark-0.2.1 → trailmark-0.2.2}/tests/test_cpp_parser.py +0 -0
- {trailmark-0.2.1 → trailmark-0.2.2}/tests/test_csharp_parser.py +0 -0
- {trailmark-0.2.1 → trailmark-0.2.2}/tests/test_dart_parser.py +0 -0
- {trailmark-0.2.1 → trailmark-0.2.2}/tests/test_detector_tags.py +0 -0
- {trailmark-0.2.1 → trailmark-0.2.2}/tests/test_diagram_script.py +0 -0
- {trailmark-0.2.1 → trailmark-0.2.2}/tests/test_diff.py +0 -0
- {trailmark-0.2.1 → trailmark-0.2.2}/tests/test_erlang_parser.py +0 -0
- {trailmark-0.2.1 → trailmark-0.2.2}/tests/test_go_parser.py +0 -0
- {trailmark-0.2.1 → trailmark-0.2.2}/tests/test_haskell_parser.py +0 -0
- {trailmark-0.2.1 → trailmark-0.2.2}/tests/test_hidden_apis.py +0 -0
- {trailmark-0.2.1 → trailmark-0.2.2}/tests/test_java_parser.py +0 -0
- {trailmark-0.2.1 → trailmark-0.2.2}/tests/test_javascript_parser.py +0 -0
- {trailmark-0.2.1 → trailmark-0.2.2}/tests/test_kotlin_parser.py +0 -0
- {trailmark-0.2.1 → trailmark-0.2.2}/tests/test_masm_parser.py +0 -0
- {trailmark-0.2.1 → trailmark-0.2.2}/tests/test_models.py +0 -0
- {trailmark-0.2.1 → trailmark-0.2.2}/tests/test_objc_parser.py +0 -0
- {trailmark-0.2.1 → trailmark-0.2.2}/tests/test_parser_edge_cases.py +0 -0
- {trailmark-0.2.1 → trailmark-0.2.2}/tests/test_php_parser.py +0 -0
- {trailmark-0.2.1 → trailmark-0.2.2}/tests/test_polyglot.py +0 -0
- {trailmark-0.2.1 → trailmark-0.2.2}/tests/test_preanalysis.py +0 -0
- {trailmark-0.2.1 → trailmark-0.2.2}/tests/test_property_based.py +0 -0
- {trailmark-0.2.1 → trailmark-0.2.2}/tests/test_python_parser.py +0 -0
- {trailmark-0.2.1 → trailmark-0.2.2}/tests/test_query.py +0 -0
- {trailmark-0.2.1 → trailmark-0.2.2}/tests/test_query_edge_cases.py +0 -0
- {trailmark-0.2.1 → trailmark-0.2.2}/tests/test_readme.py +0 -0
- {trailmark-0.2.1 → trailmark-0.2.2}/tests/test_ruby_parser.py +0 -0
- {trailmark-0.2.1 → trailmark-0.2.2}/tests/test_rust_parser.py +0 -0
- {trailmark-0.2.1 → trailmark-0.2.2}/tests/test_solidity_parser.py +0 -0
- {trailmark-0.2.1 → trailmark-0.2.2}/tests/test_storage.py +0 -0
- {trailmark-0.2.1 → trailmark-0.2.2}/tests/test_storage_edge_cases.py +0 -0
- {trailmark-0.2.1 → trailmark-0.2.2}/tests/test_swift_parser.py +0 -0
- {trailmark-0.2.1 → trailmark-0.2.2}/tests/test_typescript_parser.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: trailmark
|
|
3
|
-
Version: 0.2.
|
|
3
|
+
Version: 0.2.2
|
|
4
4
|
Summary: Parse source code into a queryable graph of functions, classes, calls, and semantic annotations
|
|
5
5
|
Project-URL: Homepage, https://github.com/trailofbits/trailmark
|
|
6
6
|
Project-URL: Repository, https://github.com/trailofbits/trailmark
|
|
@@ -260,6 +260,10 @@ Requires Python ≥ 3.12.
|
|
|
260
260
|
## Usage
|
|
261
261
|
|
|
262
262
|
```bash
|
|
263
|
+
# Report the installed version
|
|
264
|
+
trailmark --version # or: trailmark -V
|
|
265
|
+
trailmark version # subcommand form
|
|
266
|
+
|
|
263
267
|
# Full JSON graph (Python, the default)
|
|
264
268
|
trailmark analyze path/to/project
|
|
265
269
|
|
|
@@ -243,6 +243,10 @@ Requires Python ≥ 3.12.
|
|
|
243
243
|
## Usage
|
|
244
244
|
|
|
245
245
|
```bash
|
|
246
|
+
# Report the installed version
|
|
247
|
+
trailmark --version # or: trailmark -V
|
|
248
|
+
trailmark version # subcommand form
|
|
249
|
+
|
|
246
250
|
# Full JSON graph (Python, the default)
|
|
247
251
|
trailmark analyze path/to/project
|
|
248
252
|
|
|
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "trailmark"
|
|
7
|
-
version = "0.2.
|
|
7
|
+
version = "0.2.2"
|
|
8
8
|
description = "Parse source code into a queryable graph of functions, classes, calls, and semantic annotations"
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
requires-python = ">=3.12"
|
|
@@ -1106,9 +1106,9 @@ def _detect_pyproject_scripts(
|
|
|
1106
1106
|
) -> dict[str, EntrypointTag]:
|
|
1107
1107
|
"""Read ``[project.scripts]`` from pyproject.toml and tag each target.
|
|
1108
1108
|
|
|
1109
|
-
Entries take the form ``name = "module.path:function"``. We
|
|
1110
|
-
matching node
|
|
1111
|
-
|
|
1109
|
+
Entries take the form ``name = "module.path:function"``. We first try the
|
|
1110
|
+
matching node ID directly, then fall back to an unambiguous file path suffix
|
|
1111
|
+
match for parses rooted below the import root.
|
|
1112
1112
|
"""
|
|
1113
1113
|
pyproject = repo_root / "pyproject.toml"
|
|
1114
1114
|
if not pyproject.exists():
|
|
@@ -1149,12 +1149,20 @@ def _resolve_script_target(
|
|
|
1149
1149
|
func_name: str,
|
|
1150
1150
|
) -> str | None:
|
|
1151
1151
|
"""Find the node id matching a ``module.path:function`` script target."""
|
|
1152
|
+
exact_id = f"{module_path}:{func_name}"
|
|
1153
|
+
unit = graph.nodes.get(exact_id)
|
|
1154
|
+
if unit is not None and unit.name == func_name:
|
|
1155
|
+
return exact_id
|
|
1156
|
+
|
|
1152
1157
|
suffix = module_path.replace(".", "/") + ".py"
|
|
1158
|
+
matches: list[str] = []
|
|
1153
1159
|
for node_id, unit in graph.nodes.items():
|
|
1154
1160
|
if unit.name != func_name:
|
|
1155
1161
|
continue
|
|
1156
1162
|
if unit.location.file_path.endswith(suffix):
|
|
1157
|
-
|
|
1163
|
+
matches.append(node_id)
|
|
1164
|
+
if len(matches) == 1:
|
|
1165
|
+
return matches[0]
|
|
1158
1166
|
return None
|
|
1159
1167
|
|
|
1160
1168
|
|
|
@@ -8,17 +8,31 @@ import sys
|
|
|
8
8
|
from pathlib import Path
|
|
9
9
|
from typing import Any
|
|
10
10
|
|
|
11
|
+
from trailmark import __version__
|
|
11
12
|
from trailmark.query.api import QueryEngine
|
|
12
13
|
|
|
14
|
+
_VERSION_STRING = f"trailmark {__version__}"
|
|
15
|
+
|
|
13
16
|
|
|
14
17
|
def build_parser() -> argparse.ArgumentParser:
|
|
15
18
|
"""Construct the Trailmark CLI's argparse tree."""
|
|
16
19
|
parser = argparse.ArgumentParser(
|
|
17
20
|
prog="trailmark",
|
|
18
|
-
description="Parse source code into queryable graphs",
|
|
21
|
+
description=f"Parse source code into queryable graphs (v{__version__})",
|
|
22
|
+
)
|
|
23
|
+
parser.add_argument(
|
|
24
|
+
"--version",
|
|
25
|
+
"-V",
|
|
26
|
+
action="version",
|
|
27
|
+
version=_VERSION_STRING,
|
|
19
28
|
)
|
|
20
29
|
subparsers = parser.add_subparsers(dest="command")
|
|
21
30
|
|
|
31
|
+
subparsers.add_parser(
|
|
32
|
+
"version",
|
|
33
|
+
help="Print the Trailmark version and exit",
|
|
34
|
+
)
|
|
35
|
+
|
|
22
36
|
analyze = subparsers.add_parser(
|
|
23
37
|
"analyze",
|
|
24
38
|
help="Analyze a directory and output the code graph",
|
|
@@ -133,6 +147,9 @@ def main() -> None:
|
|
|
133
147
|
parser.print_help()
|
|
134
148
|
sys.exit(1)
|
|
135
149
|
|
|
150
|
+
if args.command == "version":
|
|
151
|
+
print(_VERSION_STRING)
|
|
152
|
+
return
|
|
136
153
|
if args.command == "analyze":
|
|
137
154
|
_run_analyze(args)
|
|
138
155
|
elif args.command == "augment":
|
|
@@ -4,6 +4,7 @@ from __future__ import annotations
|
|
|
4
4
|
|
|
5
5
|
import os
|
|
6
6
|
from collections.abc import Callable
|
|
7
|
+
from contextvars import ContextVar
|
|
7
8
|
from pathlib import Path
|
|
8
9
|
from typing import TYPE_CHECKING
|
|
9
10
|
|
|
@@ -39,13 +40,49 @@ def make_location(node: Node, file_path: str) -> SourceLocation:
|
|
|
39
40
|
)
|
|
40
41
|
|
|
41
42
|
|
|
43
|
+
_DIRECTORY_PARSE_ROOT: ContextVar[Path | None] = ContextVar(
|
|
44
|
+
"_DIRECTORY_PARSE_ROOT",
|
|
45
|
+
default=None,
|
|
46
|
+
)
|
|
47
|
+
|
|
48
|
+
|
|
42
49
|
def module_id_from_path(file_path: str) -> str:
|
|
43
|
-
"""Derive a module ID from a file path.
|
|
50
|
+
"""Derive a module ID from a file path.
|
|
51
|
+
|
|
52
|
+
Single-file parsing preserves the historical filename-stem ID. Directory
|
|
53
|
+
parsing uses a root-relative dotted path so same-named files in different
|
|
54
|
+
directories do not overwrite each other when their graphs are merged.
|
|
55
|
+
"""
|
|
44
56
|
p = Path(file_path)
|
|
57
|
+
root = _DIRECTORY_PARSE_ROOT.get()
|
|
58
|
+
if root is not None:
|
|
59
|
+
try:
|
|
60
|
+
rel_path = _absolute_lexical_path(p).relative_to(root)
|
|
61
|
+
except ValueError:
|
|
62
|
+
pass
|
|
63
|
+
else:
|
|
64
|
+
if rel_path.stem == "__init__":
|
|
65
|
+
module_path = rel_path.parent
|
|
66
|
+
if module_path == Path("."):
|
|
67
|
+
return _escape_module_path_part(root.name)
|
|
68
|
+
else:
|
|
69
|
+
module_path = rel_path.with_suffix("")
|
|
70
|
+
return ".".join(_escape_module_path_part(part) for part in module_path.parts)
|
|
71
|
+
|
|
45
72
|
stem = p.stem if p.stem != "__init__" else p.parent.name
|
|
46
73
|
return stem
|
|
47
74
|
|
|
48
75
|
|
|
76
|
+
def _absolute_lexical_path(path: Path) -> Path:
|
|
77
|
+
"""Return an absolute path without resolving symlink targets."""
|
|
78
|
+
return Path(os.path.abspath(path))
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
def _escape_module_path_part(part: str) -> str:
|
|
82
|
+
"""Escape separators used in dotted module IDs."""
|
|
83
|
+
return part.replace("\\", "\\\\").replace(".", "\\.")
|
|
84
|
+
|
|
85
|
+
|
|
49
86
|
_EXCLUDED_DIRS = frozenset(
|
|
50
87
|
{
|
|
51
88
|
".git",
|
|
@@ -98,9 +135,13 @@ def parse_directory(
|
|
|
98
135
|
extensions: File extensions to include.
|
|
99
136
|
"""
|
|
100
137
|
merged = CodeGraph(language=language, root_path=dir_path)
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
138
|
+
token = _DIRECTORY_PARSE_ROOT.set(_absolute_lexical_path(Path(dir_path)))
|
|
139
|
+
try:
|
|
140
|
+
for fpath in walk_source_files(dir_path, extensions):
|
|
141
|
+
file_graph = parse_file_fn(fpath)
|
|
142
|
+
merged.merge(file_graph)
|
|
143
|
+
finally:
|
|
144
|
+
_DIRECTORY_PARSE_ROOT.reset(token)
|
|
104
145
|
return merged
|
|
105
146
|
|
|
106
147
|
|
|
@@ -58,7 +58,48 @@ class TestTopLevelParser:
|
|
|
58
58
|
def test_all_subcommands_registered(
|
|
59
59
|
self, subparsers_map: dict[str, argparse.ArgumentParser]
|
|
60
60
|
) -> None:
|
|
61
|
-
assert set(subparsers_map) == {
|
|
61
|
+
assert set(subparsers_map) == {
|
|
62
|
+
"analyze",
|
|
63
|
+
"augment",
|
|
64
|
+
"entrypoints",
|
|
65
|
+
"diff",
|
|
66
|
+
"version",
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
class TestVersionFlag:
|
|
71
|
+
"""Regression for issue #26 — the CLI must expose its version."""
|
|
72
|
+
|
|
73
|
+
def test_long_flag_prints_and_exits(
|
|
74
|
+
self, parser: argparse.ArgumentParser, capsys: pytest.CaptureFixture[str]
|
|
75
|
+
) -> None:
|
|
76
|
+
import trailmark
|
|
77
|
+
|
|
78
|
+
with pytest.raises(SystemExit) as excinfo:
|
|
79
|
+
parser.parse_args(["--version"])
|
|
80
|
+
assert excinfo.value.code == 0
|
|
81
|
+
out = capsys.readouterr().out
|
|
82
|
+
assert out.strip() == f"trailmark {trailmark.__version__}"
|
|
83
|
+
|
|
84
|
+
def test_short_flag_prints_and_exits(
|
|
85
|
+
self, parser: argparse.ArgumentParser, capsys: pytest.CaptureFixture[str]
|
|
86
|
+
) -> None:
|
|
87
|
+
import trailmark
|
|
88
|
+
|
|
89
|
+
with pytest.raises(SystemExit) as excinfo:
|
|
90
|
+
parser.parse_args(["-V"])
|
|
91
|
+
assert excinfo.value.code == 0
|
|
92
|
+
out = capsys.readouterr().out
|
|
93
|
+
assert out.strip() == f"trailmark {trailmark.__version__}"
|
|
94
|
+
|
|
95
|
+
def test_version_subcommand_registered(
|
|
96
|
+
self, subparsers_map: dict[str, argparse.ArgumentParser]
|
|
97
|
+
) -> None:
|
|
98
|
+
assert "version" in subparsers_map
|
|
99
|
+
|
|
100
|
+
def test_version_subcommand_parses(self, parser: argparse.ArgumentParser) -> None:
|
|
101
|
+
args = parser.parse_args(["version"])
|
|
102
|
+
assert args.command == "version"
|
|
62
103
|
|
|
63
104
|
|
|
64
105
|
class TestAnalyzeSubparser:
|
|
@@ -206,3 +247,16 @@ class TestParseBehavior:
|
|
|
206
247
|
def test_augment_repeatable_sarif(self, parser: argparse.ArgumentParser) -> None:
|
|
207
248
|
args = parser.parse_args(["augment", "p", "--sarif", "a.sarif", "--sarif", "b.sarif"])
|
|
208
249
|
assert args.sarif == ["a.sarif", "b.sarif"]
|
|
250
|
+
|
|
251
|
+
|
|
252
|
+
class TestVersionSubcommandExecution:
|
|
253
|
+
def test_main_version_prints_version(
|
|
254
|
+
self, capsys: pytest.CaptureFixture[str], monkeypatch: pytest.MonkeyPatch
|
|
255
|
+
) -> None:
|
|
256
|
+
import trailmark
|
|
257
|
+
from trailmark.cli import main
|
|
258
|
+
|
|
259
|
+
monkeypatch.setattr("sys.argv", ["trailmark", "version"])
|
|
260
|
+
main()
|
|
261
|
+
out = capsys.readouterr().out
|
|
262
|
+
assert out.strip() == f"trailmark {trailmark.__version__}"
|
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
"""Tests for parser utilities shared across language implementations."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
|
|
7
|
+
import pytest
|
|
8
|
+
|
|
9
|
+
from trailmark.models.edges import CodeEdge, EdgeKind
|
|
10
|
+
from trailmark.models.graph import CodeGraph
|
|
11
|
+
from trailmark.models.nodes import CodeUnit, NodeKind, SourceLocation
|
|
12
|
+
from trailmark.parsers._common import module_id_from_path, parse_directory
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def _fake_parse_file(file_path: str) -> CodeGraph:
|
|
16
|
+
"""Build a tiny graph using the shared module ID helper."""
|
|
17
|
+
module_id = module_id_from_path(file_path)
|
|
18
|
+
function_id = f"{module_id}:helper"
|
|
19
|
+
location = SourceLocation(file_path=file_path, start_line=1, end_line=1)
|
|
20
|
+
return CodeGraph(
|
|
21
|
+
nodes={
|
|
22
|
+
module_id: CodeUnit(
|
|
23
|
+
id=module_id,
|
|
24
|
+
name=module_id,
|
|
25
|
+
kind=NodeKind.MODULE,
|
|
26
|
+
location=location,
|
|
27
|
+
),
|
|
28
|
+
function_id: CodeUnit(
|
|
29
|
+
id=function_id,
|
|
30
|
+
name="helper",
|
|
31
|
+
kind=NodeKind.FUNCTION,
|
|
32
|
+
location=location,
|
|
33
|
+
),
|
|
34
|
+
},
|
|
35
|
+
edges=[
|
|
36
|
+
CodeEdge(
|
|
37
|
+
source_id=module_id,
|
|
38
|
+
target_id=function_id,
|
|
39
|
+
kind=EdgeKind.CONTAINS,
|
|
40
|
+
)
|
|
41
|
+
],
|
|
42
|
+
language="test",
|
|
43
|
+
root_path=file_path,
|
|
44
|
+
)
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def test_module_id_from_path_preserves_single_file_stem_behavior(tmp_path: Path) -> None:
|
|
48
|
+
file_path = tmp_path / "src" / "compat.py"
|
|
49
|
+
file_path.parent.mkdir()
|
|
50
|
+
file_path.write_text("")
|
|
51
|
+
|
|
52
|
+
assert module_id_from_path(str(file_path)) == "compat"
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def test_parse_directory_uses_root_relative_module_ids(tmp_path: Path) -> None:
|
|
56
|
+
src = tmp_path / "src"
|
|
57
|
+
tests = tmp_path / "tests"
|
|
58
|
+
src.mkdir()
|
|
59
|
+
tests.mkdir()
|
|
60
|
+
(src / "compat.py").write_text("")
|
|
61
|
+
(tests / "compat.py").write_text("")
|
|
62
|
+
|
|
63
|
+
graph = parse_directory(
|
|
64
|
+
_fake_parse_file,
|
|
65
|
+
language="test",
|
|
66
|
+
dir_path=str(tmp_path),
|
|
67
|
+
extensions=(".py",),
|
|
68
|
+
)
|
|
69
|
+
|
|
70
|
+
assert set(graph.nodes) == {
|
|
71
|
+
"src.compat",
|
|
72
|
+
"src.compat:helper",
|
|
73
|
+
"tests.compat",
|
|
74
|
+
"tests.compat:helper",
|
|
75
|
+
}
|
|
76
|
+
assert {(edge.source_id, edge.target_id) for edge in graph.edges} == {
|
|
77
|
+
("src.compat", "src.compat:helper"),
|
|
78
|
+
("tests.compat", "tests.compat:helper"),
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
def test_parse_directory_escapes_dotted_path_components(tmp_path: Path) -> None:
|
|
83
|
+
dotted_dir = tmp_path / "a.b"
|
|
84
|
+
dotted_file_parent = tmp_path / "a"
|
|
85
|
+
dotted_dir.mkdir()
|
|
86
|
+
dotted_file_parent.mkdir()
|
|
87
|
+
(dotted_dir / "c.py").write_text("")
|
|
88
|
+
(dotted_file_parent / "b.c.py").write_text("")
|
|
89
|
+
|
|
90
|
+
graph = parse_directory(
|
|
91
|
+
_fake_parse_file,
|
|
92
|
+
language="test",
|
|
93
|
+
dir_path=str(tmp_path),
|
|
94
|
+
extensions=(".py",),
|
|
95
|
+
)
|
|
96
|
+
|
|
97
|
+
assert set(graph.nodes) == {
|
|
98
|
+
r"a\.b.c",
|
|
99
|
+
r"a\.b.c:helper",
|
|
100
|
+
r"a.b\.c",
|
|
101
|
+
r"a.b\.c:helper",
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
def test_parse_directory_uses_package_path_for_init_files(tmp_path: Path) -> None:
|
|
106
|
+
pkg = tmp_path / "pkg"
|
|
107
|
+
subpkg = pkg / "subpkg"
|
|
108
|
+
subpkg.mkdir(parents=True)
|
|
109
|
+
(pkg / "__init__.py").write_text("")
|
|
110
|
+
(subpkg / "__init__.py").write_text("")
|
|
111
|
+
|
|
112
|
+
graph = parse_directory(
|
|
113
|
+
_fake_parse_file,
|
|
114
|
+
language="test",
|
|
115
|
+
dir_path=str(tmp_path),
|
|
116
|
+
extensions=(".py",),
|
|
117
|
+
)
|
|
118
|
+
|
|
119
|
+
assert set(graph.nodes) == {
|
|
120
|
+
"pkg",
|
|
121
|
+
"pkg:helper",
|
|
122
|
+
"pkg.subpkg",
|
|
123
|
+
"pkg.subpkg:helper",
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
def test_parse_directory_uses_lexical_path_for_symlinked_files(tmp_path: Path) -> None:
|
|
128
|
+
root = tmp_path / "root"
|
|
129
|
+
outside = tmp_path / "outside"
|
|
130
|
+
src = root / "src"
|
|
131
|
+
tests = root / "tests"
|
|
132
|
+
outside.mkdir()
|
|
133
|
+
src.mkdir(parents=True)
|
|
134
|
+
tests.mkdir()
|
|
135
|
+
(outside / "compat.py").write_text("")
|
|
136
|
+
(tests / "compat.py").write_text("")
|
|
137
|
+
try:
|
|
138
|
+
(src / "compat.py").symlink_to(outside / "compat.py")
|
|
139
|
+
except OSError:
|
|
140
|
+
pytest.skip("symlink creation is not supported on this platform")
|
|
141
|
+
|
|
142
|
+
graph = parse_directory(
|
|
143
|
+
_fake_parse_file,
|
|
144
|
+
language="test",
|
|
145
|
+
dir_path=str(root),
|
|
146
|
+
extensions=(".py",),
|
|
147
|
+
)
|
|
148
|
+
|
|
149
|
+
assert set(graph.nodes) == {
|
|
150
|
+
"src.compat",
|
|
151
|
+
"src.compat:helper",
|
|
152
|
+
"tests.compat",
|
|
153
|
+
"tests.compat:helper",
|
|
154
|
+
}
|
|
@@ -46,8 +46,8 @@ class TestSelfAnalysis:
|
|
|
46
46
|
engine = QueryEngine.from_directory(str(SRC), language="python")
|
|
47
47
|
surface = engine.attack_surface()
|
|
48
48
|
node_ids = {ep["node_id"] for ep in surface}
|
|
49
|
-
assert "cli:main" in node_ids, (
|
|
50
|
-
f"Expected cli:main (the pyproject.toml script target) in {node_ids}"
|
|
49
|
+
assert "trailmark.cli:main" in node_ids, (
|
|
50
|
+
f"Expected trailmark.cli:main (the pyproject.toml script target) in {node_ids}"
|
|
51
51
|
)
|
|
52
52
|
|
|
53
53
|
|
|
@@ -106,6 +106,28 @@ class TestPyprojectScripts:
|
|
|
106
106
|
ids = {ep["node_id"] for ep in engine.attack_surface()}
|
|
107
107
|
assert "app:main" in ids
|
|
108
108
|
|
|
109
|
+
def test_pyproject_script_ambiguous_suffix_is_skipped(self, tmp_path: Path) -> None:
|
|
110
|
+
"""Suffix fallback must not select an order-dependent script target."""
|
|
111
|
+
(tmp_path / "pyproject.toml").write_text(
|
|
112
|
+
"[project]\n"
|
|
113
|
+
'name = "demo"\n'
|
|
114
|
+
'version = "0.0.0"\n'
|
|
115
|
+
"[project.scripts]\n"
|
|
116
|
+
'demo = "pkg.app:main"\n',
|
|
117
|
+
)
|
|
118
|
+
src_pkg = tmp_path / "src" / "pkg"
|
|
119
|
+
tests_pkg = tmp_path / "tests" / "pkg"
|
|
120
|
+
src_pkg.mkdir(parents=True)
|
|
121
|
+
tests_pkg.mkdir(parents=True)
|
|
122
|
+
(src_pkg / "app.py").write_text("def main():\n pass\n")
|
|
123
|
+
(tests_pkg / "app.py").write_text("def main():\n pass\n")
|
|
124
|
+
|
|
125
|
+
engine = QueryEngine.from_directory(str(tmp_path))
|
|
126
|
+
surface = {ep["node_id"]: ep for ep in engine.attack_surface()}
|
|
127
|
+
|
|
128
|
+
assert surface["src.pkg.app:main"]["trust_level"] == "trusted_internal"
|
|
129
|
+
assert surface["tests.pkg.app:main"]["trust_level"] == "trusted_internal"
|
|
130
|
+
|
|
109
131
|
def test_malformed_pyproject_is_tolerated(self, tmp_path: Path) -> None:
|
|
110
132
|
"""A broken pyproject.toml must not crash detection."""
|
|
111
133
|
(tmp_path / "pyproject.toml").write_text("this is not valid toml = [")
|
|
@@ -169,7 +191,24 @@ class TestOverrideFile:
|
|
|
169
191
|
)
|
|
170
192
|
engine = QueryEngine.from_directory(str(tmp_path))
|
|
171
193
|
ids = {ep["node_id"] for ep in engine.attack_surface()}
|
|
172
|
-
assert "app:serve" in ids
|
|
194
|
+
assert "pkg.app:serve" in ids
|
|
195
|
+
|
|
196
|
+
def test_override_ambiguous_module_reference_is_skipped(self, tmp_path: Path) -> None:
|
|
197
|
+
"""Ambiguous module references should not tag the first suffix match."""
|
|
198
|
+
src_pkg = tmp_path / "src" / "pkg"
|
|
199
|
+
tests_pkg = tmp_path / "tests" / "pkg"
|
|
200
|
+
src_pkg.mkdir(parents=True)
|
|
201
|
+
tests_pkg.mkdir(parents=True)
|
|
202
|
+
(src_pkg / "app.py").write_text("def serve():\n pass\n")
|
|
203
|
+
(tests_pkg / "app.py").write_text("def serve():\n pass\n")
|
|
204
|
+
self._write_override(
|
|
205
|
+
tmp_path,
|
|
206
|
+
'[[entrypoint]]\nnode = "pkg.app:serve"\nkind = "api"\n',
|
|
207
|
+
)
|
|
208
|
+
|
|
209
|
+
engine = QueryEngine.from_directory(str(tmp_path))
|
|
210
|
+
|
|
211
|
+
assert engine.attack_surface() == []
|
|
173
212
|
|
|
174
213
|
def test_override_unknown_node_is_skipped(self, tmp_path: Path) -> None:
|
|
175
214
|
(tmp_path / "app.py").write_text("def real():\n pass\n")
|
|
@@ -886,7 +925,7 @@ class TestBulkOverrideRules:
|
|
|
886
925
|
engine = QueryEngine.from_directory(str(tmp_path), language="python")
|
|
887
926
|
ids = {ep["node_id"] for ep in engine.attack_surface()}
|
|
888
927
|
# In public/ AND matches name_regex -> yes
|
|
889
|
-
assert "web:handle_login" in ids
|
|
928
|
+
assert "public.web:handle_login" in ids
|
|
890
929
|
# In public/ but fails name_regex -> no
|
|
891
930
|
assert "util:helper" not in ids
|
|
892
931
|
# Matches name_regex but not in public/ -> no
|
|
@@ -8,7 +8,7 @@ resolution-markers = [
|
|
|
8
8
|
]
|
|
9
9
|
|
|
10
10
|
[options]
|
|
11
|
-
exclude-newer = "2026-04-
|
|
11
|
+
exclude-newer = "2026-04-17T00:53:06.94394Z"
|
|
12
12
|
exclude-newer-span = "P1W"
|
|
13
13
|
|
|
14
14
|
[[package]]
|
|
@@ -599,7 +599,7 @@ wheels = [
|
|
|
599
599
|
|
|
600
600
|
[[package]]
|
|
601
601
|
name = "trailmark"
|
|
602
|
-
version = "0.2.
|
|
602
|
+
version = "0.2.2"
|
|
603
603
|
source = { editable = "." }
|
|
604
604
|
dependencies = [
|
|
605
605
|
{ name = "rustworkx" },
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{trailmark-0.2.1 → trailmark-0.2.2}/src/trailmark/tree_sitter_custom/circom/src/tree_sitter/parser.h
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{trailmark-0.2.1 → trailmark-0.2.2}/src/trailmark/tree_sitter_custom/masm/src/tree_sitter/alloc.h
RENAMED
|
File without changes
|
{trailmark-0.2.1 → trailmark-0.2.2}/src/trailmark/tree_sitter_custom/masm/src/tree_sitter/array.h
RENAMED
|
File without changes
|
{trailmark-0.2.1 → trailmark-0.2.2}/src/trailmark/tree_sitter_custom/masm/src/tree_sitter/parser.h
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|