sphinx-codelinks 0.1.2__tar.gz → 1.0.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-0.1.2 → sphinx_codelinks-1.0.0}/PKG-INFO +10 -4
- {sphinx_codelinks-0.1.2 → sphinx_codelinks-1.0.0}/README.md +4 -3
- {sphinx_codelinks-0.1.2 → sphinx_codelinks-1.0.0}/pyproject.toml +27 -4
- {sphinx_codelinks-0.1.2 → sphinx_codelinks-1.0.0}/src/sphinx_codelinks/__init__.py +1 -1
- sphinx_codelinks-1.0.0/src/sphinx_codelinks/analyse/analyse.py +379 -0
- sphinx_codelinks-1.0.0/src/sphinx_codelinks/analyse/models.py +86 -0
- sphinx_codelinks-0.1.2/src/sphinx_codelinks/virtual_docs/utils.py → sphinx_codelinks-1.0.0/src/sphinx_codelinks/analyse/oneline_parser.py +14 -11
- sphinx_codelinks-1.0.0/src/sphinx_codelinks/analyse/projects.py +84 -0
- sphinx_codelinks-1.0.0/src/sphinx_codelinks/analyse/utils.py +280 -0
- sphinx_codelinks-1.0.0/src/sphinx_codelinks/cmd.py +315 -0
- sphinx_codelinks-1.0.0/src/sphinx_codelinks/config.py +856 -0
- sphinx_codelinks-1.0.0/src/sphinx_codelinks/logger.py +94 -0
- sphinx_codelinks-1.0.0/src/sphinx_codelinks/needextend_write.py +218 -0
- {sphinx_codelinks-0.1.2/src/sphinx_codelinks/source_discovery → sphinx_codelinks-1.0.0/src/sphinx_codelinks/source_discover}/config.py +31 -8
- {sphinx_codelinks-0.1.2/src/sphinx_codelinks/source_discovery → sphinx_codelinks-1.0.0/src/sphinx_codelinks/source_discover}/source_discover.py +25 -28
- {sphinx_codelinks-0.1.2 → sphinx_codelinks-1.0.0}/src/sphinx_codelinks/sphinx_extension/directives/src_trace.py +92 -134
- {sphinx_codelinks-0.1.2 → sphinx_codelinks-1.0.0}/src/sphinx_codelinks/sphinx_extension/source_tracing.py +21 -41
- sphinx_codelinks-0.1.2/src/sphinx_codelinks/README.md +0 -46
- sphinx_codelinks-0.1.2/src/sphinx_codelinks/cmd.py +0 -212
- sphinx_codelinks-0.1.2/src/sphinx_codelinks/sphinx_extension/config.py +0 -324
- sphinx_codelinks-0.1.2/src/sphinx_codelinks/virtual_docs/config.py +0 -256
- sphinx_codelinks-0.1.2/src/sphinx_codelinks/virtual_docs/ubt_models.py +0 -131
- sphinx_codelinks-0.1.2/src/sphinx_codelinks/virtual_docs/virtual_docs.py +0 -238
- {sphinx_codelinks-0.1.2 → sphinx_codelinks-1.0.0}/LICENSE +0 -0
- {sphinx_codelinks-0.1.2 → sphinx_codelinks-1.0.0}/src/sphinx_codelinks/sphinx_extension/debug.py +0 -0
- {sphinx_codelinks-0.1.2 → sphinx_codelinks-1.0.0}/src/sphinx_codelinks/sphinx_extension/html_wrapper.py +0 -0
- {sphinx_codelinks-0.1.2 → sphinx_codelinks-1.0.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: 0.
|
|
3
|
+
Version: 1.0.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>
|
|
@@ -10,12 +10,17 @@ License-File: LICENSE
|
|
|
10
10
|
Requires-Dist: comment-parser>=1.2.4
|
|
11
11
|
Requires-Dist: gitignore-parser>=0.1.11
|
|
12
12
|
Requires-Dist: typer>=0.16.0
|
|
13
|
+
Requires-Dist: click < 8.2
|
|
13
14
|
Requires-Dist: jsonschema
|
|
14
15
|
Requires-Dist: sphinx>=7.4,<9
|
|
15
16
|
Requires-Dist: sphinx-needs>=4.2.0
|
|
16
17
|
Requires-Dist: jinja2
|
|
17
18
|
Requires-Dist: pygments
|
|
18
19
|
Requires-Dist: docutils
|
|
20
|
+
Requires-Dist: tree-sitter-cpp>=0.23.4
|
|
21
|
+
Requires-Dist: giturlparse>=0.12.0
|
|
22
|
+
Requires-Dist: tree-sitter-python>=0.23.6
|
|
23
|
+
Requires-Dist: tree-sitter~=0.25.1
|
|
19
24
|
|
|
20
25
|
# Sphinx CodeLinks
|
|
21
26
|
|
|
@@ -24,7 +29,7 @@ A Sphinx extension for discovering, linking, and documenting source code across
|
|
|
24
29
|
## Features
|
|
25
30
|
|
|
26
31
|
- **Source Discovery**: Automatically discover source files in your project
|
|
27
|
-
- **
|
|
32
|
+
- **Analyse**: Parse source codes and extract specified markers with their metadata
|
|
28
33
|
- **Code Linking**: Create intelligent links between code elements
|
|
29
34
|
- **Sphinx Integration**: Seamless integration with existing Sphinx documentation
|
|
30
35
|
|
|
@@ -38,6 +43,7 @@ Add to your `conf.py`:
|
|
|
38
43
|
|
|
39
44
|
```python
|
|
40
45
|
extensions = ['sphinx_needs', 'sphinx_codelinks']
|
|
46
|
+
src_trace_config_from_toml = "codelinks.toml"
|
|
41
47
|
```
|
|
42
48
|
|
|
43
49
|
## Documentation
|
|
@@ -46,8 +52,8 @@ Full documentation: https://codelinks.useblocks.com
|
|
|
46
52
|
|
|
47
53
|
## Components
|
|
48
54
|
|
|
49
|
-
- **Source Discovery** ([`src/sphinx_codelinks/
|
|
50
|
-
- **
|
|
55
|
+
- **Source Discovery** ([`src/sphinx_codelinks/source_discover`](src/sphinx_codelinks/source_discover)): Code analysis and discovery
|
|
56
|
+
- **Analyse** ([`src/sphinx_codelinks/analyse`](src/sphinx_codelinks/analyse)): Documentation generation
|
|
51
57
|
- **Sphinx Extension** ([`src/sphinx_codelinks/sphinx_extension`](src/sphinx_codelinks/sphinx_extension)): Sphinx integration
|
|
52
58
|
- **Command Line** ([`src/sphinx_codelinks/cmd.py`](src/sphinx_codelinks/cmd.py)): CLI interface
|
|
53
59
|
|
|
@@ -5,7 +5,7 @@ A Sphinx extension for discovering, linking, and documenting source code across
|
|
|
5
5
|
## Features
|
|
6
6
|
|
|
7
7
|
- **Source Discovery**: Automatically discover source files in your project
|
|
8
|
-
- **
|
|
8
|
+
- **Analyse**: Parse source codes and extract specified markers with their metadata
|
|
9
9
|
- **Code Linking**: Create intelligent links between code elements
|
|
10
10
|
- **Sphinx Integration**: Seamless integration with existing Sphinx documentation
|
|
11
11
|
|
|
@@ -19,6 +19,7 @@ Add to your `conf.py`:
|
|
|
19
19
|
|
|
20
20
|
```python
|
|
21
21
|
extensions = ['sphinx_needs', 'sphinx_codelinks']
|
|
22
|
+
src_trace_config_from_toml = "codelinks.toml"
|
|
22
23
|
```
|
|
23
24
|
|
|
24
25
|
## Documentation
|
|
@@ -27,8 +28,8 @@ Full documentation: https://codelinks.useblocks.com
|
|
|
27
28
|
|
|
28
29
|
## Components
|
|
29
30
|
|
|
30
|
-
- **Source Discovery** ([`src/sphinx_codelinks/
|
|
31
|
-
- **
|
|
31
|
+
- **Source Discovery** ([`src/sphinx_codelinks/source_discover`](src/sphinx_codelinks/source_discover)): Code analysis and discovery
|
|
32
|
+
- **Analyse** ([`src/sphinx_codelinks/analyse`](src/sphinx_codelinks/analyse)): Documentation generation
|
|
32
33
|
- **Sphinx Extension** ([`src/sphinx_codelinks/sphinx_extension`](src/sphinx_codelinks/sphinx_extension)): Sphinx integration
|
|
33
34
|
- **Command Line** ([`src/sphinx_codelinks/cmd.py`](src/sphinx_codelinks/cmd.py)): CLI interface
|
|
34
35
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "sphinx-codelinks"
|
|
3
|
-
version = "0.
|
|
3
|
+
version = "1.0.0"
|
|
4
4
|
description = "Fast Source Code Traceability for Sphinx-Needs"
|
|
5
5
|
authors = [{ name = "team useblocks", email = "info@useblocks.com" }]
|
|
6
6
|
maintainers = [
|
|
@@ -14,13 +14,20 @@ dependencies = [
|
|
|
14
14
|
"comment-parser>=1.2.4",
|
|
15
15
|
"gitignore-parser>=0.1.11",
|
|
16
16
|
"typer>=0.16.0",
|
|
17
|
+
"click < 8.2", # click 8.2.* produces empty errors if no args are given
|
|
17
18
|
"jsonschema",
|
|
18
19
|
"sphinx>=7.4,<9",
|
|
19
|
-
"sphinx-needs>=4.2.0",
|
|
20
|
-
# unconstrained versions, to be pinned by user or Sphinx
|
|
20
|
+
"sphinx-needs>=4.2.0", # unconstrained versions, to be pinned by user or Sphinx
|
|
21
21
|
"jinja2",
|
|
22
22
|
"pygments",
|
|
23
|
-
"docutils",
|
|
23
|
+
"docutils", # constrained by user or Sphinx
|
|
24
|
+
"tree-sitter-cpp>=0.23.4",
|
|
25
|
+
"giturlparse>=0.12.0",
|
|
26
|
+
"tree-sitter-python>=0.23.6",
|
|
27
|
+
# Compiling issue on manylinux aarch64 image, so install from git instead of Pypi
|
|
28
|
+
# https://github.com/tree-sitter/py-tree-sitter/issues/386#issuecomment-3101430799
|
|
29
|
+
"tree-sitter~=0.25.1",
|
|
30
|
+
|
|
24
31
|
]
|
|
25
32
|
|
|
26
33
|
[build-system]
|
|
@@ -53,6 +60,7 @@ dev-dependencies = [
|
|
|
53
60
|
"toml>=0.10.2",
|
|
54
61
|
"sphinx-code-tabs>=0.5.5",
|
|
55
62
|
"sphinxcontrib-typer>=0.5.1",
|
|
63
|
+
"sphinxcontrib-video>=0.4.1",
|
|
56
64
|
]
|
|
57
65
|
|
|
58
66
|
[project.scripts]
|
|
@@ -68,6 +76,21 @@ codelinks = "sphinx_codelinks.cmd:app"
|
|
|
68
76
|
"docs:rm" = "rm -rf docs/_build/html"
|
|
69
77
|
"docs" = "sphinx-build -nW --keep-going -b html -T -c docs docs/source docs/_build/html"
|
|
70
78
|
"docs:clean" = { chain = ["docs:rm", "docs"] }
|
|
79
|
+
# needextend demo
|
|
80
|
+
"analyse" = "codelinks analyse tests/data/configs/minimum_config.toml"
|
|
81
|
+
"analyse:rm" = "rm -rf output/marked_content.json"
|
|
82
|
+
"write" = "codelinks write rst output/marked_content.json --outpath tests/data/needextend_demo/needextend.rst"
|
|
83
|
+
"write:rm" = "rm -rf tests/data/needextend_demo/needextend.rst"
|
|
84
|
+
"demo:rm" = "rm -rf tests/data/needextend_demo/_build"
|
|
85
|
+
"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"
|
|
86
|
+
"demo:clean" = { chain = [
|
|
87
|
+
"demo:rm",
|
|
88
|
+
"analyse:rm",
|
|
89
|
+
"write:rm",
|
|
90
|
+
"analyse",
|
|
91
|
+
"write",
|
|
92
|
+
"demo:build",
|
|
93
|
+
] }
|
|
71
94
|
|
|
72
95
|
[tool.ruff.lint]
|
|
73
96
|
extend-select = [
|
|
@@ -0,0 +1,379 @@
|
|
|
1
|
+
from collections.abc import Generator
|
|
2
|
+
from dataclasses import dataclass
|
|
3
|
+
import json
|
|
4
|
+
import logging
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
from typing import Any, TypedDict
|
|
7
|
+
|
|
8
|
+
from tree_sitter import Node as TreeSitterNode
|
|
9
|
+
|
|
10
|
+
from sphinx_codelinks.analyse import utils
|
|
11
|
+
from sphinx_codelinks.analyse.models import (
|
|
12
|
+
MarkedContentType,
|
|
13
|
+
MarkedRst,
|
|
14
|
+
NeedIdRefs,
|
|
15
|
+
OneLineNeed,
|
|
16
|
+
SourceComment,
|
|
17
|
+
SourceFile,
|
|
18
|
+
SourceMap,
|
|
19
|
+
)
|
|
20
|
+
from sphinx_codelinks.analyse.oneline_parser import (
|
|
21
|
+
OnelineParserInvalidWarning,
|
|
22
|
+
oneline_parser,
|
|
23
|
+
)
|
|
24
|
+
from sphinx_codelinks.config import (
|
|
25
|
+
UNIX_NEWLINE,
|
|
26
|
+
OneLineCommentStyle,
|
|
27
|
+
SourceAnalyseConfig,
|
|
28
|
+
)
|
|
29
|
+
|
|
30
|
+
# initialize logger
|
|
31
|
+
logger = logging.getLogger(__name__)
|
|
32
|
+
logger.setLevel(logging.INFO)
|
|
33
|
+
# log to the console
|
|
34
|
+
console = logging.StreamHandler()
|
|
35
|
+
console.setLevel(logging.INFO)
|
|
36
|
+
logger.addHandler(console)
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
class AnalyseWarningType(TypedDict):
|
|
40
|
+
file_path: str
|
|
41
|
+
lineno: int
|
|
42
|
+
msg: str
|
|
43
|
+
type: str
|
|
44
|
+
sub_type: str
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
@dataclass
|
|
48
|
+
class AnalyseWarning:
|
|
49
|
+
file_path: str
|
|
50
|
+
lineno: int
|
|
51
|
+
msg: str
|
|
52
|
+
type: str
|
|
53
|
+
sub_type: str
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
class SourceAnalyse:
|
|
57
|
+
def __init__(
|
|
58
|
+
self,
|
|
59
|
+
analyse_config: SourceAnalyseConfig,
|
|
60
|
+
) -> None:
|
|
61
|
+
self.analyse_config = analyse_config
|
|
62
|
+
self.src_files: list[SourceFile] = []
|
|
63
|
+
self.src_comments: list[SourceComment] = []
|
|
64
|
+
self.need_id_refs: list[NeedIdRefs] = []
|
|
65
|
+
self.oneline_needs: list[OneLineNeed] = []
|
|
66
|
+
self.marked_rst: list[MarkedRst] = []
|
|
67
|
+
self.all_marked_content: list[NeedIdRefs | OneLineNeed | MarkedRst] = []
|
|
68
|
+
self.git_root: Path | None = utils.locate_git_root(self.analyse_config.src_dir)
|
|
69
|
+
self.git_remote_url: str | None = (
|
|
70
|
+
utils.get_remote_url(self.git_root) if self.git_root else None
|
|
71
|
+
)
|
|
72
|
+
self.git_commit_rev: str | None = (
|
|
73
|
+
utils.get_current_rev(self.git_root) if self.git_root else None
|
|
74
|
+
)
|
|
75
|
+
self.project_path: Path = (
|
|
76
|
+
self.git_root if self.git_root else self.analyse_config.src_dir
|
|
77
|
+
)
|
|
78
|
+
self.oneline_warnings: list[AnalyseWarning] = []
|
|
79
|
+
|
|
80
|
+
def get_src_strings(self) -> Generator[tuple[Path, bytes], Any, None]: # type: ignore[explicit-any]
|
|
81
|
+
"""Load source files and extract their content."""
|
|
82
|
+
for src_path in self.analyse_config.src_files:
|
|
83
|
+
if not utils.is_text_file(src_path):
|
|
84
|
+
continue
|
|
85
|
+
with src_path.open("r", encoding="utf-8", newline="") as f:
|
|
86
|
+
# Normalize all line endings to Unix LF
|
|
87
|
+
text = f.read()
|
|
88
|
+
text = text.replace("\r\n", "\n").replace("\r", "\n")
|
|
89
|
+
yield src_path, text.encode("utf-8")
|
|
90
|
+
|
|
91
|
+
def create_src_objects(self) -> None:
|
|
92
|
+
parser, query = utils.init_tree_sitter(self.analyse_config.comment_type)
|
|
93
|
+
|
|
94
|
+
for src_path, src_string in self.get_src_strings():
|
|
95
|
+
comments: list[TreeSitterNode] | None = utils.extract_comments(
|
|
96
|
+
src_string, parser, query
|
|
97
|
+
)
|
|
98
|
+
if not comments:
|
|
99
|
+
continue
|
|
100
|
+
src_comments: list[SourceComment] = [
|
|
101
|
+
SourceComment(node) for node in comments
|
|
102
|
+
]
|
|
103
|
+
|
|
104
|
+
src_file = SourceFile(src_path.absolute())
|
|
105
|
+
src_file.add_comments(src_comments)
|
|
106
|
+
self.src_files.append(src_file)
|
|
107
|
+
self.src_comments.extend(src_comments)
|
|
108
|
+
|
|
109
|
+
logger.info(f"Source files loaded: {len(self.src_files)}")
|
|
110
|
+
logger.info(f"Source comments extracted: {len(self.src_comments)}")
|
|
111
|
+
|
|
112
|
+
def extract_marker(
|
|
113
|
+
self,
|
|
114
|
+
text: str,
|
|
115
|
+
) -> Generator[tuple[str, list[str], int, int, int], None, None]:
|
|
116
|
+
lines = text.splitlines()
|
|
117
|
+
row_offset = 0
|
|
118
|
+
for line in lines:
|
|
119
|
+
for marker in self.analyse_config.need_id_refs_config.markers:
|
|
120
|
+
marker_idx = line.find(marker)
|
|
121
|
+
if marker_idx == -1:
|
|
122
|
+
continue
|
|
123
|
+
markered_text = line[marker_idx + len(marker) :].strip()
|
|
124
|
+
need_ids = markered_text.replace(",", " ").split()
|
|
125
|
+
start_column = marker_idx + len(marker)
|
|
126
|
+
end_column = start_column + len(markered_text)
|
|
127
|
+
yield marker, need_ids, row_offset, start_column, end_column
|
|
128
|
+
row_offset += 1
|
|
129
|
+
|
|
130
|
+
def extract_anchors(
|
|
131
|
+
self,
|
|
132
|
+
text: str,
|
|
133
|
+
filepath: Path,
|
|
134
|
+
tagged_scope: TreeSitterNode | None,
|
|
135
|
+
src_comment: SourceComment,
|
|
136
|
+
) -> list[NeedIdRefs]:
|
|
137
|
+
"""Extract need-ids-refs from a comment."""
|
|
138
|
+
anchors: list[NeedIdRefs] = []
|
|
139
|
+
for (
|
|
140
|
+
marker,
|
|
141
|
+
need_ids,
|
|
142
|
+
row_offset,
|
|
143
|
+
start_column,
|
|
144
|
+
end_column,
|
|
145
|
+
) in self.extract_marker(text):
|
|
146
|
+
lineno = src_comment.node.start_point.row + row_offset + 1
|
|
147
|
+
remote_url = self.git_remote_url
|
|
148
|
+
if self.git_remote_url and self.git_commit_rev:
|
|
149
|
+
remote_url = utils.form_https_url(
|
|
150
|
+
self.git_remote_url,
|
|
151
|
+
self.git_commit_rev,
|
|
152
|
+
self.project_path,
|
|
153
|
+
filepath,
|
|
154
|
+
lineno,
|
|
155
|
+
)
|
|
156
|
+
source_map: SourceMap = {
|
|
157
|
+
"start": {
|
|
158
|
+
"row": lineno - 1,
|
|
159
|
+
"column": start_column,
|
|
160
|
+
},
|
|
161
|
+
"end": {
|
|
162
|
+
"row": lineno - 1,
|
|
163
|
+
"column": end_column,
|
|
164
|
+
},
|
|
165
|
+
}
|
|
166
|
+
anchors.append(
|
|
167
|
+
NeedIdRefs(
|
|
168
|
+
filepath,
|
|
169
|
+
remote_url,
|
|
170
|
+
source_map,
|
|
171
|
+
src_comment,
|
|
172
|
+
tagged_scope,
|
|
173
|
+
need_ids,
|
|
174
|
+
marker,
|
|
175
|
+
)
|
|
176
|
+
)
|
|
177
|
+
return anchors
|
|
178
|
+
|
|
179
|
+
def extract_oneline_need(
|
|
180
|
+
self,
|
|
181
|
+
text: str,
|
|
182
|
+
src_comment: SourceComment,
|
|
183
|
+
oneline_comment_style: OneLineCommentStyle,
|
|
184
|
+
) -> Generator[tuple[dict[str, str | list[str] | int], int]]:
|
|
185
|
+
lines = text.splitlines(keepends=True)
|
|
186
|
+
row_offset = 0
|
|
187
|
+
if len(lines) == 1:
|
|
188
|
+
# single line comment has no newline char in the extracted comment
|
|
189
|
+
lines[0] = f"{lines[0]}{UNIX_NEWLINE}"
|
|
190
|
+
|
|
191
|
+
for line in lines:
|
|
192
|
+
resolved = oneline_parser(line, oneline_comment_style)
|
|
193
|
+
if not resolved:
|
|
194
|
+
row_offset += 1
|
|
195
|
+
continue
|
|
196
|
+
if isinstance(resolved, OnelineParserInvalidWarning):
|
|
197
|
+
if not src_comment.source_file:
|
|
198
|
+
row_offset += 1
|
|
199
|
+
continue
|
|
200
|
+
self.oneline_warnings.append(
|
|
201
|
+
AnalyseWarning(
|
|
202
|
+
str(src_comment.source_file.filepath),
|
|
203
|
+
src_comment.node.start_point.row + row_offset + 1,
|
|
204
|
+
resolved.msg,
|
|
205
|
+
MarkedContentType.need,
|
|
206
|
+
resolved.sub_type.value,
|
|
207
|
+
)
|
|
208
|
+
)
|
|
209
|
+
row_offset += 1
|
|
210
|
+
continue
|
|
211
|
+
yield resolved, row_offset
|
|
212
|
+
row_offset += 1
|
|
213
|
+
|
|
214
|
+
def extract_oneline_needs(
|
|
215
|
+
self,
|
|
216
|
+
text: str,
|
|
217
|
+
filepath: Path,
|
|
218
|
+
tagged_scope: TreeSitterNode | None,
|
|
219
|
+
src_comment: SourceComment,
|
|
220
|
+
oneline_comment_style: OneLineCommentStyle,
|
|
221
|
+
) -> list[OneLineNeed]:
|
|
222
|
+
row_offset = 0
|
|
223
|
+
oneline_needs = []
|
|
224
|
+
for resolved, row_offset in self.extract_oneline_need(
|
|
225
|
+
text, src_comment, oneline_comment_style
|
|
226
|
+
):
|
|
227
|
+
lineno = src_comment.node.start_point.row + row_offset + 1
|
|
228
|
+
remote_url = self.git_remote_url
|
|
229
|
+
if self.git_remote_url and self.git_commit_rev:
|
|
230
|
+
remote_url = utils.form_https_url(
|
|
231
|
+
self.git_remote_url,
|
|
232
|
+
self.git_commit_rev,
|
|
233
|
+
self.project_path,
|
|
234
|
+
filepath,
|
|
235
|
+
lineno,
|
|
236
|
+
)
|
|
237
|
+
source_map: SourceMap = {
|
|
238
|
+
"start": {
|
|
239
|
+
"row": lineno - 1,
|
|
240
|
+
"column": resolved["start_column"], # type: ignore[typeddict-item] # dynamic keys
|
|
241
|
+
},
|
|
242
|
+
"end": {
|
|
243
|
+
"row": lineno - 1,
|
|
244
|
+
"column": resolved["end_column"], # type: ignore[typeddict-item] # dynamic keys
|
|
245
|
+
},
|
|
246
|
+
}
|
|
247
|
+
del resolved["start_column"]
|
|
248
|
+
del resolved["end_column"]
|
|
249
|
+
oneline_needs.append(
|
|
250
|
+
OneLineNeed(
|
|
251
|
+
filepath,
|
|
252
|
+
remote_url,
|
|
253
|
+
source_map,
|
|
254
|
+
src_comment,
|
|
255
|
+
tagged_scope,
|
|
256
|
+
resolved, # type: ignore[arg-type] # int arguments were deleted
|
|
257
|
+
)
|
|
258
|
+
)
|
|
259
|
+
return oneline_needs
|
|
260
|
+
|
|
261
|
+
def extract_marked_rst(
|
|
262
|
+
self,
|
|
263
|
+
text: str,
|
|
264
|
+
filepath: Path,
|
|
265
|
+
tagged_scope: TreeSitterNode | None,
|
|
266
|
+
src_comment: SourceComment,
|
|
267
|
+
) -> MarkedRst | None:
|
|
268
|
+
"""Extract marked rst from a comment.
|
|
269
|
+
|
|
270
|
+
Presumably, only one marked rst text in a comment.
|
|
271
|
+
"""
|
|
272
|
+
extracted_rst = utils.extract_rst(
|
|
273
|
+
text,
|
|
274
|
+
self.analyse_config.marked_rst_config.start_sequence,
|
|
275
|
+
self.analyse_config.marked_rst_config.end_sequence,
|
|
276
|
+
)
|
|
277
|
+
if not extracted_rst:
|
|
278
|
+
return None
|
|
279
|
+
if UNIX_NEWLINE in extracted_rst["rst_text"]:
|
|
280
|
+
rst_text = utils.remove_leading_sequences(extracted_rst["rst_text"], ["*"])
|
|
281
|
+
else:
|
|
282
|
+
rst_text = extracted_rst["rst_text"]
|
|
283
|
+
lineno = src_comment.node.start_point.row + extracted_rst["row_offset"] + 1
|
|
284
|
+
remote_url = self.git_remote_url
|
|
285
|
+
if self.git_remote_url and self.git_commit_rev:
|
|
286
|
+
remote_url = utils.form_https_url(
|
|
287
|
+
self.git_remote_url,
|
|
288
|
+
self.git_commit_rev,
|
|
289
|
+
self.project_path,
|
|
290
|
+
filepath,
|
|
291
|
+
lineno,
|
|
292
|
+
)
|
|
293
|
+
source_map: SourceMap = {
|
|
294
|
+
"start": {
|
|
295
|
+
"row": lineno - 1,
|
|
296
|
+
"column": extracted_rst["start_idx"],
|
|
297
|
+
},
|
|
298
|
+
"end": {
|
|
299
|
+
"row": lineno - 1,
|
|
300
|
+
"column": extracted_rst["end_idx"],
|
|
301
|
+
},
|
|
302
|
+
}
|
|
303
|
+
return MarkedRst(
|
|
304
|
+
filepath,
|
|
305
|
+
remote_url,
|
|
306
|
+
source_map,
|
|
307
|
+
src_comment,
|
|
308
|
+
tagged_scope,
|
|
309
|
+
rst_text,
|
|
310
|
+
)
|
|
311
|
+
|
|
312
|
+
def extract_marked_content(self) -> None:
|
|
313
|
+
for src_comment in self.src_comments:
|
|
314
|
+
text = (
|
|
315
|
+
src_comment.node.text.decode("utf-8") if src_comment.node.text else None
|
|
316
|
+
)
|
|
317
|
+
if not text:
|
|
318
|
+
continue
|
|
319
|
+
filepath = (
|
|
320
|
+
src_comment.source_file.filepath if src_comment.source_file else None
|
|
321
|
+
)
|
|
322
|
+
if not filepath:
|
|
323
|
+
continue
|
|
324
|
+
tagged_scope: TreeSitterNode | None = utils.find_associated_scope(
|
|
325
|
+
src_comment.node
|
|
326
|
+
)
|
|
327
|
+
if self.analyse_config.get_need_id_refs:
|
|
328
|
+
anchors = self.extract_anchors(
|
|
329
|
+
text, filepath, tagged_scope, src_comment
|
|
330
|
+
)
|
|
331
|
+
self.need_id_refs.extend(anchors)
|
|
332
|
+
|
|
333
|
+
if self.analyse_config.get_oneline_needs:
|
|
334
|
+
oneline_needs = self.extract_oneline_needs(
|
|
335
|
+
text,
|
|
336
|
+
filepath,
|
|
337
|
+
tagged_scope,
|
|
338
|
+
src_comment,
|
|
339
|
+
self.analyse_config.oneline_comment_style,
|
|
340
|
+
)
|
|
341
|
+
self.oneline_needs.extend(oneline_needs)
|
|
342
|
+
if self.analyse_config.get_rst:
|
|
343
|
+
marked_rst = self.extract_marked_rst(
|
|
344
|
+
text, filepath, tagged_scope, src_comment
|
|
345
|
+
)
|
|
346
|
+
if marked_rst:
|
|
347
|
+
self.marked_rst.append(marked_rst)
|
|
348
|
+
|
|
349
|
+
if self.analyse_config.get_need_id_refs:
|
|
350
|
+
logger.info(f"Need-id-refs extracted: {len(self.need_id_refs)}")
|
|
351
|
+
if self.analyse_config.get_oneline_needs:
|
|
352
|
+
logger.info(f"Oneline needs extracted: {len(self.oneline_needs)}")
|
|
353
|
+
if self.analyse_config.get_rst:
|
|
354
|
+
logger.info(f"Marked rst extracted: {len(self.marked_rst)}")
|
|
355
|
+
|
|
356
|
+
def merge_marked_content(self) -> None:
|
|
357
|
+
self.all_marked_content.extend(self.need_id_refs)
|
|
358
|
+
self.oneline_needs.sort(key=lambda x: x.source_map["start"]["row"])
|
|
359
|
+
self.all_marked_content.extend(self.oneline_needs)
|
|
360
|
+
self.all_marked_content.extend(self.marked_rst)
|
|
361
|
+
self.all_marked_content.sort(
|
|
362
|
+
key=lambda x: (x.filepath, x.source_map["start"]["row"])
|
|
363
|
+
)
|
|
364
|
+
|
|
365
|
+
def dump_marked_content(self, outdir: Path) -> None:
|
|
366
|
+
output_path = outdir / "marked_content.json"
|
|
367
|
+
if not output_path.parent.exists():
|
|
368
|
+
output_path.parent.mkdir(parents=True)
|
|
369
|
+
to_dump = [
|
|
370
|
+
marked_content.to_dict() for marked_content in self.all_marked_content
|
|
371
|
+
]
|
|
372
|
+
with output_path.open("w") as f:
|
|
373
|
+
json.dump(to_dump, f)
|
|
374
|
+
logger.info(f"Marked content dumped to {output_path}")
|
|
375
|
+
|
|
376
|
+
def run(self) -> None:
|
|
377
|
+
self.create_src_objects()
|
|
378
|
+
self.extract_marked_content()
|
|
379
|
+
self.merge_marked_content()
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
from dataclasses import dataclass, field
|
|
2
|
+
from enum import Enum
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
from typing import TypedDict
|
|
5
|
+
|
|
6
|
+
from tree_sitter import Node as TreeSitterNode
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class MarkedContentType(str, Enum):
|
|
10
|
+
need = "need"
|
|
11
|
+
need_id_refs = "need-id-refs"
|
|
12
|
+
rst = "rst"
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class SourceComment:
|
|
16
|
+
def __init__(self, node: TreeSitterNode) -> None:
|
|
17
|
+
self.node: TreeSitterNode = node
|
|
18
|
+
self.source_file: SourceFile | None = None
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class SourceFile:
|
|
22
|
+
def __init__(self, filepath: Path) -> None:
|
|
23
|
+
self.filepath: Path = filepath
|
|
24
|
+
self.src_comments: list[SourceComment] = []
|
|
25
|
+
|
|
26
|
+
def add_comment(self, comment: SourceComment) -> None:
|
|
27
|
+
self.src_comments.append(comment)
|
|
28
|
+
comment.source_file = self
|
|
29
|
+
|
|
30
|
+
def add_comments(self, comments: list[SourceComment]) -> None:
|
|
31
|
+
for comment in comments:
|
|
32
|
+
self.add_comment(comment)
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
class Position(TypedDict):
|
|
36
|
+
row: int
|
|
37
|
+
column: int
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
class SourceMap(TypedDict):
|
|
41
|
+
start: Position
|
|
42
|
+
end: Position
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
@dataclass
|
|
46
|
+
class Metadata:
|
|
47
|
+
filepath: Path
|
|
48
|
+
remote_url: str | None
|
|
49
|
+
source_map: SourceMap
|
|
50
|
+
source_comment: SourceComment
|
|
51
|
+
tagged_scope: TreeSitterNode | None
|
|
52
|
+
type: MarkedContentType
|
|
53
|
+
|
|
54
|
+
def to_dict(self) -> dict[str, str | int | list[str]]:
|
|
55
|
+
obj = self.__dict__.copy()
|
|
56
|
+
obj["filepath"] = str(self.filepath)
|
|
57
|
+
obj["tagged_scope"] = (
|
|
58
|
+
str(self.tagged_scope.text.decode("utf-8"))
|
|
59
|
+
if self.tagged_scope and self.tagged_scope.text
|
|
60
|
+
else None
|
|
61
|
+
)
|
|
62
|
+
obj["type"] = self.type.value
|
|
63
|
+
del obj["source_comment"]
|
|
64
|
+
return obj
|
|
65
|
+
|
|
66
|
+
def add_src_comment(self, src_comment: SourceComment) -> None:
|
|
67
|
+
self.source_comment = src_comment
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
@dataclass
|
|
71
|
+
class NeedIdRefs(Metadata):
|
|
72
|
+
need_ids: list[str]
|
|
73
|
+
marker: str
|
|
74
|
+
type: MarkedContentType = field(init=False, default=MarkedContentType.need_id_refs)
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
@dataclass
|
|
78
|
+
class OneLineNeed(Metadata):
|
|
79
|
+
need: dict[str, str | list[str]]
|
|
80
|
+
type: MarkedContentType = field(init=False, default=MarkedContentType.need)
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
@dataclass
|
|
84
|
+
class MarkedRst(Metadata):
|
|
85
|
+
rst: str
|
|
86
|
+
type: MarkedContentType = field(init=False, default=MarkedContentType.rst)
|