trailmark 0.2.2__tar.gz → 0.3.1__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.2 → trailmark-0.3.1}/.github/workflows/ci.yml +2 -2
- {trailmark-0.2.2 → trailmark-0.3.1}/.github/workflows/mutation.yml +1 -1
- {trailmark-0.2.2 → trailmark-0.3.1}/.github/workflows/publish.yml +1 -1
- {trailmark-0.2.2 → trailmark-0.3.1}/CONTRIBUTING.md +7 -2
- {trailmark-0.2.2 → trailmark-0.3.1}/PKG-INFO +8 -3
- {trailmark-0.2.2 → trailmark-0.3.1}/README.md +6 -1
- {trailmark-0.2.2 → trailmark-0.3.1}/pyproject.toml +2 -2
- trailmark-0.3.1/src/trailmark/__init__.py +16 -0
- {trailmark-0.2.2 → trailmark-0.3.1}/src/trailmark/analysis/preanalysis.py +3 -3
- trailmark-0.3.1/src/trailmark/parse.py +216 -0
- {trailmark-0.2.2 → trailmark-0.3.1}/src/trailmark/parsers/_common.py +67 -2
- {trailmark-0.2.2 → trailmark-0.3.1}/src/trailmark/parsers/c/parser.py +14 -0
- {trailmark-0.2.2 → trailmark-0.3.1}/src/trailmark/parsers/cpp/parser.py +8 -3
- {trailmark-0.2.2 → trailmark-0.3.1}/src/trailmark/parsers/erlang/parser.py +25 -2
- {trailmark-0.2.2 → trailmark-0.3.1}/src/trailmark/query/api.py +5 -178
- {trailmark-0.2.2 → trailmark-0.3.1}/src/trailmark/storage/graph_store.py +24 -10
- {trailmark-0.2.2 → trailmark-0.3.1}/tests/test_c_parser.py +60 -0
- trailmark-0.3.1/tests/test_common_parser.py +320 -0
- {trailmark-0.2.2 → trailmark-0.3.1}/tests/test_hidden_apis.py +3 -7
- trailmark-0.3.1/tests/test_package_metadata.py +16 -0
- trailmark-0.3.1/tests/test_parse_api.py +106 -0
- {trailmark-0.2.2 → trailmark-0.3.1}/tests/test_polyglot.py +13 -1
- {trailmark-0.2.2 → trailmark-0.3.1}/tests/test_preanalysis.py +18 -1
- {trailmark-0.2.2 → trailmark-0.3.1}/tests/test_readme.py +16 -5
- {trailmark-0.2.2 → trailmark-0.3.1}/tests/test_storage.py +39 -0
- {trailmark-0.2.2 → trailmark-0.3.1}/uv.lock +8 -8
- trailmark-0.2.2/src/trailmark/__init__.py +0 -3
- trailmark-0.2.2/tests/test_common_parser.py +0 -154
- {trailmark-0.2.2 → trailmark-0.3.1}/.github/dependabot.yml +0 -0
- {trailmark-0.2.2 → trailmark-0.3.1}/.github/workflows/semgrep.yml +0 -0
- {trailmark-0.2.2 → trailmark-0.3.1}/.github/workflows/zizmor.yml +0 -0
- {trailmark-0.2.2 → trailmark-0.3.1}/.gitignore +0 -0
- {trailmark-0.2.2 → trailmark-0.3.1}/AGENTS.md +0 -0
- {trailmark-0.2.2 → trailmark-0.3.1}/LICENSE +0 -0
- {trailmark-0.2.2 → trailmark-0.3.1}/NOTICE +0 -0
- {trailmark-0.2.2 → trailmark-0.3.1}/docs/entrypoint-patterns.md +0 -0
- {trailmark-0.2.2 → trailmark-0.3.1}/src/trailmark/analysis/__init__.py +0 -0
- {trailmark-0.2.2 → trailmark-0.3.1}/src/trailmark/analysis/augment.py +0 -0
- {trailmark-0.2.2 → trailmark-0.3.1}/src/trailmark/analysis/diff.py +0 -0
- {trailmark-0.2.2 → trailmark-0.3.1}/src/trailmark/analysis/entrypoints.py +0 -0
- {trailmark-0.2.2 → trailmark-0.3.1}/src/trailmark/cli.py +0 -0
- {trailmark-0.2.2 → trailmark-0.3.1}/src/trailmark/diagram.py +0 -0
- {trailmark-0.2.2 → trailmark-0.3.1}/src/trailmark/models/__init__.py +0 -0
- {trailmark-0.2.2 → trailmark-0.3.1}/src/trailmark/models/annotations.py +0 -0
- {trailmark-0.2.2 → trailmark-0.3.1}/src/trailmark/models/edges.py +0 -0
- {trailmark-0.2.2 → trailmark-0.3.1}/src/trailmark/models/graph.py +0 -0
- {trailmark-0.2.2 → trailmark-0.3.1}/src/trailmark/models/nodes.py +0 -0
- {trailmark-0.2.2 → trailmark-0.3.1}/src/trailmark/parsers/__init__.py +0 -0
- {trailmark-0.2.2 → trailmark-0.3.1}/src/trailmark/parsers/base.py +0 -0
- {trailmark-0.2.2 → trailmark-0.3.1}/src/trailmark/parsers/c/__init__.py +0 -0
- {trailmark-0.2.2 → trailmark-0.3.1}/src/trailmark/parsers/cairo/__init__.py +0 -0
- {trailmark-0.2.2 → trailmark-0.3.1}/src/trailmark/parsers/cairo/parser.py +0 -0
- {trailmark-0.2.2 → trailmark-0.3.1}/src/trailmark/parsers/circom/__init__.py +0 -0
- {trailmark-0.2.2 → trailmark-0.3.1}/src/trailmark/parsers/circom/parser.py +0 -0
- {trailmark-0.2.2 → trailmark-0.3.1}/src/trailmark/parsers/cpp/__init__.py +0 -0
- {trailmark-0.2.2 → trailmark-0.3.1}/src/trailmark/parsers/csharp/__init__.py +0 -0
- {trailmark-0.2.2 → trailmark-0.3.1}/src/trailmark/parsers/csharp/parser.py +0 -0
- {trailmark-0.2.2 → trailmark-0.3.1}/src/trailmark/parsers/dart/__init__.py +0 -0
- {trailmark-0.2.2 → trailmark-0.3.1}/src/trailmark/parsers/dart/parser.py +0 -0
- {trailmark-0.2.2 → trailmark-0.3.1}/src/trailmark/parsers/erlang/__init__.py +0 -0
- {trailmark-0.2.2 → trailmark-0.3.1}/src/trailmark/parsers/go/__init__.py +0 -0
- {trailmark-0.2.2 → trailmark-0.3.1}/src/trailmark/parsers/go/parser.py +0 -0
- {trailmark-0.2.2 → trailmark-0.3.1}/src/trailmark/parsers/haskell/__init__.py +0 -0
- {trailmark-0.2.2 → trailmark-0.3.1}/src/trailmark/parsers/haskell/parser.py +0 -0
- {trailmark-0.2.2 → trailmark-0.3.1}/src/trailmark/parsers/java/__init__.py +0 -0
- {trailmark-0.2.2 → trailmark-0.3.1}/src/trailmark/parsers/java/parser.py +0 -0
- {trailmark-0.2.2 → trailmark-0.3.1}/src/trailmark/parsers/javascript/__init__.py +0 -0
- {trailmark-0.2.2 → trailmark-0.3.1}/src/trailmark/parsers/javascript/parser.py +0 -0
- {trailmark-0.2.2 → trailmark-0.3.1}/src/trailmark/parsers/kotlin/__init__.py +0 -0
- {trailmark-0.2.2 → trailmark-0.3.1}/src/trailmark/parsers/kotlin/parser.py +0 -0
- {trailmark-0.2.2 → trailmark-0.3.1}/src/trailmark/parsers/masm/__init__.py +0 -0
- {trailmark-0.2.2 → trailmark-0.3.1}/src/trailmark/parsers/masm/parser.py +0 -0
- {trailmark-0.2.2 → trailmark-0.3.1}/src/trailmark/parsers/objc/__init__.py +0 -0
- {trailmark-0.2.2 → trailmark-0.3.1}/src/trailmark/parsers/objc/parser.py +0 -0
- {trailmark-0.2.2 → trailmark-0.3.1}/src/trailmark/parsers/php/__init__.py +0 -0
- {trailmark-0.2.2 → trailmark-0.3.1}/src/trailmark/parsers/php/parser.py +0 -0
- {trailmark-0.2.2 → trailmark-0.3.1}/src/trailmark/parsers/python/__init__.py +0 -0
- {trailmark-0.2.2 → trailmark-0.3.1}/src/trailmark/parsers/python/parser.py +0 -0
- {trailmark-0.2.2 → trailmark-0.3.1}/src/trailmark/parsers/ruby/__init__.py +0 -0
- {trailmark-0.2.2 → trailmark-0.3.1}/src/trailmark/parsers/ruby/parser.py +0 -0
- {trailmark-0.2.2 → trailmark-0.3.1}/src/trailmark/parsers/rust/__init__.py +0 -0
- {trailmark-0.2.2 → trailmark-0.3.1}/src/trailmark/parsers/rust/parser.py +0 -0
- {trailmark-0.2.2 → trailmark-0.3.1}/src/trailmark/parsers/solidity/__init__.py +0 -0
- {trailmark-0.2.2 → trailmark-0.3.1}/src/trailmark/parsers/solidity/parser.py +0 -0
- {trailmark-0.2.2 → trailmark-0.3.1}/src/trailmark/parsers/swift/__init__.py +0 -0
- {trailmark-0.2.2 → trailmark-0.3.1}/src/trailmark/parsers/swift/parser.py +0 -0
- {trailmark-0.2.2 → trailmark-0.3.1}/src/trailmark/parsers/typescript/__init__.py +0 -0
- {trailmark-0.2.2 → trailmark-0.3.1}/src/trailmark/parsers/typescript/parser.py +0 -0
- {trailmark-0.2.2 → trailmark-0.3.1}/src/trailmark/py.typed +0 -0
- {trailmark-0.2.2 → trailmark-0.3.1}/src/trailmark/query/__init__.py +0 -0
- {trailmark-0.2.2 → trailmark-0.3.1}/src/trailmark/storage/__init__.py +0 -0
- {trailmark-0.2.2 → trailmark-0.3.1}/src/trailmark/tree_sitter_custom/__init__.py +0 -0
- {trailmark-0.2.2 → trailmark-0.3.1}/src/trailmark/tree_sitter_custom/circom/LICENSE +0 -0
- {trailmark-0.2.2 → trailmark-0.3.1}/src/trailmark/tree_sitter_custom/circom/__init__.py +0 -0
- {trailmark-0.2.2 → trailmark-0.3.1}/src/trailmark/tree_sitter_custom/circom/binding.c +0 -0
- {trailmark-0.2.2 → trailmark-0.3.1}/src/trailmark/tree_sitter_custom/circom/grammar.js +0 -0
- {trailmark-0.2.2 → trailmark-0.3.1}/src/trailmark/tree_sitter_custom/circom/src/parser.c +0 -0
- {trailmark-0.2.2 → trailmark-0.3.1}/src/trailmark/tree_sitter_custom/circom/src/tree_sitter/parser.h +0 -0
- {trailmark-0.2.2 → trailmark-0.3.1}/src/trailmark/tree_sitter_custom/masm/LICENSE +0 -0
- {trailmark-0.2.2 → trailmark-0.3.1}/src/trailmark/tree_sitter_custom/masm/__init__.py +0 -0
- {trailmark-0.2.2 → trailmark-0.3.1}/src/trailmark/tree_sitter_custom/masm/binding.c +0 -0
- {trailmark-0.2.2 → trailmark-0.3.1}/src/trailmark/tree_sitter_custom/masm/grammar.js +0 -0
- {trailmark-0.2.2 → trailmark-0.3.1}/src/trailmark/tree_sitter_custom/masm/src/parser.c +0 -0
- {trailmark-0.2.2 → trailmark-0.3.1}/src/trailmark/tree_sitter_custom/masm/src/tree_sitter/alloc.h +0 -0
- {trailmark-0.2.2 → trailmark-0.3.1}/src/trailmark/tree_sitter_custom/masm/src/tree_sitter/array.h +0 -0
- {trailmark-0.2.2 → trailmark-0.3.1}/src/trailmark/tree_sitter_custom/masm/src/tree_sitter/parser.h +0 -0
- {trailmark-0.2.2 → trailmark-0.3.1}/tests/__init__.py +0 -0
- {trailmark-0.2.2 → trailmark-0.3.1}/tests/test_augment.py +0 -0
- {trailmark-0.2.2 → trailmark-0.3.1}/tests/test_cairo_parser.py +0 -0
- {trailmark-0.2.2 → trailmark-0.3.1}/tests/test_circom_grammar.py +0 -0
- {trailmark-0.2.2 → trailmark-0.3.1}/tests/test_circom_parser.py +0 -0
- {trailmark-0.2.2 → trailmark-0.3.1}/tests/test_cli.py +0 -0
- {trailmark-0.2.2 → trailmark-0.3.1}/tests/test_cli_parser.py +0 -0
- {trailmark-0.2.2 → trailmark-0.3.1}/tests/test_cpp_parser.py +0 -0
- {trailmark-0.2.2 → trailmark-0.3.1}/tests/test_csharp_parser.py +0 -0
- {trailmark-0.2.2 → trailmark-0.3.1}/tests/test_dart_parser.py +0 -0
- {trailmark-0.2.2 → trailmark-0.3.1}/tests/test_detector_tags.py +0 -0
- {trailmark-0.2.2 → trailmark-0.3.1}/tests/test_diagram_script.py +0 -0
- {trailmark-0.2.2 → trailmark-0.3.1}/tests/test_diff.py +0 -0
- {trailmark-0.2.2 → trailmark-0.3.1}/tests/test_entrypoints.py +0 -0
- {trailmark-0.2.2 → trailmark-0.3.1}/tests/test_erlang_parser.py +0 -0
- {trailmark-0.2.2 → trailmark-0.3.1}/tests/test_go_parser.py +0 -0
- {trailmark-0.2.2 → trailmark-0.3.1}/tests/test_haskell_parser.py +0 -0
- {trailmark-0.2.2 → trailmark-0.3.1}/tests/test_java_parser.py +0 -0
- {trailmark-0.2.2 → trailmark-0.3.1}/tests/test_javascript_parser.py +0 -0
- {trailmark-0.2.2 → trailmark-0.3.1}/tests/test_kotlin_parser.py +0 -0
- {trailmark-0.2.2 → trailmark-0.3.1}/tests/test_masm_parser.py +0 -0
- {trailmark-0.2.2 → trailmark-0.3.1}/tests/test_models.py +0 -0
- {trailmark-0.2.2 → trailmark-0.3.1}/tests/test_objc_parser.py +0 -0
- {trailmark-0.2.2 → trailmark-0.3.1}/tests/test_parser_edge_cases.py +0 -0
- {trailmark-0.2.2 → trailmark-0.3.1}/tests/test_php_parser.py +0 -0
- {trailmark-0.2.2 → trailmark-0.3.1}/tests/test_property_based.py +0 -0
- {trailmark-0.2.2 → trailmark-0.3.1}/tests/test_python_parser.py +0 -0
- {trailmark-0.2.2 → trailmark-0.3.1}/tests/test_query.py +0 -0
- {trailmark-0.2.2 → trailmark-0.3.1}/tests/test_query_edge_cases.py +0 -0
- {trailmark-0.2.2 → trailmark-0.3.1}/tests/test_ruby_parser.py +0 -0
- {trailmark-0.2.2 → trailmark-0.3.1}/tests/test_rust_parser.py +0 -0
- {trailmark-0.2.2 → trailmark-0.3.1}/tests/test_solidity_parser.py +0 -0
- {trailmark-0.2.2 → trailmark-0.3.1}/tests/test_storage_edge_cases.py +0 -0
- {trailmark-0.2.2 → trailmark-0.3.1}/tests/test_swift_parser.py +0 -0
- {trailmark-0.2.2 → trailmark-0.3.1}/tests/test_typescript_parser.py +0 -0
|
@@ -38,7 +38,7 @@ jobs:
|
|
|
38
38
|
with:
|
|
39
39
|
persist-credentials: false
|
|
40
40
|
|
|
41
|
-
- uses: astral-sh/setup-uv@
|
|
41
|
+
- uses: astral-sh/setup-uv@08807647e7069bb48b6ef5acd8ec9567f424441b # v8.1.0
|
|
42
42
|
|
|
43
43
|
- uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0
|
|
44
44
|
with:
|
|
@@ -59,7 +59,7 @@ jobs:
|
|
|
59
59
|
with:
|
|
60
60
|
persist-credentials: false
|
|
61
61
|
|
|
62
|
-
- uses: astral-sh/setup-uv@
|
|
62
|
+
- uses: astral-sh/setup-uv@08807647e7069bb48b6ef5acd8ec9567f424441b # v8.1.0
|
|
63
63
|
|
|
64
64
|
- uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0
|
|
65
65
|
with:
|
|
@@ -22,7 +22,7 @@ jobs:
|
|
|
22
22
|
with:
|
|
23
23
|
persist-credentials: false
|
|
24
24
|
|
|
25
|
-
- uses: astral-sh/setup-uv@
|
|
25
|
+
- uses: astral-sh/setup-uv@08807647e7069bb48b6ef5acd8ec9567f424441b # v8.1.0
|
|
26
26
|
|
|
27
27
|
- uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0
|
|
28
28
|
with:
|
|
@@ -17,7 +17,7 @@ jobs:
|
|
|
17
17
|
with:
|
|
18
18
|
persist-credentials: false
|
|
19
19
|
|
|
20
|
-
- uses: astral-sh/setup-uv@
|
|
20
|
+
- uses: astral-sh/setup-uv@08807647e7069bb48b6ef5acd8ec9567f424441b # v8.1.0
|
|
21
21
|
|
|
22
22
|
- uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0
|
|
23
23
|
with:
|
|
@@ -50,8 +50,12 @@ Linux (CI runs on Ubuntu without issue).
|
|
|
50
50
|
|
|
51
51
|
## Architecture
|
|
52
52
|
|
|
53
|
-
Trailmark has a three-layer
|
|
53
|
+
Trailmark has a parse layer followed by a three-layer analysis stack:
|
|
54
54
|
|
|
55
|
+
0. **Parse API** (`src/trailmark/parse.py`) -- public entry point for
|
|
56
|
+
parsing files or directories into a raw `CodeGraph`. Lazily imports
|
|
57
|
+
the appropriate language parser. Used directly for parse-only
|
|
58
|
+
workflows and internally by `QueryEngine`.
|
|
55
59
|
1. **CodeGraph** (`src/trailmark/models/graph.py`) -- mutable data
|
|
56
60
|
container holding nodes, edges, annotations, and entrypoints.
|
|
57
61
|
2. **GraphStore** (`src/trailmark/storage/graph_store.py`) -- wraps
|
|
@@ -75,6 +79,7 @@ Trailmark has a three-layer architecture:
|
|
|
75
79
|
|
|
76
80
|
1. Create `src/trailmark/parsers/<lang>/parser.py` implementing the
|
|
77
81
|
`BaseParser` protocol from `src/trailmark/parsers/base.py`.
|
|
78
|
-
2. Register the
|
|
82
|
+
2. Register the parser in `_PARSER_MAP` and its file extensions in
|
|
83
|
+
`_LANGUAGE_EXTENSIONS`, both in `src/trailmark/parse.py`.
|
|
79
84
|
3. Add tests in `tests/test_<lang>_parser.py`.
|
|
80
85
|
4. Update the language table in `README.md`.
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: trailmark
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.3.1
|
|
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
|
|
@@ -11,7 +11,7 @@ License-File: LICENSE
|
|
|
11
11
|
License-File: NOTICE
|
|
12
12
|
Requires-Python: >=3.12
|
|
13
13
|
Requires-Dist: rustworkx<1.0,>=0.17
|
|
14
|
-
Requires-Dist: tree-sitter-language-pack
|
|
14
|
+
Requires-Dist: tree-sitter-language-pack!=1.6.3,<1.7,>=0.13
|
|
15
15
|
Requires-Dist: tree-sitter<1.0,>=0.25
|
|
16
16
|
Description-Content-Type: text/markdown
|
|
17
17
|
|
|
@@ -107,7 +107,7 @@ flowchart TD
|
|
|
107
107
|
class CG data
|
|
108
108
|
```
|
|
109
109
|
|
|
110
|
-
Node IDs follow the scheme `module:function`, `module:Class`, or `module:Class.method` for unambiguous lookup. Edge confidence is tagged as `certain` (direct calls, `self.method()`), `inferred` (attribute access on non-self objects), or `uncertain` (dynamic dispatch).
|
|
110
|
+
Node IDs follow the scheme `module:function`, `module:Class`, or `module:Class.method` for unambiguous lookup. Directory parsing resolves bare cross-file calls when a unique definition exists; ambiguous cross-file calls are left at their original best-effort target and marked `uncertain`. Edge confidence is tagged as `certain` (direct calls, `self.method()`), `inferred` (attribute access on non-self objects), or `uncertain` (dynamic dispatch or ambiguous resolution).
|
|
111
111
|
|
|
112
112
|
### 2. Index
|
|
113
113
|
|
|
@@ -384,8 +384,13 @@ See [docs/entrypoint-patterns.md](docs/entrypoint-patterns.md) for the full refe
|
|
|
384
384
|
### Programmatic API
|
|
385
385
|
|
|
386
386
|
```python
|
|
387
|
+
from trailmark.parse import parse_directory, parse_file
|
|
387
388
|
from trailmark.query.api import QueryEngine
|
|
388
389
|
|
|
390
|
+
# Parse-only API: get the raw CodeGraph without building GraphStore/QueryEngine.
|
|
391
|
+
graph = parse_file("path/to/file.py")
|
|
392
|
+
graph = parse_directory("path/to/project", language="auto")
|
|
393
|
+
|
|
389
394
|
# Single-language (default) or auto-detect + merge across all languages
|
|
390
395
|
engine = QueryEngine.from_directory("path/to/project")
|
|
391
396
|
engine = QueryEngine.from_directory("path/to/project", language="auto")
|
|
@@ -90,7 +90,7 @@ flowchart TD
|
|
|
90
90
|
class CG data
|
|
91
91
|
```
|
|
92
92
|
|
|
93
|
-
Node IDs follow the scheme `module:function`, `module:Class`, or `module:Class.method` for unambiguous lookup. Edge confidence is tagged as `certain` (direct calls, `self.method()`), `inferred` (attribute access on non-self objects), or `uncertain` (dynamic dispatch).
|
|
93
|
+
Node IDs follow the scheme `module:function`, `module:Class`, or `module:Class.method` for unambiguous lookup. Directory parsing resolves bare cross-file calls when a unique definition exists; ambiguous cross-file calls are left at their original best-effort target and marked `uncertain`. Edge confidence is tagged as `certain` (direct calls, `self.method()`), `inferred` (attribute access on non-self objects), or `uncertain` (dynamic dispatch or ambiguous resolution).
|
|
94
94
|
|
|
95
95
|
### 2. Index
|
|
96
96
|
|
|
@@ -367,8 +367,13 @@ See [docs/entrypoint-patterns.md](docs/entrypoint-patterns.md) for the full refe
|
|
|
367
367
|
### Programmatic API
|
|
368
368
|
|
|
369
369
|
```python
|
|
370
|
+
from trailmark.parse import parse_directory, parse_file
|
|
370
371
|
from trailmark.query.api import QueryEngine
|
|
371
372
|
|
|
373
|
+
# Parse-only API: get the raw CodeGraph without building GraphStore/QueryEngine.
|
|
374
|
+
graph = parse_file("path/to/file.py")
|
|
375
|
+
graph = parse_directory("path/to/project", language="auto")
|
|
376
|
+
|
|
372
377
|
# Single-language (default) or auto-detect + merge across all languages
|
|
373
378
|
engine = QueryEngine.from_directory("path/to/project")
|
|
374
379
|
engine = QueryEngine.from_directory("path/to/project", language="auto")
|
|
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "trailmark"
|
|
7
|
-
version = "0.
|
|
7
|
+
version = "0.3.1"
|
|
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"
|
|
@@ -14,7 +14,7 @@ authors = [
|
|
|
14
14
|
]
|
|
15
15
|
dependencies = [
|
|
16
16
|
"tree-sitter>=0.25,<1.0",
|
|
17
|
-
"tree-sitter-language-pack>=0.13,<
|
|
17
|
+
"tree-sitter-language-pack>=0.13,<1.7,!=1.6.3",
|
|
18
18
|
"rustworkx>=0.17,<1.0",
|
|
19
19
|
]
|
|
20
20
|
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
"""Trailmark: Parse source code into queryable graphs for security analysis."""
|
|
2
|
+
|
|
3
|
+
from trailmark.parse import (
|
|
4
|
+
detect_languages,
|
|
5
|
+
parse_directory,
|
|
6
|
+
parse_file,
|
|
7
|
+
supported_languages,
|
|
8
|
+
)
|
|
9
|
+
|
|
10
|
+
__all__ = [
|
|
11
|
+
"detect_languages",
|
|
12
|
+
"parse_directory",
|
|
13
|
+
"parse_file",
|
|
14
|
+
"supported_languages",
|
|
15
|
+
]
|
|
16
|
+
__version__ = "0.3.1"
|
|
@@ -78,9 +78,9 @@ def _compute_blast_radius(store: GraphStore) -> dict[str, Any]:
|
|
|
78
78
|
Nodes above the threshold are collected in a subgraph.
|
|
79
79
|
"""
|
|
80
80
|
graph = store._graph # noqa: SLF001
|
|
81
|
-
digraph = store.
|
|
82
|
-
id_to_idx = store.
|
|
83
|
-
idx_to_id = store.
|
|
81
|
+
digraph = store._call_digraph # noqa: SLF001
|
|
82
|
+
id_to_idx = store._call_id_to_idx # noqa: SLF001
|
|
83
|
+
idx_to_id = store._call_idx_to_id # noqa: SLF001
|
|
84
84
|
|
|
85
85
|
high_blast: set[str] = set()
|
|
86
86
|
max_radius = 0
|
|
@@ -0,0 +1,216 @@
|
|
|
1
|
+
"""Public parse-only API for building raw Trailmark code graphs."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import importlib
|
|
6
|
+
import os
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
|
|
9
|
+
from trailmark.models.graph import CodeGraph
|
|
10
|
+
from trailmark.parsers._common import should_skip_dir
|
|
11
|
+
from trailmark.parsers.base import LanguageParser
|
|
12
|
+
|
|
13
|
+
_PARSER_MAP: dict[str, tuple[str, str]] = {
|
|
14
|
+
"python": ("trailmark.parsers.python", "PythonParser"),
|
|
15
|
+
"javascript": ("trailmark.parsers.javascript", "JavaScriptParser"),
|
|
16
|
+
"typescript": ("trailmark.parsers.typescript", "TypeScriptParser"),
|
|
17
|
+
"php": ("trailmark.parsers.php", "PHPParser"),
|
|
18
|
+
"ruby": ("trailmark.parsers.ruby", "RubyParser"),
|
|
19
|
+
"c": ("trailmark.parsers.c", "CParser"),
|
|
20
|
+
"cpp": ("trailmark.parsers.cpp", "CppParser"),
|
|
21
|
+
"c_sharp": ("trailmark.parsers.csharp", "CSharpParser"),
|
|
22
|
+
"java": ("trailmark.parsers.java", "JavaParser"),
|
|
23
|
+
"go": ("trailmark.parsers.go", "GoParser"),
|
|
24
|
+
"rust": ("trailmark.parsers.rust", "RustParser"),
|
|
25
|
+
"solidity": ("trailmark.parsers.solidity", "SolidityParser"),
|
|
26
|
+
"cairo": ("trailmark.parsers.cairo", "CairoParser"),
|
|
27
|
+
"circom": ("trailmark.parsers.circom", "CircomParser"),
|
|
28
|
+
"haskell": ("trailmark.parsers.haskell", "HaskellParser"),
|
|
29
|
+
"erlang": ("trailmark.parsers.erlang", "ErlangParser"),
|
|
30
|
+
"masm": ("trailmark.parsers.masm", "MasmParser"),
|
|
31
|
+
"swift": ("trailmark.parsers.swift", "SwiftParser"),
|
|
32
|
+
"objc": ("trailmark.parsers.objc", "ObjCParser"),
|
|
33
|
+
"kotlin": ("trailmark.parsers.kotlin", "KotlinParser"),
|
|
34
|
+
"dart": ("trailmark.parsers.dart", "DartParser"),
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
# Extensions used for language auto-detection. Keep these aligned with each
|
|
38
|
+
# parser's internal _EXTENSIONS tuple. Shared extensions (e.g., `.h` between C
|
|
39
|
+
# and C++) are handled by prioritizing the more specific language — C++ is
|
|
40
|
+
# tried before plain C when both report files.
|
|
41
|
+
_LANGUAGE_EXTENSIONS: dict[str, tuple[str, ...]] = {
|
|
42
|
+
"python": (".py",),
|
|
43
|
+
"javascript": (".js", ".jsx", ".mjs", ".cjs"),
|
|
44
|
+
"typescript": (".ts", ".tsx"),
|
|
45
|
+
"php": (".php",),
|
|
46
|
+
"ruby": (".rb",),
|
|
47
|
+
"c": (".c",),
|
|
48
|
+
"cpp": (".cpp", ".cc", ".cxx", ".hpp", ".hh", ".hxx"),
|
|
49
|
+
"c_sharp": (".cs",),
|
|
50
|
+
"java": (".java",),
|
|
51
|
+
"go": (".go",),
|
|
52
|
+
"rust": (".rs",),
|
|
53
|
+
"solidity": (".sol",),
|
|
54
|
+
"cairo": (".cairo",),
|
|
55
|
+
"circom": (".circom",),
|
|
56
|
+
"haskell": (".hs",),
|
|
57
|
+
"erlang": (".erl",),
|
|
58
|
+
"masm": (".masm",),
|
|
59
|
+
"swift": (".swift",),
|
|
60
|
+
# `.h` files can be C, ObjC, or C++; auto-detect only fires on .m/.mm.
|
|
61
|
+
# The parser itself still walks .h when invoked with language="objc".
|
|
62
|
+
"objc": (".m", ".mm"),
|
|
63
|
+
"kotlin": (".kt", ".kts"),
|
|
64
|
+
"dart": (".dart",),
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
_SUPPORTED_LANGUAGES = tuple(_PARSER_MAP.keys())
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
def supported_languages() -> tuple[str, ...]:
|
|
71
|
+
"""Return the supported Trailmark parser language names."""
|
|
72
|
+
return _SUPPORTED_LANGUAGES
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
def parse_file(path: str, language: str | None = None) -> CodeGraph:
|
|
76
|
+
"""Parse a single file into a raw ``CodeGraph``.
|
|
77
|
+
|
|
78
|
+
Args:
|
|
79
|
+
path: Source file to parse.
|
|
80
|
+
language: Optional explicit parser language. If omitted, language is
|
|
81
|
+
inferred from the file extension.
|
|
82
|
+
|
|
83
|
+
Raises:
|
|
84
|
+
ValueError: If the language is unsupported or cannot be inferred.
|
|
85
|
+
"""
|
|
86
|
+
file_path = Path(path)
|
|
87
|
+
resolved_language = _resolve_file_language(file_path, language)
|
|
88
|
+
return _get_parser(resolved_language).parse_file(str(file_path))
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
def parse_directory(path: str, language: str = "python") -> CodeGraph:
|
|
92
|
+
"""Parse a directory into a raw ``CodeGraph``.
|
|
93
|
+
|
|
94
|
+
``language`` accepts a specific language name (e.g. ``"python"``),
|
|
95
|
+
``"auto"`` to detect and merge all supported languages found under the
|
|
96
|
+
directory, or a comma-separated list like ``"python,rust"``.
|
|
97
|
+
"""
|
|
98
|
+
languages = _resolve_directory_languages(path, language)
|
|
99
|
+
return _parse_and_merge(path, languages)
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
def detect_languages(path: str) -> list[str]:
|
|
103
|
+
"""Return the sorted languages with at least one file under ``path``.
|
|
104
|
+
|
|
105
|
+
Detection walks the directory once, classifies each file by extension,
|
|
106
|
+
and returns the languages that have at least one match. Order is the
|
|
107
|
+
order languages are registered in ``_LANGUAGE_EXTENSIONS``, which
|
|
108
|
+
roughly corresponds to popularity and keeps deterministic behavior.
|
|
109
|
+
"""
|
|
110
|
+
root = Path(path)
|
|
111
|
+
if not root.exists():
|
|
112
|
+
return []
|
|
113
|
+
|
|
114
|
+
ext_to_language: dict[str, str] = {}
|
|
115
|
+
for lang, exts in _LANGUAGE_EXTENSIONS.items():
|
|
116
|
+
for ext in exts:
|
|
117
|
+
# When languages share an extension (none currently do, but
|
|
118
|
+
# guard against it), the FIRST registration wins.
|
|
119
|
+
ext_to_language.setdefault(ext, lang)
|
|
120
|
+
|
|
121
|
+
found: set[str] = set()
|
|
122
|
+
for _dirpath, dirs, files in os.walk(root):
|
|
123
|
+
# Keep detection aligned with the parser walk rules.
|
|
124
|
+
dirs[:] = [d for d in dirs if not should_skip_dir(d)]
|
|
125
|
+
for name in files:
|
|
126
|
+
ext = _file_extension(name)
|
|
127
|
+
if ext in ext_to_language:
|
|
128
|
+
found.add(ext_to_language[ext])
|
|
129
|
+
if len(found) == len(_LANGUAGE_EXTENSIONS):
|
|
130
|
+
break
|
|
131
|
+
|
|
132
|
+
return [lang for lang in _LANGUAGE_EXTENSIONS if lang in found]
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
def _get_parser(language: str) -> LanguageParser:
|
|
136
|
+
"""Lazily import and instantiate a parser for the given language."""
|
|
137
|
+
entry = _PARSER_MAP.get(language)
|
|
138
|
+
if entry is None:
|
|
139
|
+
msg = f"Unsupported language: {language}"
|
|
140
|
+
raise ValueError(msg)
|
|
141
|
+
module = importlib.import_module(entry[0])
|
|
142
|
+
cls = getattr(module, entry[1])
|
|
143
|
+
return cls()
|
|
144
|
+
|
|
145
|
+
|
|
146
|
+
def _resolve_file_language(path: Path, language: str | None) -> str:
|
|
147
|
+
"""Resolve a single-file parse request to one concrete language."""
|
|
148
|
+
if language is None or language == "auto":
|
|
149
|
+
detected = _detect_file_language(path)
|
|
150
|
+
if detected is None:
|
|
151
|
+
msg = f"Could not infer supported language from file extension: {path}"
|
|
152
|
+
raise ValueError(msg)
|
|
153
|
+
return detected
|
|
154
|
+
if "," in language:
|
|
155
|
+
msg = f"Single-file parse requires one language, got: {language}"
|
|
156
|
+
raise ValueError(msg)
|
|
157
|
+
if language not in _PARSER_MAP:
|
|
158
|
+
msg = f"Unsupported language: {language}"
|
|
159
|
+
raise ValueError(msg)
|
|
160
|
+
return language
|
|
161
|
+
|
|
162
|
+
|
|
163
|
+
def _resolve_directory_languages(path: str, spec: str) -> list[str]:
|
|
164
|
+
"""Expand a directory ``language`` argument into concrete languages."""
|
|
165
|
+
if spec == "auto":
|
|
166
|
+
detected = detect_languages(path)
|
|
167
|
+
if not detected:
|
|
168
|
+
msg = f"No supported languages detected under {path}"
|
|
169
|
+
raise ValueError(msg)
|
|
170
|
+
return detected
|
|
171
|
+
names = [name.strip() for name in spec.split(",") if name.strip()] if "," in spec else [spec]
|
|
172
|
+
for name in names:
|
|
173
|
+
if name not in _PARSER_MAP:
|
|
174
|
+
msg = f"Unsupported language: {name}"
|
|
175
|
+
raise ValueError(msg)
|
|
176
|
+
return names
|
|
177
|
+
|
|
178
|
+
|
|
179
|
+
def _parse_and_merge(path: str, languages: list[str]) -> CodeGraph:
|
|
180
|
+
"""Parse ``path`` with each language parser and merge into one graph."""
|
|
181
|
+
if len(languages) == 1:
|
|
182
|
+
# Preserves pre-polyglot behavior exactly for the common case.
|
|
183
|
+
return _get_parser(languages[0]).parse_directory(path)
|
|
184
|
+
|
|
185
|
+
merged = CodeGraph(language="polyglot", root_path=str(Path(path).resolve()))
|
|
186
|
+
for lang in languages:
|
|
187
|
+
sub = _get_parser(lang).parse_directory(path)
|
|
188
|
+
merged.merge(sub)
|
|
189
|
+
# merge() doesn't touch `language`; preserve the polyglot marker.
|
|
190
|
+
merged.language = "polyglot"
|
|
191
|
+
return merged
|
|
192
|
+
|
|
193
|
+
|
|
194
|
+
def _detect_file_language(path: Path) -> str | None:
|
|
195
|
+
"""Infer one supported language from a file extension."""
|
|
196
|
+
ext = _file_extension(path.name)
|
|
197
|
+
for language, exts in _LANGUAGE_EXTENSIONS.items():
|
|
198
|
+
if ext in exts:
|
|
199
|
+
return language
|
|
200
|
+
return None
|
|
201
|
+
|
|
202
|
+
|
|
203
|
+
def _file_extension(name: str) -> str:
|
|
204
|
+
"""Return the lowercase extension including leading dot, or ``''``."""
|
|
205
|
+
dot = name.rfind(".")
|
|
206
|
+
if dot < 0:
|
|
207
|
+
return ""
|
|
208
|
+
return name[dot:].lower()
|
|
209
|
+
|
|
210
|
+
|
|
211
|
+
__all__ = [
|
|
212
|
+
"detect_languages",
|
|
213
|
+
"parse_directory",
|
|
214
|
+
"parse_file",
|
|
215
|
+
"supported_languages",
|
|
216
|
+
]
|
|
@@ -2,13 +2,14 @@
|
|
|
2
2
|
|
|
3
3
|
from __future__ import annotations
|
|
4
4
|
|
|
5
|
+
import dataclasses
|
|
5
6
|
import os
|
|
6
7
|
from collections.abc import Callable
|
|
7
8
|
from contextvars import ContextVar
|
|
8
9
|
from pathlib import Path
|
|
9
10
|
from typing import TYPE_CHECKING
|
|
10
11
|
|
|
11
|
-
from trailmark.models.edges import CodeEdge, EdgeKind
|
|
12
|
+
from trailmark.models.edges import CodeEdge, EdgeConfidence, EdgeKind
|
|
12
13
|
from trailmark.models.graph import CodeGraph
|
|
13
14
|
from trailmark.models.nodes import (
|
|
14
15
|
BranchInfo,
|
|
@@ -104,6 +105,11 @@ _EXCLUDED_DIRS = frozenset(
|
|
|
104
105
|
)
|
|
105
106
|
|
|
106
107
|
|
|
108
|
+
def should_skip_dir(dirname: str) -> bool:
|
|
109
|
+
"""Return whether a directory should be skipped during source walks."""
|
|
110
|
+
return dirname in _EXCLUDED_DIRS or dirname.startswith(".")
|
|
111
|
+
|
|
112
|
+
|
|
107
113
|
def walk_source_files(
|
|
108
114
|
dir_path: str,
|
|
109
115
|
extensions: tuple[str, ...],
|
|
@@ -114,7 +120,8 @@ def walk_source_files(
|
|
|
114
120
|
and does not follow symlinks.
|
|
115
121
|
"""
|
|
116
122
|
for root, dirs, files in os.walk(dir_path, followlinks=False):
|
|
117
|
-
dirs[:] = [d for d in dirs if
|
|
123
|
+
dirs[:] = [d for d in dirs if not should_skip_dir(d)]
|
|
124
|
+
dirs.sort()
|
|
118
125
|
for fname in sorted(files):
|
|
119
126
|
if any(fname.endswith(ext) for ext in extensions):
|
|
120
127
|
yield os.path.join(root, fname)
|
|
@@ -142,6 +149,7 @@ def parse_directory(
|
|
|
142
149
|
merged.merge(file_graph)
|
|
143
150
|
finally:
|
|
144
151
|
_DIRECTORY_PARSE_ROOT.reset(token)
|
|
152
|
+
_link_cross_file_calls(merged)
|
|
145
153
|
return merged
|
|
146
154
|
|
|
147
155
|
|
|
@@ -291,3 +299,60 @@ def first_child_by_type(node: Node, type_name: str) -> Node | None:
|
|
|
291
299
|
if child.type == type_name:
|
|
292
300
|
return child
|
|
293
301
|
return None
|
|
302
|
+
|
|
303
|
+
|
|
304
|
+
def _link_cross_file_calls(graph: CodeGraph) -> None:
|
|
305
|
+
"""Rewrite dangling call edges to point at definitions in other modules.
|
|
306
|
+
|
|
307
|
+
Per-file parsers resolve bare calls like ``foo()`` as
|
|
308
|
+
``current_module:foo``. After merging, if ``foo`` is defined in
|
|
309
|
+
another module, that edge target doesn't exist. This pass builds a
|
|
310
|
+
name index and rewrites those edges to the actual definition site.
|
|
311
|
+
"""
|
|
312
|
+
defined_nodes = graph.nodes
|
|
313
|
+
|
|
314
|
+
name_to_ids: dict[str, list[str]] = {}
|
|
315
|
+
for node_id, unit in defined_nodes.items():
|
|
316
|
+
if unit.kind.value in {"function", "method"}:
|
|
317
|
+
name_to_ids.setdefault(unit.name, []).append(node_id)
|
|
318
|
+
|
|
319
|
+
new_edges: list[CodeEdge] = []
|
|
320
|
+
for edge in graph.edges:
|
|
321
|
+
if edge.kind != EdgeKind.CALLS or edge.target_id in defined_nodes:
|
|
322
|
+
new_edges.append(edge)
|
|
323
|
+
continue
|
|
324
|
+
|
|
325
|
+
target = edge.target_id
|
|
326
|
+
if "::" in target or "->" in target:
|
|
327
|
+
new_edges.append(edge)
|
|
328
|
+
continue
|
|
329
|
+
|
|
330
|
+
bare_name = target.rsplit(":", 1)[-1] if ":" in target else target
|
|
331
|
+
if "." in bare_name or "->" in bare_name:
|
|
332
|
+
new_edges.append(edge)
|
|
333
|
+
continue
|
|
334
|
+
candidates = name_to_ids.get(bare_name)
|
|
335
|
+
|
|
336
|
+
if not candidates:
|
|
337
|
+
new_edges.append(edge)
|
|
338
|
+
continue
|
|
339
|
+
|
|
340
|
+
if len(candidates) == 1:
|
|
341
|
+
new_edges.append(dataclasses.replace(edge, target_id=candidates[0]))
|
|
342
|
+
else:
|
|
343
|
+
# Ambiguous: prefer a single cross-file candidate when a same-file
|
|
344
|
+
# declaration is also present, but do not invent an arbitrary call
|
|
345
|
+
# target when multiple cross-file definitions share the same name.
|
|
346
|
+
src_module = edge.source_id.rsplit(":", 1)[0] if ":" in edge.source_id else ""
|
|
347
|
+
cross = [c for c in candidates if not c.startswith(src_module + ":")]
|
|
348
|
+
if len(cross) == 1:
|
|
349
|
+
new_edges.append(dataclasses.replace(edge, target_id=cross[0]))
|
|
350
|
+
else:
|
|
351
|
+
new_edges.append(
|
|
352
|
+
dataclasses.replace(
|
|
353
|
+
edge,
|
|
354
|
+
confidence=EdgeConfidence.UNCERTAIN,
|
|
355
|
+
)
|
|
356
|
+
)
|
|
357
|
+
|
|
358
|
+
graph.edges = new_edges
|
|
@@ -92,6 +92,17 @@ def _visit_module(
|
|
|
92
92
|
_visit_top_level_node(child, file_path, module_id, graph)
|
|
93
93
|
|
|
94
94
|
|
|
95
|
+
_PREPROC_CONTAINER_TYPES = frozenset(
|
|
96
|
+
{
|
|
97
|
+
"preproc_if",
|
|
98
|
+
"preproc_ifdef",
|
|
99
|
+
"preproc_else",
|
|
100
|
+
"preproc_elif",
|
|
101
|
+
"ERROR",
|
|
102
|
+
}
|
|
103
|
+
)
|
|
104
|
+
|
|
105
|
+
|
|
95
106
|
def _visit_top_level_node(
|
|
96
107
|
child: Node,
|
|
97
108
|
file_path: str,
|
|
@@ -109,6 +120,9 @@ def _visit_top_level_node(
|
|
|
109
120
|
_extract_struct(child, file_path, module_id, graph)
|
|
110
121
|
elif child.type == "enum_specifier":
|
|
111
122
|
_extract_enum(child, file_path, module_id, graph)
|
|
123
|
+
elif child.type in _PREPROC_CONTAINER_TYPES:
|
|
124
|
+
for nested in child.children:
|
|
125
|
+
_visit_top_level_node(nested, file_path, module_id, graph)
|
|
112
126
|
|
|
113
127
|
|
|
114
128
|
def _extract_type_def(
|
|
@@ -127,9 +127,14 @@ def _visit_top_level_node(
|
|
|
127
127
|
module_id,
|
|
128
128
|
graph,
|
|
129
129
|
)
|
|
130
|
-
elif child.type
|
|
131
|
-
|
|
132
|
-
|
|
130
|
+
elif child.type in (
|
|
131
|
+
"linkage_specification",
|
|
132
|
+
"preproc_if",
|
|
133
|
+
"preproc_ifdef",
|
|
134
|
+
"preproc_else",
|
|
135
|
+
"preproc_elif",
|
|
136
|
+
"ERROR",
|
|
137
|
+
):
|
|
133
138
|
for nested in child.children:
|
|
134
139
|
_visit_top_level_node(nested, file_path, module_id, class_id, graph)
|
|
135
140
|
elif child.type == "class_specifier":
|
|
@@ -405,14 +405,37 @@ def _walk_calls(
|
|
|
405
405
|
calls: list[tuple[str, Node]],
|
|
406
406
|
) -> None:
|
|
407
407
|
"""Recursively find call nodes and extract targets."""
|
|
408
|
+
if node.type == "remote":
|
|
409
|
+
name = _remote_call_name(node)
|
|
410
|
+
if name:
|
|
411
|
+
calls.append((name, node))
|
|
412
|
+
_walk_call_arguments(node.child_by_field_name("fun"), calls)
|
|
413
|
+
return
|
|
414
|
+
|
|
408
415
|
if node.type == "call":
|
|
409
416
|
name = _call_target_name(node)
|
|
410
417
|
if name:
|
|
411
418
|
calls.append((name, node))
|
|
419
|
+
_walk_call_arguments(node, calls)
|
|
420
|
+
return
|
|
421
|
+
|
|
412
422
|
for child in node.children:
|
|
413
423
|
_walk_calls(child, calls)
|
|
414
424
|
|
|
415
425
|
|
|
426
|
+
def _walk_call_arguments(
|
|
427
|
+
node: Node | None,
|
|
428
|
+
calls: list[tuple[str, Node]],
|
|
429
|
+
) -> None:
|
|
430
|
+
"""Walk a call's argument expressions without re-visiting its target."""
|
|
431
|
+
if node is None:
|
|
432
|
+
return
|
|
433
|
+
for i, child in enumerate(node.children):
|
|
434
|
+
if node.field_name_for_child(i) == "expr":
|
|
435
|
+
continue
|
|
436
|
+
_walk_calls(child, calls)
|
|
437
|
+
|
|
438
|
+
|
|
416
439
|
def _call_target_name(node: Node) -> str:
|
|
417
440
|
"""Extract the function name from a call node."""
|
|
418
441
|
expr = node.child_by_field_name("expr")
|
|
@@ -433,8 +456,8 @@ def _remote_call_name(remote: Node) -> str:
|
|
|
433
456
|
return ""
|
|
434
457
|
# remote_module has a module field containing the atom.
|
|
435
458
|
mod_atom = mod_node.child_by_field_name("module")
|
|
436
|
-
mod_name = node_text(mod_atom) if mod_atom is not None else ""
|
|
437
|
-
fun_name = node_text(fun_node)
|
|
459
|
+
mod_name = node_text(mod_atom) if mod_atom is not None else node_text(mod_node).rstrip(":")
|
|
460
|
+
fun_name = _call_target_name(fun_node) if fun_node.type == "call" else node_text(fun_node)
|
|
438
461
|
if mod_name and fun_name:
|
|
439
462
|
return f"{mod_name}:{fun_name}"
|
|
440
463
|
return ""
|