sphinx-codelinks 1.2.0__tar.gz → 1.4.0__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.
- {sphinx_codelinks-1.2.0 → sphinx_codelinks-1.4.0}/PKG-INFO +10 -5
- {sphinx_codelinks-1.2.0 → sphinx_codelinks-1.4.0}/pyproject.toml +25 -71
- {sphinx_codelinks-1.2.0 → sphinx_codelinks-1.4.0}/src/sphinx_codelinks/analyse/analyse.py +164 -26
- {sphinx_codelinks-1.2.0 → sphinx_codelinks-1.4.0}/src/sphinx_codelinks/analyse/oneline_parser.py +13 -9
- sphinx_codelinks-1.4.0/src/sphinx_codelinks/analyse/preproc/__init__.py +5 -0
- sphinx_codelinks-1.4.0/src/sphinx_codelinks/analyse/preproc/compile_db.py +156 -0
- sphinx_codelinks-1.4.0/src/sphinx_codelinks/analyse/preproc/libclang_parser.py +102 -0
- sphinx_codelinks-1.4.0/src/sphinx_codelinks/analyse/preproc/loader.py +95 -0
- {sphinx_codelinks-1.2.0 → sphinx_codelinks-1.4.0}/src/sphinx_codelinks/analyse/projects.py +4 -10
- {sphinx_codelinks-1.2.0 → sphinx_codelinks-1.4.0}/src/sphinx_codelinks/analyse/utils.py +128 -22
- {sphinx_codelinks-1.2.0 → sphinx_codelinks-1.4.0}/src/sphinx_codelinks/cmd.py +20 -4
- {sphinx_codelinks-1.2.0 → sphinx_codelinks-1.4.0}/src/sphinx_codelinks/config.py +138 -2
- sphinx_codelinks-1.4.0/src/sphinx_codelinks/logger.py +261 -0
- {sphinx_codelinks-1.2.0 → sphinx_codelinks-1.4.0}/src/sphinx_codelinks/source_discover/config.py +19 -0
- sphinx_codelinks-1.4.0/src/sphinx_codelinks/source_discover/source_discover.py +112 -0
- {sphinx_codelinks-1.2.0 → sphinx_codelinks-1.4.0}/src/sphinx_codelinks/sphinx_extension/directives/src_trace.py +40 -20
- {sphinx_codelinks-1.2.0 → sphinx_codelinks-1.4.0}/src/sphinx_codelinks/sphinx_extension/source_tracing.py +42 -5
- sphinx_codelinks-1.2.0/src/sphinx_codelinks/logger.py +0 -94
- sphinx_codelinks-1.2.0/src/sphinx_codelinks/source_discover/source_discover.py +0 -67
- {sphinx_codelinks-1.2.0 → sphinx_codelinks-1.4.0}/LICENSE +0 -0
- {sphinx_codelinks-1.2.0 → sphinx_codelinks-1.4.0}/README.md +0 -0
- {sphinx_codelinks-1.2.0 → sphinx_codelinks-1.4.0}/src/sphinx_codelinks/__init__.py +0 -0
- {sphinx_codelinks-1.2.0 → sphinx_codelinks-1.4.0}/src/sphinx_codelinks/analyse/models.py +0 -0
- {sphinx_codelinks-1.2.0 → sphinx_codelinks-1.4.0}/src/sphinx_codelinks/needextend_write.py +0 -0
- {sphinx_codelinks-1.2.0 → sphinx_codelinks-1.4.0}/src/sphinx_codelinks/sphinx_extension/debug.py +0 -0
- {sphinx_codelinks-1.2.0 → sphinx_codelinks-1.4.0}/src/sphinx_codelinks/sphinx_extension/html_wrapper.py +0 -0
- {sphinx_codelinks-1.2.0 → sphinx_codelinks-1.4.0}/src/sphinx_codelinks/sphinx_extension/ub_sct.css +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: sphinx-codelinks
|
|
3
|
-
Version: 1.
|
|
3
|
+
Version: 1.4.0
|
|
4
4
|
Summary: Fast Source Code Traceability for Sphinx-Needs
|
|
5
5
|
Author-email: team useblocks <info@useblocks.com>
|
|
6
6
|
Maintainer-email: Marco Heinemann <marco.heinemann@useblocks.com>, Jui-Wen Chen <jui-wen.chen@useblocks.com>
|
|
@@ -8,12 +8,12 @@ Requires-Python: >= 3.12
|
|
|
8
8
|
Description-Content-Type: text/markdown
|
|
9
9
|
License-File: LICENSE
|
|
10
10
|
Requires-Dist: comment-parser>=1.2.4
|
|
11
|
-
Requires-Dist:
|
|
12
|
-
Requires-Dist: typer>=0.16.0
|
|
11
|
+
Requires-Dist: ignore-python>=0.3.3
|
|
12
|
+
Requires-Dist: typer>=0.16.0,<0.26.8
|
|
13
13
|
Requires-Dist: click < 8.2
|
|
14
14
|
Requires-Dist: jsonschema
|
|
15
|
-
Requires-Dist: sphinx>=7.4,<
|
|
16
|
-
Requires-Dist: sphinx-needs>=
|
|
15
|
+
Requires-Dist: sphinx>=7.4,<10
|
|
16
|
+
Requires-Dist: sphinx-needs>=5,<9
|
|
17
17
|
Requires-Dist: jinja2
|
|
18
18
|
Requires-Dist: pygments
|
|
19
19
|
Requires-Dist: docutils
|
|
@@ -24,6 +24,11 @@ Requires-Dist: tree-sitter~=0.25.1
|
|
|
24
24
|
Requires-Dist: tree-sitter-c-sharp>=0.23.1
|
|
25
25
|
Requires-Dist: tree-sitter-yaml>=0.7.1
|
|
26
26
|
Requires-Dist: tree-sitter-rust>=0.23.0
|
|
27
|
+
Requires-Dist: tree-sitter-go>=0.23.0
|
|
28
|
+
Requires-Dist: tree-sitter-json>=0.24.8
|
|
29
|
+
Requires-Dist: tree-sitter-bash>=0.25.1
|
|
30
|
+
Requires-Dist: libclang>=16 ; extra == "libclang"
|
|
31
|
+
Provides-Extra: libclang
|
|
27
32
|
|
|
28
33
|
# Sphinx CodeLinks
|
|
29
34
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "sphinx-codelinks"
|
|
3
|
-
version = "1.
|
|
3
|
+
version = "1.4.0"
|
|
4
4
|
description = "Fast Source Code Traceability for Sphinx-Needs"
|
|
5
5
|
authors = [{ name = "team useblocks", email = "info@useblocks.com" }]
|
|
6
6
|
maintainers = [
|
|
@@ -12,12 +12,12 @@ readme = "README.md"
|
|
|
12
12
|
requires-python = ">= 3.12"
|
|
13
13
|
dependencies = [
|
|
14
14
|
"comment-parser>=1.2.4",
|
|
15
|
-
"
|
|
16
|
-
"typer>=0.16.0",
|
|
15
|
+
"ignore-python>=0.3.3",
|
|
16
|
+
"typer>=0.16.0,<0.26.8", # 0.26.8 removed rich_utils.STYLE_METAVAR, still imported by sphinxcontrib-typer (docs)
|
|
17
17
|
"click < 8.2", # click 8.2.* produces empty errors if no args are given
|
|
18
18
|
"jsonschema",
|
|
19
|
-
"sphinx>=7.4,<
|
|
20
|
-
"sphinx-needs>=
|
|
19
|
+
"sphinx>=7.4,<10",
|
|
20
|
+
"sphinx-needs>=5,<9",
|
|
21
21
|
"jinja2",
|
|
22
22
|
"pygments",
|
|
23
23
|
"docutils", # constrained by user or Sphinx
|
|
@@ -30,8 +30,21 @@ dependencies = [
|
|
|
30
30
|
"tree-sitter-c-sharp>=0.23.1",
|
|
31
31
|
"tree-sitter-yaml>=0.7.1",
|
|
32
32
|
"tree-sitter-rust>=0.23.0",
|
|
33
|
+
"tree-sitter-go>=0.23.0",
|
|
34
|
+
"tree-sitter-json>=0.24.8",
|
|
35
|
+
"tree-sitter-bash>=0.25.1",
|
|
33
36
|
]
|
|
34
37
|
|
|
38
|
+
[project.optional-dependencies]
|
|
39
|
+
# libclang enables the preprocessor-aware C/C++ engine (skips comments inside
|
|
40
|
+
# inactive #if/#ifdef regions). Plain tree-sitter C/C++ extraction needs none of
|
|
41
|
+
# this, so libclang stays optional. Named after the backend, not "cpp": the
|
|
42
|
+
# engine handles C and Objective-C too, and tree-sitter already covers C++.
|
|
43
|
+
# Floor 16 (not 18): the extraction snapshots are byte-identical on libclang 16,
|
|
44
|
+
# 17 and 18 (the engine only uses get_all_skipped_ranges + comment tokenization,
|
|
45
|
+
# stable since long before 16), so support older toolchains too. No upper bound.
|
|
46
|
+
libclang = ["libclang>=16"]
|
|
47
|
+
|
|
35
48
|
[build-system]
|
|
36
49
|
requires = ["flit_core >=3.4,<4"]
|
|
37
50
|
build-backend = "flit_core.buildapi"
|
|
@@ -46,13 +59,14 @@ testing = [
|
|
|
46
59
|
"moto ~= 5.0",
|
|
47
60
|
"toml>=0.10.2",
|
|
48
61
|
"furo>=2024.5.6",
|
|
62
|
+
"libclang>=18",
|
|
49
63
|
]
|
|
50
64
|
docs = [
|
|
51
65
|
"furo>=2024.5.6",
|
|
52
66
|
"myst-parser>=4.0.0",
|
|
53
67
|
"sphinx-design>=0.6.1",
|
|
54
68
|
"sphinx-code-tabs>=0.5.5",
|
|
55
|
-
"sphinxcontrib-typer>=0.5.1",
|
|
69
|
+
"sphinxcontrib-typer>=0.5.1,<0.9.1", # 0.9.1 imports typer.rich_utils.STYLE_TYPES, absent in the capped typer <0.26.8, breaking the docs build
|
|
56
70
|
"sphinxcontrib-video>=0.4.1",
|
|
57
71
|
]
|
|
58
72
|
mypy = [
|
|
@@ -66,64 +80,9 @@ mypy = [
|
|
|
66
80
|
ruff = ["ruff>=0.8.0"]
|
|
67
81
|
build = ["simple-build>=0.0.2", "shiv>=1.0.8"]
|
|
68
82
|
|
|
69
|
-
[tool.rye]
|
|
70
|
-
managed = true
|
|
71
|
-
dev-dependencies = [
|
|
72
|
-
"types-docutils",
|
|
73
|
-
"types-Pygments",
|
|
74
|
-
"syrupy>=4.9.1",
|
|
75
|
-
"furo>=2024.5.6",
|
|
76
|
-
"moto ~= 5.0",
|
|
77
|
-
"mypy>=1.12.1",
|
|
78
|
-
"myst-parser>=4.0.0",
|
|
79
|
-
"pydantic ~= 2.9",
|
|
80
|
-
"pip-licenses>=5.0.0",
|
|
81
|
-
"psutil>=7.0.0",
|
|
82
|
-
"pytest-cov>=5.0.0",
|
|
83
|
-
"pytest>=8.2.2",
|
|
84
|
-
"simple-build>=0.0.2",
|
|
85
|
-
"sphinx-design>=0.6.1",
|
|
86
|
-
"types-psutil>=7.0.0.20250218",
|
|
87
|
-
"uv>=0.5.5",
|
|
88
|
-
"pytest-docker>=3.1.2",
|
|
89
|
-
"shiv>=1.0.8",
|
|
90
|
-
"insta-science>=0.2.1",
|
|
91
|
-
"types-jsonschema>=4.23.0.20241208",
|
|
92
|
-
"toml>=0.10.2",
|
|
93
|
-
"sphinx-code-tabs>=0.5.5",
|
|
94
|
-
"sphinxcontrib-typer>=0.5.1",
|
|
95
|
-
"sphinxcontrib-video>=0.4.1",
|
|
96
|
-
]
|
|
97
|
-
|
|
98
83
|
[project.scripts]
|
|
99
84
|
codelinks = "sphinx_codelinks.cmd:app"
|
|
100
85
|
|
|
101
|
-
[tool.rye.scripts]
|
|
102
|
-
# linting and formatting
|
|
103
|
-
"mypy:all" = "mypy ."
|
|
104
|
-
"rye:lint" = "rye lint"
|
|
105
|
-
"rye:format" = "rye format"
|
|
106
|
-
"check" = { chain = ["rye:format", "rye:lint", "mypy:all"] }
|
|
107
|
-
# docs html
|
|
108
|
-
"docs:rm" = "rm -rf docs/_build/html"
|
|
109
|
-
"docs" = "sphinx-build -nW --keep-going -b html -T -c docs docs/source docs/_build/html"
|
|
110
|
-
"docs:clean" = { chain = ["docs:rm", "docs"] }
|
|
111
|
-
# needextend demo
|
|
112
|
-
"analyse" = "codelinks analyse tests/data/configs/minimum_config.toml"
|
|
113
|
-
"analyse:rm" = "rm -rf output/marked_content.json"
|
|
114
|
-
"write" = "codelinks write rst output/marked_content.json --outpath tests/data/needextend_demo/needextend.rst"
|
|
115
|
-
"write:rm" = "rm -rf tests/data/needextend_demo/needextend.rst"
|
|
116
|
-
"demo:rm" = "rm -rf tests/data/needextend_demo/_build"
|
|
117
|
-
"demo:build" = "sphinx-build -nW --keep-going -b html -T -c tests/data/needextend_demo tests/data/needextend_demo tests/data/needextend_demo/_build/html"
|
|
118
|
-
"demo:clean" = { chain = [
|
|
119
|
-
"demo:rm",
|
|
120
|
-
"analyse:rm",
|
|
121
|
-
"write:rm",
|
|
122
|
-
"analyse",
|
|
123
|
-
"write",
|
|
124
|
-
"demo:build",
|
|
125
|
-
] }
|
|
126
|
-
|
|
127
86
|
[tool.ruff.lint]
|
|
128
87
|
extend-select = [
|
|
129
88
|
# "ANN",
|
|
@@ -159,17 +118,11 @@ force-sort-within-sections = true
|
|
|
159
118
|
"**/tests/*" = [
|
|
160
119
|
"ARG001", # unused-function-argument - fixtures
|
|
161
120
|
"ARG005", # unused-lambda-argument - monkeypatches
|
|
121
|
+
"E402", # module-import-not-at-top - pytest.importorskip guard pattern
|
|
122
|
+
"PLC0415", # import-outside-top-level - inline imports in test bodies
|
|
162
123
|
"PLR2004", # magic-value-comparison - valueable for tests
|
|
163
124
|
"S101", # assert - needed for tests
|
|
164
|
-
|
|
165
|
-
"**/build_hooks_*/**" = [
|
|
166
|
-
"S607", # start-process-with-partial-path - pyarmor call in rye context
|
|
167
|
-
"S603", # subprocess-without-shell-equals-true - pyarmor call
|
|
168
|
-
]
|
|
169
|
-
"scripts/*.py" = [
|
|
170
|
-
"T201", # print - used for output
|
|
171
|
-
"S607", # start-process-with-partial-path - pyarmor call in rye context
|
|
172
|
-
"S603", # subprocess-without-shell-equals-true - build scripts
|
|
125
|
+
"SIM300", # yoda-conditions - assert order in tests is intentional
|
|
173
126
|
]
|
|
174
127
|
"src/sphinx_codelinks/sphinx_extension/debug.py" = [
|
|
175
128
|
"T201", # print - used for output
|
|
@@ -202,7 +155,8 @@ plugins = ["pydantic.mypy"]
|
|
|
202
155
|
mypy_path = "typings"
|
|
203
156
|
|
|
204
157
|
[[tool.mypy.overrides]]
|
|
205
|
-
|
|
158
|
+
# clang.cindex ships no type stubs / py.typed; the libclang engine wraps it.
|
|
159
|
+
module = ["licensing.*", "tomlkit.*", "clang.*"]
|
|
206
160
|
ignore_missing_imports = true
|
|
207
161
|
|
|
208
162
|
[[tool.mypy.overrides]]
|
|
@@ -1,9 +1,8 @@
|
|
|
1
1
|
from collections.abc import Generator
|
|
2
2
|
from dataclasses import dataclass
|
|
3
3
|
import json
|
|
4
|
-
import logging
|
|
5
4
|
from pathlib import Path
|
|
6
|
-
from typing import Any, TypedDict
|
|
5
|
+
from typing import Any, TypedDict, cast
|
|
7
6
|
|
|
8
7
|
from tree_sitter import Node as TreeSitterNode
|
|
9
8
|
|
|
@@ -26,14 +25,15 @@ from sphinx_codelinks.config import (
|
|
|
26
25
|
OneLineCommentStyle,
|
|
27
26
|
SourceAnalyseConfig,
|
|
28
27
|
)
|
|
28
|
+
from sphinx_codelinks.logger import get_logger
|
|
29
|
+
from sphinx_codelinks.source_discover.config import CommentType
|
|
29
30
|
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
logger.addHandler(console)
|
|
31
|
+
logger = get_logger(__name__)
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def _count(n: int, noun: str) -> str:
|
|
35
|
+
"""Format ``n noun`` with a naive (append-s) plural for progress summaries."""
|
|
36
|
+
return f"{n} {noun}" if n == 1 else f"{n} {noun}s"
|
|
37
37
|
|
|
38
38
|
|
|
39
39
|
class AnalyseWarningType(TypedDict):
|
|
@@ -57,7 +57,10 @@ class SourceAnalyse:
|
|
|
57
57
|
def __init__(
|
|
58
58
|
self,
|
|
59
59
|
analyse_config: SourceAnalyseConfig,
|
|
60
|
+
*,
|
|
61
|
+
name: str = "",
|
|
60
62
|
) -> None:
|
|
63
|
+
self.name = name
|
|
61
64
|
self.analyse_config = analyse_config
|
|
62
65
|
self.src_files: list[SourceFile] = []
|
|
63
66
|
self.src_comments: list[SourceComment] = []
|
|
@@ -76,10 +79,11 @@ class SourceAnalyse:
|
|
|
76
79
|
self.git_commit_rev: str | None = (
|
|
77
80
|
utils.get_current_rev(self.git_root) if self.git_root else None
|
|
78
81
|
)
|
|
79
|
-
self.project_path: Path =
|
|
80
|
-
self.git_root if self.git_root else self.analyse_config.src_dir
|
|
81
|
-
)
|
|
82
|
+
self.project_path: Path = self.git_root or self.analyse_config.src_dir
|
|
82
83
|
self.oneline_warnings: list[AnalyseWarning] = []
|
|
84
|
+
# Per-run memo of parsed compile_commands.json, keyed by DB path, so the
|
|
85
|
+
# database is read once per run instead of once per source file.
|
|
86
|
+
self._flags_map_cache: dict[Path, dict[Path, list[str]] | None] = {}
|
|
83
87
|
|
|
84
88
|
def get_src_strings(self) -> Generator[tuple[Path, bytes], Any, None]: # type: ignore[explicit-any]
|
|
85
89
|
"""Load source files and extract their content."""
|
|
@@ -110,8 +114,126 @@ class SourceAnalyse:
|
|
|
110
114
|
self.src_files.append(src_file)
|
|
111
115
|
self.src_comments.extend(src_comments)
|
|
112
116
|
|
|
113
|
-
|
|
114
|
-
|
|
117
|
+
def _flags_map_for(self, db_path: Path) -> dict[Path, list[str]] | None:
|
|
118
|
+
"""Return the parsed compile_commands.json for ``db_path``, memoized.
|
|
119
|
+
|
|
120
|
+
The database is read and parsed once per run instead of once per source
|
|
121
|
+
file (O(files x entries) -> O(entries)). A present-but-malformed or
|
|
122
|
+
unreadable database is warned once and cached as ``None`` so callers fall
|
|
123
|
+
back to the configured defines without re-reading or re-warning per file.
|
|
124
|
+
"""
|
|
125
|
+
from sphinx_codelinks.analyse.preproc import compile_db # noqa: PLC0415
|
|
126
|
+
|
|
127
|
+
if db_path in self._flags_map_cache:
|
|
128
|
+
return self._flags_map_cache[db_path]
|
|
129
|
+
try:
|
|
130
|
+
flags = compile_db.load_flags_map(db_path)
|
|
131
|
+
except (OSError, ValueError, TypeError) as exc:
|
|
132
|
+
logger.warning(
|
|
133
|
+
f"codelinks: failed to read {db_path} ({exc}); "
|
|
134
|
+
f"falling back to the configured defines"
|
|
135
|
+
)
|
|
136
|
+
self._flags_map_cache[db_path] = None
|
|
137
|
+
return None
|
|
138
|
+
self._flags_map_cache[db_path] = flags
|
|
139
|
+
return flags
|
|
140
|
+
|
|
141
|
+
def _resolve_preproc_args(self, src_path: Path) -> list[str] | None:
|
|
142
|
+
from sphinx_codelinks.analyse.preproc import compile_db # noqa: PLC0415
|
|
143
|
+
|
|
144
|
+
# `run()` calls this (via create_src_objects_libclang) only when
|
|
145
|
+
# `preprocessor is not None`, but keep the guard: it narrows the type for
|
|
146
|
+
# the checker and is a cheap defense (an `assert` would trip bandit S101).
|
|
147
|
+
preproc = self.analyse_config.preprocessor
|
|
148
|
+
if preproc is None:
|
|
149
|
+
return []
|
|
150
|
+
db_path = preproc.compile_commands
|
|
151
|
+
if db_path is None:
|
|
152
|
+
db_path = compile_db.find_compile_db(src_path, self.project_path)
|
|
153
|
+
if db_path is not None and db_path.is_file():
|
|
154
|
+
flags = self._flags_map_for(db_path)
|
|
155
|
+
if flags is None:
|
|
156
|
+
# Present-but-malformed/unreadable DB (warned once in the helper):
|
|
157
|
+
# fall back to the configured defines so extraction still runs.
|
|
158
|
+
return compile_db.defines_to_args(
|
|
159
|
+
preproc.defines, preproc.includes, preproc.std
|
|
160
|
+
)
|
|
161
|
+
args = flags.get(src_path.absolute().resolve())
|
|
162
|
+
if args is not None:
|
|
163
|
+
return args
|
|
164
|
+
# Absent from the DB. compile_commands.json lists only compiled
|
|
165
|
+
# translation units, never headers — so a header here is parsed
|
|
166
|
+
# standalone with the global defines (one run = one variant). A
|
|
167
|
+
# compiled source absent from the build is skipped (spec §3.3).
|
|
168
|
+
if compile_db.is_translation_unit_source(src_path):
|
|
169
|
+
return None
|
|
170
|
+
return compile_db.defines_to_args(
|
|
171
|
+
preproc.defines, preproc.includes, preproc.std
|
|
172
|
+
)
|
|
173
|
+
# No readable DB. `find_compile_db` only ever returns a real file, so a
|
|
174
|
+
# non-None `db_path` that reaches here is an explicit `compile_commands`
|
|
175
|
+
# path that is not a readable file (typo/missing): warn and fall back
|
|
176
|
+
# rather than skip silently. A genuinely absent DB (db_path is None) just
|
|
177
|
+
# falls back to the manual defines applied globally.
|
|
178
|
+
if db_path is not None:
|
|
179
|
+
logger.warning(
|
|
180
|
+
f"codelinks: compile_commands path {db_path} is not a readable "
|
|
181
|
+
f"file; falling back to the configured defines"
|
|
182
|
+
)
|
|
183
|
+
return compile_db.defines_to_args(
|
|
184
|
+
preproc.defines, preproc.includes, preproc.std
|
|
185
|
+
)
|
|
186
|
+
|
|
187
|
+
# @Extract traceability objects with the preprocessor-aware libclang engine, IMPL_PREPROC_1, impl, [FE_PREPROC]
|
|
188
|
+
def create_src_objects_libclang(self) -> None:
|
|
189
|
+
from sphinx_codelinks.analyse.preproc import ( # noqa: PLC0415
|
|
190
|
+
libclang_parser,
|
|
191
|
+
loader,
|
|
192
|
+
)
|
|
193
|
+
|
|
194
|
+
# Resolve the exception via the loader (never a direct ``import
|
|
195
|
+
# clang.cindex``) so a missing ``libclang`` extra still surfaces the
|
|
196
|
+
# loader's friendly install hint rather than a bare ImportError.
|
|
197
|
+
translation_unit_load_error = (
|
|
198
|
+
loader.load_clang_cindex().TranslationUnitLoadError
|
|
199
|
+
)
|
|
200
|
+
|
|
201
|
+
for src_path in self.analyse_config.src_files:
|
|
202
|
+
if not utils.is_text_file(src_path):
|
|
203
|
+
continue
|
|
204
|
+
args = self._resolve_preproc_args(src_path)
|
|
205
|
+
if args is None:
|
|
206
|
+
logger.debug(
|
|
207
|
+
f"codelinks: skipping {src_path} — not found in compile_commands.json"
|
|
208
|
+
)
|
|
209
|
+
continue
|
|
210
|
+
try:
|
|
211
|
+
comments = libclang_parser.extract_active_comments(src_path, args)
|
|
212
|
+
except translation_unit_load_error:
|
|
213
|
+
# Last-resort guard. Standalone parses pin ``-x`` to match the
|
|
214
|
+
# ``-std`` (see defines_to_args), so the common case — a ``.h``/
|
|
215
|
+
# ``.c`` header handed a C++ ``-std`` — now parses as C++ and its
|
|
216
|
+
# markers extract. This only fires if libclang still cannot load
|
|
217
|
+
# the file as a translation unit at all; skip it rather than
|
|
218
|
+
# aborting the whole run. Surfaced as a ``warning`` so the silent
|
|
219
|
+
# data-loss (a file's markers dropped) is visible — accepting that
|
|
220
|
+
# this fails ``sphinx-build -W``.
|
|
221
|
+
logger.warning(
|
|
222
|
+
f"codelinks: skipping {src_path} — libclang could not load it "
|
|
223
|
+
f"as a translation unit"
|
|
224
|
+
)
|
|
225
|
+
continue
|
|
226
|
+
if not comments:
|
|
227
|
+
continue
|
|
228
|
+
# ``c`` is a LibclangComment duck-typing the tree-sitter Node
|
|
229
|
+
# interface SourceComment reads (``.text`` / ``.start_point.row``);
|
|
230
|
+
# the Node-only path (find_associated_scope) is guarded by
|
|
231
|
+
# ``is_libclang`` so it never runs on these.
|
|
232
|
+
src_comments = [SourceComment(cast("TreeSitterNode", c)) for c in comments]
|
|
233
|
+
src_file = SourceFile(src_path.absolute())
|
|
234
|
+
src_file.add_comments(src_comments)
|
|
235
|
+
self.src_files.append(src_file)
|
|
236
|
+
self.src_comments.extend(src_comments)
|
|
115
237
|
|
|
116
238
|
def extract_marker(
|
|
117
239
|
self,
|
|
@@ -328,9 +450,12 @@ class SourceAnalyse:
|
|
|
328
450
|
)
|
|
329
451
|
if not filepath:
|
|
330
452
|
continue
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
453
|
+
if getattr(src_comment.node, "is_libclang", False):
|
|
454
|
+
tagged_scope: TreeSitterNode | None = None
|
|
455
|
+
else:
|
|
456
|
+
tagged_scope = utils.find_associated_scope(
|
|
457
|
+
src_comment.node, self.analyse_config.comment_type
|
|
458
|
+
)
|
|
334
459
|
if self.analyse_config.get_need_id_refs:
|
|
335
460
|
anchors = self.extract_anchors(
|
|
336
461
|
text, filepath, tagged_scope, src_comment
|
|
@@ -353,13 +478,6 @@ class SourceAnalyse:
|
|
|
353
478
|
if marked_rst:
|
|
354
479
|
self.marked_rst.append(marked_rst)
|
|
355
480
|
|
|
356
|
-
if self.analyse_config.get_need_id_refs:
|
|
357
|
-
logger.info(f"Need-id-refs extracted: {len(self.need_id_refs)}")
|
|
358
|
-
if self.analyse_config.get_oneline_needs:
|
|
359
|
-
logger.info(f"Oneline needs extracted: {len(self.oneline_needs)}")
|
|
360
|
-
if self.analyse_config.get_rst:
|
|
361
|
-
logger.info(f"Marked rst extracted: {len(self.marked_rst)}")
|
|
362
|
-
|
|
363
481
|
def merge_marked_content(self) -> None:
|
|
364
482
|
self.all_marked_content.extend(self.need_id_refs)
|
|
365
483
|
self.oneline_needs.sort(key=lambda x: x.source_map["start"]["row"])
|
|
@@ -378,9 +496,29 @@ class SourceAnalyse:
|
|
|
378
496
|
]
|
|
379
497
|
with output_path.open("w") as f:
|
|
380
498
|
json.dump(to_dump, f)
|
|
381
|
-
logger.info(f"Marked content dumped to {output_path}")
|
|
382
499
|
|
|
383
500
|
def run(self) -> None:
|
|
384
|
-
|
|
501
|
+
if (
|
|
502
|
+
self.analyse_config.preprocessor is not None
|
|
503
|
+
and self.analyse_config.comment_type == CommentType.cpp
|
|
504
|
+
):
|
|
505
|
+
self.create_src_objects_libclang()
|
|
506
|
+
else:
|
|
507
|
+
self.create_src_objects()
|
|
385
508
|
self.extract_marked_content()
|
|
386
509
|
self.merge_marked_content()
|
|
510
|
+
self._log_summary()
|
|
511
|
+
|
|
512
|
+
def _log_summary(self) -> None:
|
|
513
|
+
"""Emit a per-project marker (default-visible) plus a -v breakdown."""
|
|
514
|
+
label = f"codelinks [{self.name}]" if self.name else "codelinks"
|
|
515
|
+
logger.info(
|
|
516
|
+
f"{label}: {_count(len(self.src_files), 'file')}, "
|
|
517
|
+
f"{_count(len(self.all_marked_content), 'marker')}"
|
|
518
|
+
)
|
|
519
|
+
logger.debug(
|
|
520
|
+
f"{label}: {_count(len(self.src_comments), 'comment')}, "
|
|
521
|
+
f"{_count(len(self.oneline_needs), 'oneline need')}, "
|
|
522
|
+
f"{_count(len(self.need_id_refs), 'id-ref')}, "
|
|
523
|
+
f"{_count(len(self.marked_rst), 'marked-rst block')}"
|
|
524
|
+
)
|
{sphinx_codelinks-1.2.0 → sphinx_codelinks-1.4.0}/src/sphinx_codelinks/analyse/oneline_parser.py
RENAMED
|
@@ -1,17 +1,8 @@
|
|
|
1
1
|
from dataclasses import dataclass
|
|
2
2
|
from enum import Enum
|
|
3
|
-
import logging
|
|
4
3
|
|
|
5
4
|
from sphinx_codelinks.config import ESCAPE, UNIX_NEWLINE, OneLineCommentStyle
|
|
6
5
|
|
|
7
|
-
# initialize logger
|
|
8
|
-
logger = logging.getLogger(__name__)
|
|
9
|
-
logger.setLevel(logging.INFO)
|
|
10
|
-
# log to the console
|
|
11
|
-
console = logging.StreamHandler()
|
|
12
|
-
console.setLevel(logging.INFO)
|
|
13
|
-
logger.addHandler(console)
|
|
14
|
-
|
|
15
6
|
|
|
16
7
|
class WarningSubTypeEnum(str, Enum):
|
|
17
8
|
"""Enum for warning sub types."""
|
|
@@ -52,6 +43,19 @@ def oneline_parser( # noqa: PLR0912, PLR0911 # handel warnings
|
|
|
52
43
|
# start or end sequences do not exist
|
|
53
44
|
return None
|
|
54
45
|
|
|
46
|
+
# A marker whose end sequence is the newline extends to the end of the
|
|
47
|
+
# line, so an unanchored start sequence would swallow trailing prose. Anchor
|
|
48
|
+
# such a marker to the start of the comment content: everything preceding
|
|
49
|
+
# the start sequence must be comment decoration (`//`, `#`, `*`, ...) and
|
|
50
|
+
# whitespace. A word character before it means the start sequence is part of
|
|
51
|
+
# free-form prose (e.g. `// see @author, ...`) and the line is ignored
|
|
52
|
+
# (issue #88). Explicitly-bounded markers (e.g. `[[ ... ]]`) are
|
|
53
|
+
# self-delimiting and may appear anywhere, so they are exempt.
|
|
54
|
+
if oneline_config.end_sequence == UNIX_NEWLINE and any(
|
|
55
|
+
char.isalnum() for char in oneline[:start_idx]
|
|
56
|
+
):
|
|
57
|
+
return None
|
|
58
|
+
|
|
55
59
|
# extract the string wrapped by start and end
|
|
56
60
|
start_idx = start_idx + len(oneline_config.start_sequence)
|
|
57
61
|
string = oneline[start_idx:end_idx].strip()
|
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
"""Discover, read, and filter compile_commands.json for libclang."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import json
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
import shlex
|
|
8
|
+
|
|
9
|
+
from sphinx_codelinks.config import DEFAULT_CPP_STD
|
|
10
|
+
|
|
11
|
+
_DB_NAME = "compile_commands.json"
|
|
12
|
+
_BOUNDARY_MARKERS = (".git", "ubproject.toml", "pyproject.toml")
|
|
13
|
+
|
|
14
|
+
# Flags that take a following value we must also drop.
|
|
15
|
+
_DROP_WITH_VALUE = {"-o", "-MF", "-MT", "-MQ"}
|
|
16
|
+
# Exact flags to drop.
|
|
17
|
+
_DROP_EXACT = {"-c", "-MMD", "-MD", "-MG", "-MP"}
|
|
18
|
+
# Separate-form flags whose following VALUE token must be kept verbatim (never
|
|
19
|
+
# treated as the input source and stripped by basename).
|
|
20
|
+
_KEEP_WITH_VALUE = {
|
|
21
|
+
"-include",
|
|
22
|
+
"-isystem",
|
|
23
|
+
"-iquote",
|
|
24
|
+
"-idirafter",
|
|
25
|
+
"-isysroot",
|
|
26
|
+
"-x",
|
|
27
|
+
"-I",
|
|
28
|
+
}
|
|
29
|
+
# Minimum length for joined-form flags like -MFdep.d (prefix length = 3)
|
|
30
|
+
_MIN_JOINED_FLAG_LEN = 3
|
|
31
|
+
|
|
32
|
+
# Suffixes a compiler builds as a translation unit. A discovered C/C++ file
|
|
33
|
+
# absent from compile_commands.json is skipped only when it is a TU source
|
|
34
|
+
# (build-excluded); all other discovered files (headers) are parsed standalone.
|
|
35
|
+
TU_SOURCE_SUFFIXES = {".c", ".cpp", ".cc", ".cxx"}
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
# @Discover compile_commands.json by walking up from the source file, IMPL_PREPROC_3, impl, [FE_PREPROC]
|
|
39
|
+
def find_compile_db(start: Path, project_root: Path | None = None) -> Path | None:
|
|
40
|
+
"""Walk up from ``start`` looking for compile_commands.json.
|
|
41
|
+
|
|
42
|
+
Stops at (inclusive) the directory that contains the db, or at
|
|
43
|
+
``project_root`` / a directory containing a boundary marker / fs root.
|
|
44
|
+
"""
|
|
45
|
+
current = start if start.is_dir() else start.parent
|
|
46
|
+
current = current.resolve()
|
|
47
|
+
root = project_root.resolve() if project_root else None
|
|
48
|
+
while True:
|
|
49
|
+
candidate = current / _DB_NAME
|
|
50
|
+
if candidate.is_file():
|
|
51
|
+
return candidate
|
|
52
|
+
if root is not None and current == root:
|
|
53
|
+
return None
|
|
54
|
+
if any((current / m).exists() for m in _BOUNDARY_MARKERS):
|
|
55
|
+
return None
|
|
56
|
+
if current.parent == current: # filesystem root
|
|
57
|
+
return None
|
|
58
|
+
current = current.parent
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def filter_args(argv: list[str], input_file: str) -> list[str]:
|
|
62
|
+
"""Keep only flags libclang needs; drop the compiler, -c/-o, depfiles, input."""
|
|
63
|
+
out: list[str] = []
|
|
64
|
+
skip_next = False
|
|
65
|
+
keep_next_value = False
|
|
66
|
+
input_base = Path(input_file).name
|
|
67
|
+
# Skip leading non-flag tokens. argv[0] is the compiler, but a build may prefix
|
|
68
|
+
# it with a launcher (ccache/sccache/distcc), so drop every leading non-flag
|
|
69
|
+
# token up to the first flag — otherwise the real compiler leaks in as a
|
|
70
|
+
# positional and libclang treats it as a second input (a NULL TU that silently
|
|
71
|
+
# drops the file). The input source (also a non-flag positional) is stripped by
|
|
72
|
+
# name below.
|
|
73
|
+
in_leading = True
|
|
74
|
+
for arg in argv:
|
|
75
|
+
if in_leading:
|
|
76
|
+
if not arg.startswith("-"):
|
|
77
|
+
continue
|
|
78
|
+
in_leading = False
|
|
79
|
+
if skip_next:
|
|
80
|
+
skip_next = False
|
|
81
|
+
continue
|
|
82
|
+
if keep_next_value:
|
|
83
|
+
# Value of a separate-form -include/-isystem/-I/... — keep verbatim.
|
|
84
|
+
keep_next_value = False
|
|
85
|
+
out.append(arg)
|
|
86
|
+
continue
|
|
87
|
+
if arg in _DROP_WITH_VALUE:
|
|
88
|
+
skip_next = True
|
|
89
|
+
continue
|
|
90
|
+
if arg in _DROP_EXACT:
|
|
91
|
+
continue
|
|
92
|
+
if arg.startswith(("-MF", "-MT", "-MQ")) and len(arg) > _MIN_JOINED_FLAG_LEN:
|
|
93
|
+
continue # joined form, e.g. -MFdep.d
|
|
94
|
+
if arg in _KEEP_WITH_VALUE:
|
|
95
|
+
out.append(arg)
|
|
96
|
+
keep_next_value = True
|
|
97
|
+
continue
|
|
98
|
+
# Strip the TU source positional (exact match, or same basename).
|
|
99
|
+
if not arg.startswith("-") and (
|
|
100
|
+
arg in (input_file, input_base) or Path(arg).name == input_base
|
|
101
|
+
):
|
|
102
|
+
continue
|
|
103
|
+
out.append(arg)
|
|
104
|
+
return out
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
# @Resolve per-file compiler flags from the compilation database, IMPL_PREPROC_4, impl, [FE_PREPROC]
|
|
108
|
+
def load_flags_map(db_path: Path) -> dict[Path, list[str]]:
|
|
109
|
+
"""Parse compile_commands.json -> {absolute file path: filtered args}."""
|
|
110
|
+
entries = json.loads(db_path.read_text())
|
|
111
|
+
flags: dict[Path, list[str]] = {}
|
|
112
|
+
for entry in entries:
|
|
113
|
+
if "file" not in entry or "directory" not in entry:
|
|
114
|
+
continue # malformed entry: skip, keep going
|
|
115
|
+
if "arguments" in entry:
|
|
116
|
+
argv = list(entry["arguments"])
|
|
117
|
+
elif "command" in entry:
|
|
118
|
+
argv = shlex.split(entry["command"])
|
|
119
|
+
else:
|
|
120
|
+
continue
|
|
121
|
+
directory = Path(entry["directory"])
|
|
122
|
+
file_field = entry["file"]
|
|
123
|
+
abs_file = (directory / file_field).resolve()
|
|
124
|
+
flags[abs_file] = filter_args(argv, file_field)
|
|
125
|
+
return flags
|
|
126
|
+
|
|
127
|
+
|
|
128
|
+
# @Parse headers standalone from configured defines and includes, IMPL_PREPROC_5, impl, [FE_PREPROC]
|
|
129
|
+
def defines_to_args(
|
|
130
|
+
defines: list[str], includes: list[Path], std: str = DEFAULT_CPP_STD
|
|
131
|
+
) -> list[str]:
|
|
132
|
+
"""Build a global flag list for parsing a file standalone.
|
|
133
|
+
|
|
134
|
+
Used for headers and files absent from a compile DB. libclang infers the
|
|
135
|
+
language from the file extension, so a C-inferred file (``.h`` / ``.c`` /
|
|
136
|
+
``.inc``) handed a C++ ``-std`` makes clang reject the combination and return
|
|
137
|
+
a NULL translation unit (surfacing as ``TranslationUnitLoadError``). Pin the
|
|
138
|
+
language to match ``std`` with ``-x`` so such files — e.g. a ``.h`` header
|
|
139
|
+
carrying oneline need markers — parse and extract instead of failing. (C
|
|
140
|
+
sources parse as C++ well enough for comment/marker extraction.)
|
|
141
|
+
"""
|
|
142
|
+
lang = "c++" if std.startswith(("c++", "gnu++")) else "c"
|
|
143
|
+
args = ["-x", lang, f"-std={std}"]
|
|
144
|
+
args += [f"-D{d}" for d in defines]
|
|
145
|
+
args += [f"-I{inc}" for inc in includes]
|
|
146
|
+
return args
|
|
147
|
+
|
|
148
|
+
|
|
149
|
+
def is_translation_unit_source(path: Path) -> bool:
|
|
150
|
+
"""True if ``path`` is a compiled translation-unit source (not a header).
|
|
151
|
+
|
|
152
|
+
compile_commands.json lists one entry per compiled TU; headers are never
|
|
153
|
+
entries. So a discovered file absent from the DB is skipped only when it is
|
|
154
|
+
a TU source; header-like files are parsed standalone (see _resolve_preproc_args).
|
|
155
|
+
"""
|
|
156
|
+
return path.suffix.lower() in TU_SOURCE_SUFFIXES
|