Skip to content

Commit 8ee477e

Browse files
floroksubmarco
authored andcommitted
✨ Add JSONC language support for comment parsing and traceability
Adds jsonc language support, also checks .json files if they start with a comment (see jsonc.org). Signed-off-by: Florian Roks <florian.roks@mercedes-benz.com>
1 parent a76f8fd commit 8ee477e

14 files changed

Lines changed: 235 additions & 12 deletions

File tree

docs/source/components/analyse.rst

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -47,7 +47,7 @@ Limitations
4747

4848
**Current Limitations:**
4949

50-
- **Language Support**: C/C++ (``//``, ``/* */``), C# (``//``, ``/* */``, ``///``), Python (``#``), YAML (``#``), Rust (``//``, ``/* */``, ``///``) and Go (``//``, ``/* */``) comment styles are supported
50+
- **Language Support**: C/C++ (``//``, ``/* */``), C# (``//``, ``/* */``, ``///``), Python (``#``), YAML (``#``), Rust (``//``, ``/* */``, ``///``), Go (``//``, ``/* */``) and JSONC (``//``, ``/* */``) comment styles are supported
5151
- **Single Comment Style**: Each analysis run processes only one comment style at a time
5252

5353
Extraction Examples

docs/source/components/configuration.rst

Lines changed: 11 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -271,7 +271,7 @@ Specifies the comment syntax style used in the source code files. This determine
271271

272272
**Type:** ``str``
273273
**Default:** ``"cpp"``
274-
**Supported values:** ``"cpp"``, ``"python"``, ``"cs"``, ``"yaml"``, ``"rust"``, ``"go"``
274+
**Supported values:** ``"cpp"``, ``"python"``, ``"cs"``, ``"yaml"``, ``"rust"``, ``"go"``, ``"jsonc"``
275275

276276
.. code-block:: toml
277277
@@ -316,10 +316,16 @@ Specifies the comment syntax style used in the source code files. This determine
316316
``//!`` (inner doc comments)
317317
- ``.rs``
318318
* - Go
319-
- ``"go"``
320-
- ``//`` (single-line),
321-
``/* */`` (multi-line)
322-
- ``.go``
319+
- ``"go"``
320+
- ``//`` (single-line),
321+
``/* */`` (multi-line)
322+
- ``.go``
323+
* - JSON with Comments (JSONC)
324+
- ``"jsonc"``
325+
- ``//`` (single-line),
326+
``/* */`` (multi-line)
327+
- ``.jsonc`` (always); ``.json`` only when the file opens with a comment
328+
(e.g. the mode line ``// -*- mode: jsonc -*-``)
323329

324330
.. note:: Future versions may support additional programming languages.
325331

docs/source/components/features.rst

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -207,6 +207,33 @@ Features
207207
.. fault:: Sphinx-codelinks halucinates traceability objects in Go
208208
:id: FAULT_GO_2
209209

210+
.. feature:: JSONC Language Support
211+
:id: FE_JSONC
212+
213+
Support for defining traceability objects in JSON with Comments (JSONC) files.
214+
215+
The JSONC parser leverages tree-sitter to identify and extract single-line (``//``)
216+
and multi-line (``/* */``) comments from JSON data, associating each marker with the
217+
surrounding data structure such as the key/value pair, array item, or object it
218+
annotates.
219+
220+
``.jsonc`` files are always parsed as JSONC. A ``.json`` file is only treated as JSONC
221+
when it opens with a comment (e.g. the mode line ``// -*- mode: jsonc -*-``), following
222+
the `JSONC filename convention <https://jsonc.org/#filename-extension>`_.
223+
224+
Key capabilities:
225+
226+
* Detection of inline and leading comments
227+
* Association of comments with key/value pairs and array items
228+
* Support for both ``//`` and ``/* */`` comment styles
229+
* Opt-in handling of ``.json`` files via a leading comment
230+
231+
.. fault:: Traceability objects are not detected in JSONC
232+
:id: FAULT_JSONC_1
233+
234+
.. fault:: Sphinx-codelinks hallucinates traceability objects in JSONC
235+
:id: FAULT_JSONC_2
236+
210237
.. feature:: Customized comment styles
211238
:id: FE_CMT
212239

pyproject.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@ dependencies = [
3131
"tree-sitter-yaml>=0.7.1",
3232
"tree-sitter-rust>=0.23.0",
3333
"tree-sitter-go>=0.23.0",
34+
"tree-sitter-json>=0.24.8",
3435
]
3536

3637
[build-system]

src/sphinx_codelinks/analyse/utils.py

Lines changed: 60 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,8 @@
3636
"type_declaration",
3737
"type_spec",
3838
},
39+
# @JSONC Scope Node Types, IMPL_JSONC_2, impl, [FE_JSONC]
40+
CommentType.jsonc: {"pair", "object", "array", "document"},
3941
}
4042

4143
logger = get_logger(__name__)
@@ -65,6 +67,19 @@
6567
GO_QUERY = """
6668
(comment) @comment
6769
"""
70+
JSONC_QUERY = """(comment) @comment"""
71+
72+
# JSON value node types that can be associated with a comment.
73+
JSON_STRUCTURE_TYPES = {
74+
"pair",
75+
"object",
76+
"array",
77+
"string",
78+
"number",
79+
"true",
80+
"false",
81+
"null",
82+
}
6883

6984

7085
def is_text_file(filepath: Path, sample_size: int = 2048) -> bool:
@@ -82,7 +97,7 @@ def is_text_file(filepath: Path, sample_size: int = 2048) -> bool:
8297
return False
8398

8499

85-
# @Tree-sitter parser initialization for multiple languages, IMPL_LANG_1, impl, [FE_C_SUPPORT, FE_CPP, FE_PY, FE_YAML, FE_RUST, FE_GO]
100+
# @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]
86101
def init_tree_sitter(comment_type: CommentType) -> tuple[Parser, Query]:
87102
if comment_type == CommentType.cpp:
88103
import tree_sitter_cpp # noqa: PLC0415
@@ -114,6 +129,11 @@ def init_tree_sitter(comment_type: CommentType) -> tuple[Parser, Query]:
114129

115130
parsed_language = Language(tree_sitter_go.language())
116131
query = Query(parsed_language, GO_QUERY)
132+
elif comment_type == CommentType.jsonc:
133+
import tree_sitter_json # noqa: PLC0415
134+
135+
parsed_language = Language(tree_sitter_json.language())
136+
query = Query(parsed_language, JSONC_QUERY)
117137
else:
118138
raise ValueError(f"Unsupported comment style: {comment_type}")
119139
parser = Parser(parsed_language)
@@ -213,8 +233,11 @@ def find_yaml_next_structure(node: TreeSitterNode) -> TreeSitterNode | None:
213233
return None
214234

215235

216-
def find_yaml_prev_sibling_on_same_row(node: TreeSitterNode) -> TreeSitterNode | None:
217-
"""Find a previous named sibling that is on the same row as the comment."""
236+
def find_prev_sibling_on_same_row(node: TreeSitterNode) -> TreeSitterNode | None:
237+
"""Find a previous named sibling that is on the same row as the comment.
238+
239+
Grammar-agnostic: used to detect inline comments in both YAML and JSONC.
240+
"""
218241
comment_row = node.start_point.row
219242
current = node.prev_named_sibling
220243

@@ -235,7 +258,7 @@ def find_yaml_prev_sibling_on_same_row(node: TreeSitterNode) -> TreeSitterNode |
235258
def find_yaml_associated_structure(node: TreeSitterNode) -> TreeSitterNode | None:
236259
"""Find the YAML structure (key-value pair, list item, etc.) associated with a comment."""
237260
# First, check if this is an inline comment by looking for a previous sibling on the same row
238-
prev_sibling_same_row = find_yaml_prev_sibling_on_same_row(node)
261+
prev_sibling_same_row = find_prev_sibling_on_same_row(node)
239262
if prev_sibling_same_row:
240263
return prev_sibling_same_row
241264

@@ -254,6 +277,35 @@ def find_yaml_associated_structure(node: TreeSitterNode) -> TreeSitterNode | Non
254277
return None
255278

256279

280+
def find_jsonc_associated_structure(node: TreeSitterNode) -> TreeSitterNode | None:
281+
"""Find the JSON structure (key/value pair, value, list item) for a comment.
282+
283+
JSON is data rather than code, so association follows the same intent as YAML:
284+
an inline comment belongs to the value on its row, a leading comment belongs to
285+
the following structure, otherwise it belongs to the enclosing structure.
286+
"""
287+
# Inline comment: a value/pair on the same row, before the comment
288+
prev_sibling_same_row = find_prev_sibling_on_same_row(node)
289+
if prev_sibling_same_row:
290+
return prev_sibling_same_row
291+
292+
# Leading comment: the next structure following the comment
293+
current = node.next_named_sibling
294+
while current:
295+
if current.type in JSON_STRUCTURE_TYPES:
296+
return current
297+
current = current.next_named_sibling
298+
299+
# Otherwise: the enclosing structure
300+
parent = node.parent
301+
while parent:
302+
if parent.type in {"pair", "object", "array"}:
303+
return parent
304+
parent = parent.parent
305+
306+
return None
307+
308+
257309
def find_associated_scope(
258310
node: TreeSitterNode, comment_type: CommentType = CommentType.cpp
259311
) -> TreeSitterNode | None:
@@ -262,6 +314,10 @@ def find_associated_scope(
262314
# YAML uses different structure association logic
263315
return find_yaml_associated_structure(node)
264316

317+
if comment_type == CommentType.jsonc:
318+
# JSONC uses data-aware structure association logic
319+
return find_jsonc_associated_structure(node)
320+
265321
if node.type == CommentCategory.docstring:
266322
# Only for python's docstring
267323
return find_enclosing_scope(node, comment_type)

src/sphinx_codelinks/source_discover/config.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
"yaml": ["yml", "yaml"],
1313
"rust": ["rs"],
1414
"go": ["go"],
15+
"jsonc": ["jsonc", "json"],
1516
}
1617

1718

@@ -24,6 +25,8 @@ class CommentType(str, Enum):
2425
rust = "rust"
2526
# @Support Go style comments, IMPL_GO_1, impl, [FE_GO];
2627
go = "go"
28+
# @Support JSONC style comments, IMPL_JSONC_1, impl, [FE_JSONC];
29+
jsonc = "jsonc"
2730

2831

2932
class SourceDiscoverSectionConfigType(TypedDict, total=False):

src/sphinx_codelinks/source_discover/source_discover.py

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,10 +6,28 @@
66

77
from sphinx_codelinks.source_discover.config import (
88
COMMENT_FILETYPE,
9+
CommentType,
910
SourceDiscoverConfig,
1011
)
1112

1213

14+
def _json_starts_with_comment(filepath: Path, sample_size: int = 256) -> bool:
15+
"""Return True if a ``.json`` file's first non-whitespace content is a comment.
16+
17+
Used to decide whether a ``.json`` file should be treated as JSONC. Per
18+
https://jsonc.org/#filename-extension a ``.json`` file should only be treated as
19+
JSONC when it opens with a comment (e.g. the mode line ``// -*- mode: jsonc -*-``).
20+
"""
21+
try:
22+
with filepath.open("rb") as f:
23+
chunk = f.read(sample_size)
24+
except OSError:
25+
return False
26+
# strip a leading UTF-8 BOM, then leading whitespace
27+
text = chunk.removeprefix(b"\xef\xbb\xbf").lstrip()
28+
return text.startswith((b"//", b"/*"))
29+
30+
1331
# @Source code file discovery with gitignore support, IMPL_DISC_1, impl, [FE_DISCOVERY, FE_CLI_DISCOVER]
1432
class SourceDiscover:
1533
def __init__(self, src_discover_config: SourceDiscoverConfig):
@@ -75,6 +93,15 @@ def _discover(self) -> list[Path]:
7593
continue
7694
if self.file_types and filepath.suffix.lower() not in self.file_types:
7795
continue
96+
# @JSONC .json files require a leading comment, IMPL_JSONC_3, impl, [FE_JSONC]
97+
# A plain ``.json`` file is only treated as JSONC when it opens with a
98+
# comment; otherwise it is skipped under the ``jsonc`` comment type.
99+
if (
100+
self.src_discover_config.comment_type == CommentType.jsonc
101+
and filepath.suffix.lower() == ".json"
102+
and not _json_starts_with_comment(filepath)
103+
):
104+
continue
78105
# resolve() produces canonical absolute paths; follow_links only
79106
# controls whether the walker descends into symlinked directories
80107
discovered_files.append(filepath.resolve())

tests/data/jsonc/demo.jsonc

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
// -*- mode: jsonc -*-
2+
{
3+
// @JSONC alpha implementation, IMPL_JSONC_A, impl, [REQ_JSONC_1]
4+
"alpha": 1,
5+
"items": [
6+
"first", // @JSONC inline item, IMPL_JSONC_B, impl, [REQ_JSONC_2]
7+
"second"
8+
],
9+
/* Block comment with marker
10+
@JSONC beta implementation, IMPL_JSONC_C, impl, [REQ_JSONC_3]
11+
*/
12+
"beta": {
13+
"nested": true
14+
}
15+
}

tests/data/jsonc/plain.json

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
{
2+
"value": 42
3+
}
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
// -*- mode: jsonc -*-
2+
{
3+
// @JSONC modeline file, IMPL_JSONC_D, impl, [REQ_JSONC_4]
4+
"value": 42
5+
}

0 commit comments

Comments
 (0)