sphinx-codelinks 1.2.0__tar.gz → 1.3.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.3.0}/PKG-INFO +6 -4
- {sphinx_codelinks-1.2.0 → sphinx_codelinks-1.3.0}/pyproject.toml +6 -68
- {sphinx_codelinks-1.2.0 → sphinx_codelinks-1.3.0}/src/sphinx_codelinks/analyse/analyse.py +25 -19
- {sphinx_codelinks-1.2.0 → sphinx_codelinks-1.3.0}/src/sphinx_codelinks/analyse/oneline_parser.py +0 -9
- {sphinx_codelinks-1.2.0 → sphinx_codelinks-1.3.0}/src/sphinx_codelinks/analyse/projects.py +4 -10
- {sphinx_codelinks-1.2.0 → sphinx_codelinks-1.3.0}/src/sphinx_codelinks/analyse/utils.py +119 -22
- {sphinx_codelinks-1.2.0 → sphinx_codelinks-1.3.0}/src/sphinx_codelinks/cmd.py +12 -4
- {sphinx_codelinks-1.2.0 → sphinx_codelinks-1.3.0}/src/sphinx_codelinks/config.py +20 -0
- sphinx_codelinks-1.3.0/src/sphinx_codelinks/logger.py +261 -0
- {sphinx_codelinks-1.2.0 → sphinx_codelinks-1.3.0}/src/sphinx_codelinks/source_discover/config.py +11 -0
- sphinx_codelinks-1.3.0/src/sphinx_codelinks/source_discover/source_discover.py +112 -0
- {sphinx_codelinks-1.2.0 → sphinx_codelinks-1.3.0}/src/sphinx_codelinks/sphinx_extension/directives/src_trace.py +21 -16
- {sphinx_codelinks-1.2.0 → sphinx_codelinks-1.3.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.3.0}/LICENSE +0 -0
- {sphinx_codelinks-1.2.0 → sphinx_codelinks-1.3.0}/README.md +0 -0
- {sphinx_codelinks-1.2.0 → sphinx_codelinks-1.3.0}/src/sphinx_codelinks/__init__.py +0 -0
- {sphinx_codelinks-1.2.0 → sphinx_codelinks-1.3.0}/src/sphinx_codelinks/analyse/models.py +0 -0
- {sphinx_codelinks-1.2.0 → sphinx_codelinks-1.3.0}/src/sphinx_codelinks/needextend_write.py +0 -0
- {sphinx_codelinks-1.2.0 → sphinx_codelinks-1.3.0}/src/sphinx_codelinks/sphinx_extension/debug.py +0 -0
- {sphinx_codelinks-1.2.0 → sphinx_codelinks-1.3.0}/src/sphinx_codelinks/sphinx_extension/html_wrapper.py +0 -0
- {sphinx_codelinks-1.2.0 → sphinx_codelinks-1.3.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.3.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:
|
|
11
|
+
Requires-Dist: ignore-python>=0.3.3
|
|
12
12
|
Requires-Dist: typer>=0.16.0
|
|
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,8 @@ 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
|
|
27
29
|
|
|
28
30
|
# Sphinx CodeLinks
|
|
29
31
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "sphinx-codelinks"
|
|
3
|
-
version = "1.
|
|
3
|
+
version = "1.3.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
|
-
"
|
|
15
|
+
"ignore-python>=0.3.3",
|
|
16
16
|
"typer>=0.16.0",
|
|
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,6 +30,8 @@ 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",
|
|
33
35
|
]
|
|
34
36
|
|
|
35
37
|
[build-system]
|
|
@@ -66,64 +68,9 @@ mypy = [
|
|
|
66
68
|
ruff = ["ruff>=0.8.0"]
|
|
67
69
|
build = ["simple-build>=0.0.2", "shiv>=1.0.8"]
|
|
68
70
|
|
|
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
71
|
[project.scripts]
|
|
99
72
|
codelinks = "sphinx_codelinks.cmd:app"
|
|
100
73
|
|
|
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
74
|
[tool.ruff.lint]
|
|
128
75
|
extend-select = [
|
|
129
76
|
# "ANN",
|
|
@@ -162,15 +109,6 @@ force-sort-within-sections = true
|
|
|
162
109
|
"PLR2004", # magic-value-comparison - valueable for tests
|
|
163
110
|
"S101", # assert - needed for tests
|
|
164
111
|
]
|
|
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
|
|
173
|
-
]
|
|
174
112
|
"src/sphinx_codelinks/sphinx_extension/debug.py" = [
|
|
175
113
|
"T201", # print - used for output
|
|
176
114
|
"UP047", # on-pep695-generic-function - it's generic
|
|
@@ -1,7 +1,6 @@
|
|
|
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
5
|
from typing import Any, TypedDict
|
|
7
6
|
|
|
@@ -26,14 +25,14 @@ from sphinx_codelinks.config import (
|
|
|
26
25
|
OneLineCommentStyle,
|
|
27
26
|
SourceAnalyseConfig,
|
|
28
27
|
)
|
|
28
|
+
from sphinx_codelinks.logger import get_logger
|
|
29
29
|
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
logger.addHandler(console)
|
|
30
|
+
logger = get_logger(__name__)
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def _count(n: int, noun: str) -> str:
|
|
34
|
+
"""Format ``n noun`` with a naive (append-s) plural for progress summaries."""
|
|
35
|
+
return f"{n} {noun}" if n == 1 else f"{n} {noun}s"
|
|
37
36
|
|
|
38
37
|
|
|
39
38
|
class AnalyseWarningType(TypedDict):
|
|
@@ -57,7 +56,10 @@ class SourceAnalyse:
|
|
|
57
56
|
def __init__(
|
|
58
57
|
self,
|
|
59
58
|
analyse_config: SourceAnalyseConfig,
|
|
59
|
+
*,
|
|
60
|
+
name: str = "",
|
|
60
61
|
) -> None:
|
|
62
|
+
self.name = name
|
|
61
63
|
self.analyse_config = analyse_config
|
|
62
64
|
self.src_files: list[SourceFile] = []
|
|
63
65
|
self.src_comments: list[SourceComment] = []
|
|
@@ -110,9 +112,6 @@ class SourceAnalyse:
|
|
|
110
112
|
self.src_files.append(src_file)
|
|
111
113
|
self.src_comments.extend(src_comments)
|
|
112
114
|
|
|
113
|
-
logger.info(f"Source files loaded: {len(self.src_files)}")
|
|
114
|
-
logger.info(f"Source comments extracted: {len(self.src_comments)}")
|
|
115
|
-
|
|
116
115
|
def extract_marker(
|
|
117
116
|
self,
|
|
118
117
|
text: str,
|
|
@@ -353,13 +352,6 @@ class SourceAnalyse:
|
|
|
353
352
|
if marked_rst:
|
|
354
353
|
self.marked_rst.append(marked_rst)
|
|
355
354
|
|
|
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
355
|
def merge_marked_content(self) -> None:
|
|
364
356
|
self.all_marked_content.extend(self.need_id_refs)
|
|
365
357
|
self.oneline_needs.sort(key=lambda x: x.source_map["start"]["row"])
|
|
@@ -378,9 +370,23 @@ class SourceAnalyse:
|
|
|
378
370
|
]
|
|
379
371
|
with output_path.open("w") as f:
|
|
380
372
|
json.dump(to_dump, f)
|
|
381
|
-
logger.info(f"Marked content dumped to {output_path}")
|
|
382
373
|
|
|
383
374
|
def run(self) -> None:
|
|
384
375
|
self.create_src_objects()
|
|
385
376
|
self.extract_marked_content()
|
|
386
377
|
self.merge_marked_content()
|
|
378
|
+
self._log_summary()
|
|
379
|
+
|
|
380
|
+
def _log_summary(self) -> None:
|
|
381
|
+
"""Emit a per-project marker (default-visible) plus a -v breakdown."""
|
|
382
|
+
label = f"codelinks [{self.name}]" if self.name else "codelinks"
|
|
383
|
+
logger.info(
|
|
384
|
+
f"{label}: {_count(len(self.src_files), 'file')}, "
|
|
385
|
+
f"{_count(len(self.all_marked_content), 'marker')}"
|
|
386
|
+
)
|
|
387
|
+
logger.debug(
|
|
388
|
+
f"{label}: {_count(len(self.src_comments), 'comment')}, "
|
|
389
|
+
f"{_count(len(self.oneline_needs), 'oneline need')}, "
|
|
390
|
+
f"{_count(len(self.need_id_refs), 'id-ref')}, "
|
|
391
|
+
f"{_count(len(self.marked_rst), 'marked-rst block')}"
|
|
392
|
+
)
|
{sphinx_codelinks-1.2.0 → sphinx_codelinks-1.3.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."""
|
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
import json
|
|
2
|
-
import logging
|
|
3
2
|
from pathlib import Path
|
|
4
3
|
from typing import cast
|
|
5
4
|
|
|
@@ -9,14 +8,9 @@ from sphinx_codelinks.analyse.analyse import (
|
|
|
9
8
|
SourceAnalyse,
|
|
10
9
|
)
|
|
11
10
|
from sphinx_codelinks.config import CodeLinksConfig, CodeLinksProjectConfigType
|
|
11
|
+
from sphinx_codelinks.logger import get_logger
|
|
12
12
|
|
|
13
|
-
|
|
14
|
-
logger = logging.getLogger(__name__)
|
|
15
|
-
logger.setLevel(logging.INFO)
|
|
16
|
-
# log to the console
|
|
17
|
-
console = logging.StreamHandler()
|
|
18
|
-
console.setLevel(logging.INFO)
|
|
19
|
-
logger.addHandler(console)
|
|
13
|
+
logger = get_logger(__name__)
|
|
20
14
|
|
|
21
15
|
|
|
22
16
|
class AnalyseProjects:
|
|
@@ -32,7 +26,7 @@ class AnalyseProjects:
|
|
|
32
26
|
|
|
33
27
|
def run(self) -> None:
|
|
34
28
|
for project, config in self.projects_configs.items():
|
|
35
|
-
src_analyse = SourceAnalyse(config["analyse_config"])
|
|
29
|
+
src_analyse = SourceAnalyse(config["analyse_config"], name=project)
|
|
36
30
|
src_analyse.run()
|
|
37
31
|
self.projects_analyse[project] = src_analyse
|
|
38
32
|
|
|
@@ -46,7 +40,7 @@ class AnalyseProjects:
|
|
|
46
40
|
}
|
|
47
41
|
with output_path.open("w") as f:
|
|
48
42
|
json.dump(to_dump, f)
|
|
49
|
-
logger.
|
|
43
|
+
logger.debug(f"codelinks: marked content dumped to {output_path}")
|
|
50
44
|
|
|
51
45
|
@classmethod
|
|
52
46
|
def load_warnings(cls, warnings_dir: Path) -> list[AnalyseWarning] | None:
|
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
from collections.abc import ByteString, Callable
|
|
2
2
|
import configparser
|
|
3
|
-
import logging
|
|
4
3
|
from pathlib import Path
|
|
5
4
|
from typing import TypedDict
|
|
6
5
|
from urllib.request import pathname2url
|
|
@@ -10,16 +9,25 @@ from tree_sitter import Language, Parser, Point, Query, QueryCursor
|
|
|
10
9
|
from tree_sitter import Node as TreeSitterNode
|
|
11
10
|
|
|
12
11
|
from sphinx_codelinks.config import UNIX_NEWLINE, CommentCategory
|
|
12
|
+
from sphinx_codelinks.logger import get_logger
|
|
13
13
|
from sphinx_codelinks.source_discover.config import CommentType
|
|
14
14
|
|
|
15
|
-
# Language-specific node types for scope detection
|
|
15
|
+
# Language-specific node types for scope detection.
|
|
16
|
+
#
|
|
17
|
+
# YAML and JSONC are intentionally absent. They are data formats, not code, so a
|
|
18
|
+
# comment associates with the surrounding data structure (key/value pair, list
|
|
19
|
+
# item, or scalar) rather than with an enclosing or following declaration. That
|
|
20
|
+
# needs a different algorithm (inline same-row association first, scalar targets,
|
|
21
|
+
# grammar-specific traversal), implemented in find_yaml_associated_structure and
|
|
22
|
+
# find_jsonc_associated_structure and dispatched from find_associated_scope.
|
|
23
|
+
# Those bespoke finders never read this table (only find_next_scope and
|
|
24
|
+
# find_enclosing_scope do), so an entry here would be dead.
|
|
16
25
|
SCOPE_NODE_TYPES = {
|
|
17
26
|
# @Python Scope Node Types, IMPL_PY_2, impl, [FE_PY]
|
|
18
27
|
CommentType.python: {"function_definition", "class_definition"},
|
|
19
28
|
# @C and C++ Scope Node Types, IMPL_C_2, impl, [FE_C_SUPPORT, FE_CPP]
|
|
20
29
|
CommentType.cpp: {"function_definition", "class_definition"},
|
|
21
30
|
CommentType.cs: {"method_declaration", "class_declaration", "property_declaration"},
|
|
22
|
-
CommentType.yaml: {"block_mapping_pair", "block_sequence_item", "document"},
|
|
23
31
|
# @Rust Scope Node Types, IMPL_RUST_2, impl, [FE_RUST];
|
|
24
32
|
CommentType.rust: {
|
|
25
33
|
"function_item",
|
|
@@ -29,15 +37,16 @@ SCOPE_NODE_TYPES = {
|
|
|
29
37
|
"trait_item",
|
|
30
38
|
"mod_item",
|
|
31
39
|
},
|
|
40
|
+
# @Go Scope Node Types, IMPL_GO_4, impl, [FE_GO]
|
|
41
|
+
CommentType.go: {
|
|
42
|
+
"function_declaration",
|
|
43
|
+
"method_declaration",
|
|
44
|
+
"type_declaration",
|
|
45
|
+
"type_spec",
|
|
46
|
+
},
|
|
32
47
|
}
|
|
33
48
|
|
|
34
|
-
|
|
35
|
-
logger = logging.getLogger(__name__)
|
|
36
|
-
logger.setLevel(logging.INFO)
|
|
37
|
-
# log to the console
|
|
38
|
-
console = logging.StreamHandler()
|
|
39
|
-
console.setLevel(logging.INFO)
|
|
40
|
-
logger.addHandler(console)
|
|
49
|
+
logger = get_logger(__name__)
|
|
41
50
|
|
|
42
51
|
GIT_HOST_URL_TEMPLATE = {
|
|
43
52
|
"github": "https://github.com/{owner}/{repo}/blob/{rev}/{path}#L{lineno}",
|
|
@@ -60,6 +69,23 @@ RUST_QUERY = """
|
|
|
60
69
|
(line_comment) @comment
|
|
61
70
|
(block_comment) @comment
|
|
62
71
|
"""
|
|
72
|
+
# @Go comment query for tree-sitter, IMPL_GO_3, impl, [FE_GO]
|
|
73
|
+
GO_QUERY = """
|
|
74
|
+
(comment) @comment
|
|
75
|
+
"""
|
|
76
|
+
JSONC_QUERY = """(comment) @comment"""
|
|
77
|
+
|
|
78
|
+
# JSON value node types that can be associated with a comment.
|
|
79
|
+
JSON_STRUCTURE_TYPES = {
|
|
80
|
+
"pair",
|
|
81
|
+
"object",
|
|
82
|
+
"array",
|
|
83
|
+
"string",
|
|
84
|
+
"number",
|
|
85
|
+
"true",
|
|
86
|
+
"false",
|
|
87
|
+
"null",
|
|
88
|
+
}
|
|
63
89
|
|
|
64
90
|
|
|
65
91
|
def is_text_file(filepath: Path, sample_size: int = 2048) -> bool:
|
|
@@ -77,7 +103,7 @@ def is_text_file(filepath: Path, sample_size: int = 2048) -> bool:
|
|
|
77
103
|
return False
|
|
78
104
|
|
|
79
105
|
|
|
80
|
-
# @Tree-sitter parser initialization for multiple languages, IMPL_LANG_1, impl, [FE_C_SUPPORT, FE_CPP, FE_PY, FE_YAML, FE_RUST]
|
|
106
|
+
# @Tree-sitter parser initialization for multiple languages, IMPL_LANG_1, impl, [FE_C_SUPPORT, FE_CPP, FE_PY, FE_YAML, FE_RUST, FE_GO, FE_JSONC]
|
|
81
107
|
def init_tree_sitter(comment_type: CommentType) -> tuple[Parser, Query]:
|
|
82
108
|
if comment_type == CommentType.cpp:
|
|
83
109
|
import tree_sitter_cpp # noqa: PLC0415
|
|
@@ -104,6 +130,16 @@ def init_tree_sitter(comment_type: CommentType) -> tuple[Parser, Query]:
|
|
|
104
130
|
|
|
105
131
|
parsed_language = Language(tree_sitter_rust.language())
|
|
106
132
|
query = Query(parsed_language, RUST_QUERY)
|
|
133
|
+
elif comment_type == CommentType.go:
|
|
134
|
+
import tree_sitter_go # noqa: PLC0415
|
|
135
|
+
|
|
136
|
+
parsed_language = Language(tree_sitter_go.language())
|
|
137
|
+
query = Query(parsed_language, GO_QUERY)
|
|
138
|
+
elif comment_type == CommentType.jsonc:
|
|
139
|
+
import tree_sitter_json # noqa: PLC0415
|
|
140
|
+
|
|
141
|
+
parsed_language = Language(tree_sitter_json.language())
|
|
142
|
+
query = Query(parsed_language, JSONC_QUERY)
|
|
107
143
|
else:
|
|
108
144
|
raise ValueError(f"Unsupported comment style: {comment_type}")
|
|
109
145
|
parser = Parser(parsed_language)
|
|
@@ -203,8 +239,11 @@ def find_yaml_next_structure(node: TreeSitterNode) -> TreeSitterNode | None:
|
|
|
203
239
|
return None
|
|
204
240
|
|
|
205
241
|
|
|
206
|
-
def
|
|
207
|
-
"""Find a previous named sibling that is on the same row as the comment.
|
|
242
|
+
def find_prev_sibling_on_same_row(node: TreeSitterNode) -> TreeSitterNode | None:
|
|
243
|
+
"""Find a previous named sibling that is on the same row as the comment.
|
|
244
|
+
|
|
245
|
+
Grammar-agnostic: used to detect inline comments in both YAML and JSONC.
|
|
246
|
+
"""
|
|
208
247
|
comment_row = node.start_point.row
|
|
209
248
|
current = node.prev_named_sibling
|
|
210
249
|
|
|
@@ -225,7 +264,7 @@ def find_yaml_prev_sibling_on_same_row(node: TreeSitterNode) -> TreeSitterNode |
|
|
|
225
264
|
def find_yaml_associated_structure(node: TreeSitterNode) -> TreeSitterNode | None:
|
|
226
265
|
"""Find the YAML structure (key-value pair, list item, etc.) associated with a comment."""
|
|
227
266
|
# First, check if this is an inline comment by looking for a previous sibling on the same row
|
|
228
|
-
prev_sibling_same_row =
|
|
267
|
+
prev_sibling_same_row = find_prev_sibling_on_same_row(node)
|
|
229
268
|
if prev_sibling_same_row:
|
|
230
269
|
return prev_sibling_same_row
|
|
231
270
|
|
|
@@ -244,6 +283,36 @@ def find_yaml_associated_structure(node: TreeSitterNode) -> TreeSitterNode | Non
|
|
|
244
283
|
return None
|
|
245
284
|
|
|
246
285
|
|
|
286
|
+
# @JSONC comment-to-structure association, IMPL_JSONC_2, impl, [FE_JSONC]
|
|
287
|
+
def find_jsonc_associated_structure(node: TreeSitterNode) -> TreeSitterNode | None:
|
|
288
|
+
"""Find the JSON structure (key/value pair, value, list item) for a comment.
|
|
289
|
+
|
|
290
|
+
JSON is data rather than code, so association follows the same intent as YAML:
|
|
291
|
+
an inline comment belongs to the value on its row, a leading comment belongs to
|
|
292
|
+
the following structure, otherwise it belongs to the enclosing structure.
|
|
293
|
+
"""
|
|
294
|
+
# Inline comment: a value/pair on the same row, before the comment
|
|
295
|
+
prev_sibling_same_row = find_prev_sibling_on_same_row(node)
|
|
296
|
+
if prev_sibling_same_row:
|
|
297
|
+
return prev_sibling_same_row
|
|
298
|
+
|
|
299
|
+
# Leading comment: the next structure following the comment
|
|
300
|
+
current = node.next_named_sibling
|
|
301
|
+
while current:
|
|
302
|
+
if current.type in JSON_STRUCTURE_TYPES:
|
|
303
|
+
return current
|
|
304
|
+
current = current.next_named_sibling
|
|
305
|
+
|
|
306
|
+
# Otherwise: the enclosing structure
|
|
307
|
+
parent = node.parent
|
|
308
|
+
while parent:
|
|
309
|
+
if parent.type in {"pair", "object", "array"}:
|
|
310
|
+
return parent
|
|
311
|
+
parent = parent.parent
|
|
312
|
+
|
|
313
|
+
return None
|
|
314
|
+
|
|
315
|
+
|
|
247
316
|
def find_associated_scope(
|
|
248
317
|
node: TreeSitterNode, comment_type: CommentType = CommentType.cpp
|
|
249
318
|
) -> TreeSitterNode | None:
|
|
@@ -252,6 +321,10 @@ def find_associated_scope(
|
|
|
252
321
|
# YAML uses different structure association logic
|
|
253
322
|
return find_yaml_associated_structure(node)
|
|
254
323
|
|
|
324
|
+
if comment_type == CommentType.jsonc:
|
|
325
|
+
# JSONC uses data-aware structure association logic
|
|
326
|
+
return find_jsonc_associated_structure(node)
|
|
327
|
+
|
|
255
328
|
if node.type == CommentCategory.docstring:
|
|
256
329
|
# Only for python's docstring
|
|
257
330
|
return find_enclosing_scope(node, comment_type)
|
|
@@ -270,7 +343,11 @@ def locate_git_root(src_dir: Path) -> Path | None:
|
|
|
270
343
|
for parent in parents:
|
|
271
344
|
if (parent / ".git").exists() and (parent / ".git").is_dir():
|
|
272
345
|
return parent
|
|
273
|
-
logger.warning(
|
|
346
|
+
logger.warning(
|
|
347
|
+
f"git root is not found in the parent of {src_dir}",
|
|
348
|
+
subtype="git_root",
|
|
349
|
+
location=str(src_dir),
|
|
350
|
+
)
|
|
274
351
|
return None
|
|
275
352
|
|
|
276
353
|
|
|
@@ -278,7 +355,11 @@ def get_remote_url(git_root: Path, remote_name: str = "origin") -> str | None:
|
|
|
278
355
|
"""Get remote url from .git/config."""
|
|
279
356
|
config_path = git_root / ".git" / "config"
|
|
280
357
|
if not config_path.exists():
|
|
281
|
-
|
|
358
|
+
logger.warning(
|
|
359
|
+
f"{config_path} does not exist",
|
|
360
|
+
subtype="git_config",
|
|
361
|
+
location=str(config_path),
|
|
362
|
+
)
|
|
282
363
|
return None
|
|
283
364
|
|
|
284
365
|
config = configparser.ConfigParser(allow_no_value=True, strict=False)
|
|
@@ -287,7 +368,11 @@ def get_remote_url(git_root: Path, remote_name: str = "origin") -> str | None:
|
|
|
287
368
|
if section in config and "url" in config[section]:
|
|
288
369
|
url: str = config[section]["url"]
|
|
289
370
|
return url
|
|
290
|
-
logger.warning(
|
|
371
|
+
logger.warning(
|
|
372
|
+
f"remote-url is not found in {config_path}",
|
|
373
|
+
subtype="git_remote",
|
|
374
|
+
location=str(config_path),
|
|
375
|
+
)
|
|
291
376
|
return None
|
|
292
377
|
|
|
293
378
|
|
|
@@ -295,16 +380,25 @@ def get_current_rev(git_root: Path) -> str | None:
|
|
|
295
380
|
"""Get current commit rev from .git/HEAD."""
|
|
296
381
|
head_path = git_root / ".git" / "HEAD"
|
|
297
382
|
if not head_path.exists():
|
|
298
|
-
|
|
383
|
+
logger.warning(
|
|
384
|
+
f"{head_path} does not exist",
|
|
385
|
+
subtype="git_head",
|
|
386
|
+
location=str(head_path),
|
|
387
|
+
)
|
|
299
388
|
return None
|
|
300
389
|
head_content = head_path.read_text().strip()
|
|
301
390
|
if not head_content.startswith("ref: "):
|
|
302
|
-
|
|
303
|
-
|
|
391
|
+
# Detached HEAD (e.g. CI checkouts): .git/HEAD holds the commit SHA
|
|
392
|
+
# directly, which is exactly the rev we want.
|
|
393
|
+
return head_content
|
|
304
394
|
|
|
305
395
|
ref_path = git_root / ".git" / head_content.split(":", 1)[1].strip()
|
|
306
396
|
if not ref_path.exists():
|
|
307
|
-
|
|
397
|
+
logger.warning(
|
|
398
|
+
f"{ref_path} does not exist",
|
|
399
|
+
subtype="git_ref",
|
|
400
|
+
location=str(ref_path),
|
|
401
|
+
)
|
|
308
402
|
return None
|
|
309
403
|
return ref_path.read_text().strip()
|
|
310
404
|
|
|
@@ -315,7 +409,10 @@ def form_https_url(
|
|
|
315
409
|
parsed_url = parse(git_url)
|
|
316
410
|
template = GIT_HOST_URL_TEMPLATE.get(parsed_url.platform)
|
|
317
411
|
if not template:
|
|
318
|
-
|
|
412
|
+
logger.warning(
|
|
413
|
+
f"Unsupported Git host: {parsed_url.platform}",
|
|
414
|
+
subtype="git_host",
|
|
415
|
+
)
|
|
319
416
|
return git_url
|
|
320
417
|
https_url = template.format(
|
|
321
418
|
owner=parsed_url.owner,
|
|
@@ -14,7 +14,7 @@ from sphinx_codelinks.config import (
|
|
|
14
14
|
CodeLinksProjectConfigType,
|
|
15
15
|
generate_project_configs,
|
|
16
16
|
)
|
|
17
|
-
from sphinx_codelinks.logger import logger
|
|
17
|
+
from sphinx_codelinks.logger import configure_cli, logger
|
|
18
18
|
from sphinx_codelinks.needextend_write import MarkedObjType, convert_marked_content
|
|
19
19
|
from sphinx_codelinks.source_discover.config import (
|
|
20
20
|
CommentType,
|
|
@@ -88,9 +88,12 @@ def analyse( # noqa: PLR0912 # for CLI, so it needs the branches
|
|
|
88
88
|
exists=True,
|
|
89
89
|
),
|
|
90
90
|
] = None,
|
|
91
|
+
verbose: OptVerbose = False,
|
|
92
|
+
quiet: OptQuiet = False,
|
|
91
93
|
) -> None:
|
|
92
94
|
"""Analyse marked content in source code."""
|
|
93
95
|
# @CLI command to analyse source code and extract traceability markers, IMPL_CLI_ANALYZE, impl, [FE_CLI_ANALYZE]
|
|
96
|
+
configure_cli(verbose, quiet)
|
|
94
97
|
|
|
95
98
|
data: CodeLinksConfigType = load_config_from_toml(config)
|
|
96
99
|
|
|
@@ -170,7 +173,7 @@ def analyse( # noqa: PLR0912 # for CLI, so it needs the branches
|
|
|
170
173
|
|
|
171
174
|
|
|
172
175
|
@app.command(no_args_is_help=True)
|
|
173
|
-
def discover(
|
|
176
|
+
def discover( # noqa: PLR0913 # CLI command requires multiple parameters
|
|
174
177
|
src_dir: Annotated[
|
|
175
178
|
Path,
|
|
176
179
|
typer.Argument(
|
|
@@ -203,9 +206,13 @@ def discover(
|
|
|
203
206
|
gitignore: Annotated[
|
|
204
207
|
bool,
|
|
205
208
|
typer.Option(
|
|
206
|
-
help="Respect .gitignore in the given directory
|
|
209
|
+
help="Respect .gitignore files in the given directory and its parents"
|
|
207
210
|
),
|
|
208
211
|
] = True,
|
|
212
|
+
follow_links: Annotated[
|
|
213
|
+
bool,
|
|
214
|
+
typer.Option(help="Follow symbolic links during file discovery"),
|
|
215
|
+
] = False,
|
|
209
216
|
comment_type: Annotated[
|
|
210
217
|
CommentType,
|
|
211
218
|
typer.Option(
|
|
@@ -222,6 +229,7 @@ def discover(
|
|
|
222
229
|
"exclude": exclude,
|
|
223
230
|
"include": include,
|
|
224
231
|
"gitignore": gitignore,
|
|
232
|
+
"follow_links": follow_links,
|
|
225
233
|
"comment_type": comment_type,
|
|
226
234
|
}
|
|
227
235
|
|
|
@@ -286,7 +294,7 @@ def write_rst( # noqa: PLR0913 # for CLI, so it takes as many as it requires
|
|
|
286
294
|
quiet: OptQuiet = False,
|
|
287
295
|
) -> None:
|
|
288
296
|
"""Generate needextend.rst from the extracted obj in JSON."""
|
|
289
|
-
|
|
297
|
+
configure_cli(verbose, quiet)
|
|
290
298
|
try:
|
|
291
299
|
with jsonpath.open("r") as f:
|
|
292
300
|
marked_content = json.load(f)
|
|
@@ -24,6 +24,8 @@ COMMENT_MARKERS = {
|
|
|
24
24
|
# @Support Python style comments, IMPL_PY_1, impl, [FE_PY]
|
|
25
25
|
CommentType.python: ["#"],
|
|
26
26
|
CommentType.cs: ["//", "/*", "///"],
|
|
27
|
+
# @Support Go style comments, IMPL_GO_2, impl, [FE_GO]
|
|
28
|
+
CommentType.go: ["//", "/*"],
|
|
27
29
|
}
|
|
28
30
|
ESCAPE = "\\"
|
|
29
31
|
|
|
@@ -276,11 +278,29 @@ class OneLineCommentStyle:
|
|
|
276
278
|
needs_field_names.add(_field["name"])
|
|
277
279
|
return errors
|
|
278
280
|
|
|
281
|
+
def check_fields_default_order(self) -> list[str]:
|
|
282
|
+
errors = []
|
|
283
|
+
seen_default = False
|
|
284
|
+
first_default_field = ""
|
|
285
|
+
for _field in self.needs_fields:
|
|
286
|
+
has_default = _field.get("default") is not None
|
|
287
|
+
if has_default and not seen_default:
|
|
288
|
+
seen_default = True
|
|
289
|
+
first_default_field = _field["name"]
|
|
290
|
+
elif not has_default and seen_default:
|
|
291
|
+
errors.append(
|
|
292
|
+
f"Field '{_field['name']}' without a default follows "
|
|
293
|
+
f"field '{first_default_field}' which has a default. "
|
|
294
|
+
f"Fields without defaults must be defined before fields with defaults."
|
|
295
|
+
)
|
|
296
|
+
return errors
|
|
297
|
+
|
|
279
298
|
def check_fields_configuration(self) -> list[str]:
|
|
280
299
|
return (
|
|
281
300
|
self.check_schema()
|
|
282
301
|
+ self.check_required_fields()
|
|
283
302
|
+ self.check_fields_mutually_exclusive()
|
|
303
|
+
+ self.check_fields_default_order()
|
|
284
304
|
)
|
|
285
305
|
|
|
286
306
|
def get_cnt_required_fields(self) -> int:
|