[docs] 1# @Test suite for tree-sitter parsing utilities and language support, TEST_LANG_1, test, [IMPL_LANG_1, IMPL_EXTR_1]
2import shutil
3import subprocess
4from pathlib import Path
5
6import pytest
7from tree_sitter import Node as TreeSitterNode
8from tree_sitter import Parser, Query
9
10from sphinx_codelinks.analyse import utils
11from sphinx_codelinks.source_discover.config import COMMENT_FILETYPE, CommentType
12
13
14# The fixtures build the parsers the way analysis does, through init_tree_sitter.
15@pytest.fixture(scope="session")
16def init_cpp_tree_sitter() -> tuple[Parser, Query]:
17 return utils.init_tree_sitter(CommentType.cpp)
18
19
20@pytest.fixture(scope="session")
21def init_python_tree_sitter() -> tuple[Parser, Query]:
22 return utils.init_tree_sitter(CommentType.python)
23
24
25@pytest.fixture(scope="session")
26def init_csharp_tree_sitter() -> tuple[Parser, Query]:
27 return utils.init_tree_sitter(CommentType.cs)
28
29
30@pytest.fixture(scope="session")
31def init_yaml_tree_sitter() -> tuple[Parser, Query]:
32 return utils.init_tree_sitter(CommentType.yaml)
33
34
35@pytest.fixture(scope="session")
36def init_rust_tree_sitter() -> tuple[Parser, Query]:
37 return utils.init_tree_sitter(CommentType.rust)
38
39
40@pytest.fixture(scope="session")
41def init_typescript_tree_sitter() -> tuple[Parser, Query]:
42 # A .tsx file: the TSX grammar, which the JSX fixtures below need
43 return utils.init_tree_sitter(CommentType.ts, Path("fixture.tsx"))
44
45
46@pytest.fixture(scope="session")
47def init_go_tree_sitter() -> tuple[Parser, Query]:
48 return utils.init_tree_sitter(CommentType.go)
49
50
51@pytest.fixture(scope="session")
52def init_jsonc_tree_sitter() -> tuple[Parser, Query]:
53 return utils.init_tree_sitter(CommentType.jsonc)
54
55
56@pytest.fixture(scope="session")
57def init_bash_tree_sitter() -> tuple[Parser, Query]:
58 return utils.init_tree_sitter(CommentType.bash)
59
60
61@pytest.fixture(scope="session")
62def init_powershell_tree_sitter() -> tuple[Parser, Query]:
63 return utils.init_tree_sitter(CommentType.powershell)
64
65
66def test_init_tree_sitter_powershell(init_powershell_tree_sitter):
67 """Both PowerShell comment forms, ``#`` and ``<# #>``, are comment nodes."""
68 parser, query = init_powershell_tree_sitter
69 comments = utils.extract_comments(b"# a\n<# b #>\nfunction f { }\n", parser, query)
70 assert comments
71 assert [comment.text for comment in comments] == [b"# a", b"<# b #>"]
72
73
74@pytest.mark.parametrize(
75 "code",
76 [
77 # a here-string whose ``#`` line is not its first line
78 b'$stub = @"\nfunction Test-It {\n# @Generated, IMPL_GEN, impl\n}\n"@\n',
79 # an interpolating here-string
80 b'$name = "px"\n$t = @"\nHello $name\n# @Interp, IMPL_INTERP, impl\n"@\n',
81 ],
82)
83def test_a_hash_line_in_a_powershell_here_string_is_no_comment(
84 code, init_powershell_tree_sitter
85):
86 """The grammar reads a ``#`` line inside an expandable here-string as a comment
87 node; it is string content, so extraction leaves it out."""
88 parser, query = init_powershell_tree_sitter
89 comments = utils.extract_comments(code + b"# real\nfunction f { }\n", parser, query)
90 assert comments
91 assert [comment.text for comment in comments] == [b"# real"]
92
93
94@pytest.mark.parametrize(
95 "code",
96 [
97 # an ordinary double-quoted string that interpolates before its ``#`` line
98 # and on it
99 b'$c = "one $x\n# @Fake, ID_FAKE, impl $y\nthree"\n',
100 # the interpolation first in the string
101 b'$c = "$x\n# @Fake, ID_FAKE, impl $y\nthree"\n',
102 # a subexpression is an interpolation too
103 b'$c = "one $($x)\n# @Fake, ID_FAKE, impl $($y)\nthree"\n',
104 # a string passed to a command, the ``#`` indented
105 b'Write-Host "one $x\n # @Fake $y, ID_FAKE, impl\nthree"\n',
106 # the ``#`` on the interpolation's own line
107 b'$c = "one $x # @Fake, ID_FAKE, impl $y\nthree"\n',
108 # right after the interpolation: the comment node starts with its ``#``
109 b'$c = "$x# @Fake, ID_FAKE, impl $y\nthree"\n',
110 ],
111)
112def test_a_hash_line_in_an_interpolating_powershell_string_is_no_comment(
113 code, init_powershell_tree_sitter
114):
115 """After an interpolation in a double-quoted string, the grammar may read a ``#``
116 that follows it past whitespace alone as a comment node inside the string, as it
117 does in a here-string: it is string content, never a marker."""
118 parser, query = init_powershell_tree_sitter
119 comments = utils.extract_comments(code + b"# real\nfunction f { }\n", parser, query)
120 assert comments
121 assert [comment.text for comment in comments] == [b"# real"]
122
123
124@pytest.mark.parametrize(
125 "code",
126 [
127 # the string's closing quote on the ``#`` line: the grammar's comment takes the
128 # quote in and lands under an ERROR node, in no string
129 b'$s = "one $x\n# @Fake, ID_F, impl"\n',
130 b'$s = "one $x # @Fake, ID_F, impl"\n',
131 b'Write-Host "$x\n # @Fake, ID_F, impl"\n',
132 ],
133)
134def test_a_powershell_comment_token_that_is_no_comment_is_left_out(
135 code, init_powershell_tree_sitter
136):
137 """A PowerShell comment opens with ``#`` or ``<#``; the comment node the grammar
138 makes of a ``#`` after an interpolation starts with the whitespace before it, and
139 where it is in no string node (the string's quote is lost to it), it is still no
140 comment of the file."""
141 parser, query = init_powershell_tree_sitter
142 comments = utils.extract_comments(code, parser, query) or []
143 assert [c.text for c in comments if not (c.text or b"").startswith(b"#")] == []
144
145
146@pytest.mark.parametrize(
147 "code",
148 [
149 # a subexpression in a here-string is code
150 b'$s = @"\n$( # in code\n 1 )\n"@\n',
151 # and in an ordinary double-quoted string
152 b'$s = "a $( # in code\n 1 ) b"\n',
153 ],
154)
155def test_a_comment_in_a_powershell_subexpression_is_a_comment(
156 code, init_powershell_tree_sitter
157):
158 """A comment inside ``$( … )`` is a comment of the code the string interpolates,
159 not string content: it is kept, string or not around it."""
160 parser, query = init_powershell_tree_sitter
161 comments = utils.extract_comments(code, parser, query)
162 assert comments
163 assert [comment.text for comment in comments] == [b"# in code"]
164
165
166def test_every_comment_type_is_wired():
167 """A comment type is a ``CommentType`` member plus an entry in each of several
168 tables; a member one of them misses fails here, rather than as a ``KeyError`` in
169 discovery or as no scope at all."""
170 members = set(CommentType)
171 assert set(COMMENT_FILETYPE) == {member.value for member in members}
172 # YAML and JSONC bind to data structures, not scopes (find_associated_scope)
173 assert set(utils.SCOPE_NODE_TYPES) == members - {
174 CommentType.yaml,
175 CommentType.jsonc,
176 }
177 assert set(utils.WRAPPER_TYPES) <= set(utils.ENTITY_NODE_TYPES)
178 assert set(utils.TRIVIA_TYPES) <= set(utils.ENTITY_NODE_TYPES)
179 for comment_type, predicates in utils.SCOPE_PREDICATES.items():
180 assert set(predicates) <= utils.SCOPE_NODE_TYPES[comment_type]
181 for member in members:
182 utils.init_tree_sitter(member)
183
184
185@pytest.mark.parametrize(
186 ("code", "result"),
187 [
188 (
189 b"""
190 // @req-id: need_001
191 void dummy_func1(){
192 }
193 """,
194 "void dummy_func1()",
195 ),
196 (
197 b"""
198 void dummy_func2(){
199 }
200 // @req-id: need_001
201 void dummy_func1(){
202 }
203 """,
204 "void dummy_func1()",
205 ),
206 (
207 b"""
208 void dummy_func1(){
209 a = 1;
210 /* @req-id: need_001 */
211 }
212 """,
213 "void dummy_func1()",
214 ),
215 (
216 b"""
217 void dummy_func1(){
218 // @req-id: need_001
219 a = 1;
220 }
221 void dummy_func2(){
222 }
223 """,
224 "void dummy_func1()",
225 ),
226 ],
227)
228def test_find_associated_scope_cpp(code, result, init_cpp_tree_sitter):
229 parser, query = init_cpp_tree_sitter
230 comments = utils.extract_comments(code, parser, query)
231 node: TreeSitterNode | None = utils.find_associated_scope(
232 comments[0], CommentType.cpp
233 )
234 assert node
235 assert node.text
236 func_def = node.text.decode("utf-8")
237 assert result in func_def
238
239
240#: What :func:`_attached_to` gives for a marker rule 7 attaches to nothing
241ABOVE_CONDITIONAL = utils.Unattached.above_conditional
242
243
244def _attached_to(
245 source: str, parser: Parser, query: Query
246) -> tuple[str, str, str] | utils.Unattached | None:
247 """What the comment holding ``marker`` in the C++ ``source`` is attached to: the
248 entity's node kind, its first line and the mechanism; :data:`ABOVE_CONDITIONAL`
249 for nothing by rule 7, ``None`` for nothing by any other rule."""
250 comments = utils.extract_comments(source.encode(), parser, query) or []
251 marker = next(c for c in comments if b"marker" in (c.text or b""))
252 association = utils.associate(marker, CommentType.cpp)
253 if association is None or isinstance(association, utils.Unattached):
254 return association
255 first_line = (association.node.text or b"").decode().splitlines()[0]
256 return association.node.type, first_line, association.mechanism
257
258
259#: The conditionals a marker can stand directly above, each the body of a namespace or a
260#: class with the marker as its first line
261CONDITIONAL_ARMS = {
262 "one-arm": "#if FEATURE_B\nvoid active() {}\n#endif\n",
263 "ifdef": "#ifdef FEATURE_B\nvoid active() {}\n#endif\n",
264 "ifndef": "#ifndef FEATURE_B\nvoid active() {}\n#endif\n",
265 "else-different-functions": (
266 "#if FEATURE_B\nvoid active() {}\n#else\nvoid fallback() {}\n#endif\n"
267 ),
268 "else-same-function": (
269 "#if FEATURE_B\nvoid active() { fast(); }\n"
270 "#else\nvoid active() { slow(); }\n#endif\n"
271 ),
272 "elif": (
273 "#if FEATURE_A\nvoid a() {}\n#elif FEATURE_B\nvoid b() {}\n"
274 "#else\nvoid c() {}\n#endif\n"
275 ),
276 "declaration-only": "#if FEATURE_B\nvoid declared();\n#endif\n",
277}
278
279#: The marker as the last line of an arm, directly above the next directive of the
280#: same conditional
281ARM_ENDINGS = {
282 "above-else": (
283 "#if FEATURE_B\nvoid active() {}\n// marker\n#else\nvoid fallback() {}\n#endif\n"
284 ),
285 "above-elif": (
286 "#if FEATURE_A\nvoid a() {}\n// marker\n#elif FEATURE_B\nvoid b() {}\n#endif\n"
287 ),
288 "above-elifdef": (
289 "#ifdef FEATURE_A\nvoid a() {}\n// marker\n#elifdef FEATURE_B\nvoid b() {}\n"
290 "#endif\n"
291 ),
292 "above-endif": "#if FEATURE_B\nvoid active() {}\n// marker\n#endif\n",
293 "above-endif-after-else": (
294 "#if FEATURE_B\nvoid a() {}\n#else\nvoid b() {}\n// marker\n#endif\n"
295 ),
296}
297
298CONTAINERS = {
299 "namespace": ("namespace lib {\n", "}\n"),
300 "class": ("class Widget {\n", "};\n"),
301}
302
303
304@pytest.mark.parametrize("container", list(CONTAINERS))
305@pytest.mark.parametrize(
306 "body",
307 [
308 *(
309 pytest.param(f"// marker\n{arms}", id=name)
310 for name, arms in CONDITIONAL_ARMS.items()
311 ),
312 *(pytest.param(body, id=name) for name, body in ARM_ENDINGS.items()),
313 pytest.param("/// marker\n" + CONDITIONAL_ARMS["one-arm"], id="doc-comment"),
314 pytest.param(
315 "// marker\n// and its second line\n" + CONDITIONAL_ARMS["one-arm"],
316 id="comment-run",
317 ),
318 pytest.param(
319 "/* marker\n */\n" + CONDITIONAL_ARMS["one-arm"], id="block-comment"
320 ),
321 ],
322)
323def test_marker_directly_above_a_conditional_is_on_nothing(
324 container: str, body: str, init_cpp_tree_sitter: tuple[Parser, Query]
325) -> None:
326 """A marker directly above a preprocessor conditional's line, in a namespace or a
327 class body, is attached to nothing, as clang attaches it: it would otherwise trace
328 every unit of the container, whichever arm is compiled. ``#elif``, ``#else`` and
329 ``#endif`` count as the opening directives do, and other comments may stand between
330 when no blank line does."""
331 head, tail = CONTAINERS[container]
332
333 assert _attached_to(head + body + tail, *init_cpp_tree_sitter) is ABOVE_CONDITIONAL
334
335
336@pytest.mark.parametrize(
337 "source",
338 [
339 pytest.param(
340 "struct S {\n// marker\n" + CONDITIONAL_ARMS["one-arm"] + "};\n",
341 id="struct",
342 ),
343 pytest.param(
344 "union U {\n// marker\n#if FEATURE_B\nint a;\n#endif\nfloat b;\n};\n",
345 id="union",
346 ),
347 pytest.param(
348 "enum E {\n// marker\n#if FEATURE_B\nA,\n#endif\nB,\n};\n", id="enum"
349 ),
350 pytest.param(
351 'namespace lib {\nextern "C" {\n// marker\n'
352 + CONDITIONAL_ARMS["declaration-only"]
353 + "}\n}\n",
354 id="extern-c-block-in-a-namespace",
355 ),
356 ],
357)
358def test_marker_directly_above_a_conditional_is_on_nothing_in_any_body(
359 source: str, init_cpp_tree_sitter: tuple[Parser, Query]
360) -> None:
361 """Not only in a namespace or class body: anywhere outside a function's body, a
362 struct, union or enum body and an ``extern "C"`` block included, a marker directly
363 above a conditional is attached to nothing, where the struct, union or enum, or
364 the namespace around the block, held it."""
365 assert _attached_to(source, *init_cpp_tree_sitter) is ABOVE_CONDITIONAL
366
367
368@pytest.mark.parametrize(
369 ("source", "expected"),
370 [
371 pytest.param(
372 "// marker\n" + CONDITIONAL_ARMS["one-arm"],
373 ABOVE_CONDITIONAL,
374 id="file-level-first",
375 ),
376 pytest.param(
377 "void first() {}\n// marker\n" + CONDITIONAL_ARMS["one-arm"],
378 ABOVE_CONDITIONAL,
379 id="file-level-after-an-item",
380 ),
381 pytest.param(
382 "// marker\n\n" + CONDITIONAL_ARMS["one-arm"], None, id="file-header"
383 ),
384 pytest.param("void first() {}\n// marker\n", None, id="file-level-at-the-end"),
385 pytest.param(
386 "void outer() {\n// marker\n#if FEATURE_B\nfast();\n#endif\n}\n",
387 ("function_definition", "void outer() {", "enclosing"),
388 id="function-body",
389 ),
390 pytest.param(
391 "namespace lib {\nvoid outer() {\n// marker\n#ifdef FEATURE_B\nfast();\n"
392 "#endif\n}\n}\n",
393 ("function_definition", "void outer() {", "enclosing"),
394 id="function-body-in-a-namespace",
395 ),
396 pytest.param(
397 "namespace lib {\nvoid first() {} // marker\n"
398 + CONDITIONAL_ARMS["one-arm"]
399 + "}\n",
400 ("function_definition", "void first() {}", "trailing"),
401 id="trailing",
402 ),
403 pytest.param(
404 "namespace lib {\n// marker\n\n" + CONDITIONAL_ARMS["one-arm"] + "}\n",
405 ("namespace_definition", "namespace lib {", "enclosing"),
406 id="blank-line-namespace",
407 ),
408 pytest.param(
409 "class Widget {\n// marker\n\n" + CONDITIONAL_ARMS["one-arm"] + "};\n",
410 ("class_specifier", "class Widget {", "enclosing"),
411 id="blank-line-class",
412 ),
413 pytest.param(
414 "namespace lib {\n// marker\n// and its second line\n\n"
415 + CONDITIONAL_ARMS["one-arm"]
416 + "}\n",
417 ("namespace_definition", "namespace lib {", "enclosing"),
418 id="blank-line-after-a-comment-run",
419 ),
420 pytest.param(
421 "namespace lib {\n#if FEATURE_B\nvoid active() {}\n// marker\n\n#endif\n}\n",
422 ("namespace_definition", "namespace lib {", "enclosing"),
423 id="blank-line-above-endif",
424 ),
425 pytest.param(
426 "namespace lib {\n// marker\n#define FAST 1\nvoid active() {}\n}\n",
427 ("namespace_definition", "namespace lib {", "enclosing"),
428 id="above-define",
429 ),
430 pytest.param(
431 'namespace lib {\n// marker\n#include "fast.h"\nvoid active() {}\n}\n',
432 ("namespace_definition", "namespace lib {", "enclosing"),
433 id="above-include",
434 ),
435 pytest.param(
436 "class Widget {\n// marker\n#pragma pack(1)\nvoid active() {}\n};\n",
437 ("class_specifier", "class Widget {", "enclosing"),
438 id="above-pragma",
439 ),
440 pytest.param(
441 "namespace lib {\n#if FEATURE_B\n// marker\nvoid active() {}\n#endif\n}\n",
442 ("function_definition", "void active() {}", "leading"),
443 id="inside-the-arm",
444 ),
445 pytest.param(
446 "void outer() {\nstruct L {\n// marker\n"
447 + CONDITIONAL_ARMS["one-arm"]
448 + "};\n}\n",
449 ("struct_specifier", "struct L {", "enclosing"),
450 id="class-local-to-a-function",
451 ),
452 pytest.param(
453 "namespace lib {\nauto l = []() {\n// marker\n#if FEATURE_B\nactive();\n"
454 "#endif\n};\nvoid after() {}\n}\n",
455 ("namespace_definition", "namespace lib {", "enclosing"),
456 id="lambda-in-a-namespace",
457 ),
458 pytest.param(
459 "namespace lib {\n// marker\n%:if FEATURE_B\nvoid active() {}\n%:endif\n}\n",
460 ("namespace_definition", "namespace lib {", "enclosing"),
461 id="digraph-if",
462 ),
463 pytest.param(
464 "namespace lib {\n#if FEATURE_B\nvoid active() {}\n// marker\n%:endif\n}\n",
465 ("namespace_definition", "namespace lib {", "enclosing"),
466 id="digraph-endif",
467 ),
468 pytest.param(
469 "namespace lib {\n#define F(x) \\\n /* marker */ \\\n # if x\n"
470 "void active() {}\n}\n",
471 ("namespace_definition", "namespace lib {", "enclosing"),
472 id="define-continuation",
473 ),
474 ],
475)
476def test_marker_near_a_conditional_keeps_its_attachment(
477 source: str,
478 expected: tuple[str, str, str] | utils.Unattached | None,
479 init_cpp_tree_sitter: tuple[Parser, Query],
480) -> None:
481 """Around a preprocessor conditional, every other marker keeps the attachment it
482 has: at file level it is on nothing anyway, by rule 7 when directly above the
483 conditional and by another rule (a file header, nothing after it) otherwise; in a function's body it is the
484 function's; a trailing marker is its row's entity's; a blank line between makes it
485 a comment on the namespace or class as a whole; any other directive (``#define``,
486 ``#include``, ``#pragma``) breaks the attachment to the next entity as any code
487 does; and inside the arm, the marker leads the definition, which is the remedy.
488
489 A lambda's body and a class defined in a function are a function's body too. And
490 the directive is read as the grammar reads it: a digraph (``%:if``) is no
491 conditional to it, nor is a ``# if`` line that continues a ``#define``."""
492 assert _attached_to(source, *init_cpp_tree_sitter) == expected
493
494
495@pytest.mark.parametrize(
496 ("code", "result"),
497 [
498 (
499 b"""
500 def dummy_func1():
501 # @req-id: need_001
502 pass
503 """,
504 "def dummy_func1()",
505 ),
506 (
507 b"""
508 def dummy_func1():
509 # @req-id: need_002
510 def dummy_func2():
511 pass
512 pass
513 """,
514 "def dummy_func2()",
515 ),
516 (
517 b"""
518 def dummy_func1():
519 '''@req-id: need_002'''
520 def nested_dummy_func():
521 pass
522 pass
523 """,
524 "def dummy_func1()",
525 ),
526 (
527 b"""
528 def dummy_func1():
529 def nested_dummy_func():
530 '''@req-id: need_002'''
531 pass
532 pass
533 """,
534 "def nested_dummy_func()",
535 ),
536 (
537 b"""
538 def dummy_func1():
539 def nested_dummy_func():
540 # @req-id: need_002
541 pass
542 pass
543 """,
544 "def nested_dummy_func()",
545 ),
546 (
547 b"""
548 def dummy_func1():
549 def nested_dummy_func():
550 pass
551 # @req-id: need_002
552 pass
553 """,
554 "def dummy_func1()",
555 ),
556 ],
557)
558def test_find_associated_scope_python(code, result, init_python_tree_sitter):
559 parser, query = init_python_tree_sitter
560 comments = utils.extract_comments(code, parser, query)
561 node: TreeSitterNode | None = utils.find_associated_scope(
562 comments[0], CommentType.python
563 )
564 assert node
565 assert node.text
566 func_def = node.text.decode("utf-8")
567 assert func_def.startswith(result)
568
569
570@pytest.mark.parametrize(
571 ("code", "result"),
572 [
573 (
574 b"""
575 // @req-id: need_001
576 public class DummyClass1
577 {
578 }
579 """,
580 "public class DummyClass1",
581 ),
582 (
583 b"""
584 public class DummyClass2
585 {
586 // @req-id: need_001
587 public void DummyFunc2()
588 {
589 }
590 }
591 """,
592 "public void DummyFunc2",
593 ),
594 (
595 b"""
596 public class DummyClass3
597 {
598 // @req-id: need_001
599 public string Property1 { get; set; }
600 }
601 """,
602 "public string Property1",
603 ),
604 ],
605)
606def test_find_associated_scope_csharp(code, result, init_csharp_tree_sitter):
607 parser, query = init_csharp_tree_sitter
608 comments = utils.extract_comments(code, parser, query)
609 node: TreeSitterNode | None = utils.find_associated_scope(
610 comments[0], CommentType.cs
611 )
612 assert node
613 assert node.text
614 func_def = node.text.decode("utf-8")
615 assert func_def.startswith(result)
616
617
618@pytest.mark.parametrize(
619 ("code", "result"),
620 [
621 (
622 b"""
623 # @req-id: need_001
624 database:
625 host: localhost
626 port: 5432
627 """,
628 "database:",
629 ),
630 (
631 b"""
632 services:
633 web:
634 # @req-id: need_002
635 image: nginx:latest
636 ports:
637 - "80:80"
638 """,
639 "image: nginx:latest",
640 ),
641 (
642 b"""
643 # @req-id: need_003
644 version: "3.8"
645 services:
646 app:
647 build: .
648 """,
649 "version:",
650 ),
651 (
652 b"""
653 items:
654 # @req-id: need_004
655 - name: item1
656 value: test
657 - name: item2
658 value: test2
659 """,
660 "- name: item1",
661 ),
662 ],
663)
664def test_find_associated_scope_yaml(code, result, init_yaml_tree_sitter):
665 parser, query = init_yaml_tree_sitter
666 comments = utils.extract_comments(code, parser, query)
667 node: TreeSitterNode | None = utils.find_associated_scope(
668 comments[0], CommentType.yaml
669 )
670 assert node
671 assert node.text
672 yaml_structure = node.text.decode("utf-8")
673 assert result in yaml_structure
674
675
676@pytest.mark.parametrize(
677 ("code", "result"),
678 [
679 (
680 b"""
681 // @req-id: need_001
682 fn dummy_func1() {
683 }
684 """,
685 "fn dummy_func1()",
686 ),
687 (
688 b"""
689 fn dummy_func2() {
690 }
691 // @req-id: need_001
692 fn dummy_func1() {
693 }
694 """,
695 "fn dummy_func1()",
696 ),
697 (
698 b"""
699 fn dummy_func1() {
700 let a = 1;
701 /* @req-id: need_001 */
702 }
703 """,
704 "fn dummy_func1()",
705 ),
706 (
707 b"""
708 fn dummy_func1() {
709 // @req-id: need_001
710 let a = 1;
711 }
712 fn dummy_func2() {
713 }
714 """,
715 "fn dummy_func1()",
716 ),
717 (
718 b"""
719 /// @req-id: need_001
720 fn dummy_func1() {
721 }
722 """,
723 "fn dummy_func1()",
724 ),
725 (
726 b"""
727 struct MyStruct {
728 // @req-id: need_001
729 field: i32,
730 }
731 """,
732 "struct MyStruct",
733 ),
734 ],
735)
736def test_find_associated_scope_rust(code, result, init_rust_tree_sitter):
737 parser, query = init_rust_tree_sitter
738 comments = utils.extract_comments(code, parser, query)
739 node: TreeSitterNode | None = utils.find_associated_scope(
740 comments[0], CommentType.rust
741 )
742 assert node
743 assert node.text
744 rust_def = node.text.decode("utf-8")
745 assert result in rust_def
746
747
748@pytest.mark.parametrize(
749 ("code", "result"),
750 [
751 # leading comment is associated with the following key/value pair
752 (
753 b'{\n // @req-id: need_001\n "alpha": 1\n}\n',
754 '"alpha": 1',
755 ),
756 # inline comment is associated with the array item on the same row
757 (
758 b'{\n "items": [\n "first", // @req-id: need_001\n "second"\n ]\n}\n',
759 '"first"',
760 ),
761 # inline comment is associated with the pair on the same row
762 (
763 b'{\n "alpha": 1, // @req-id: need_001\n "beta": 2\n}\n',
764 '"alpha": 1',
765 ),
766 # block comment is associated with the following pair
767 (
768 b'{\n /* @req-id: need_001 */\n "beta": 2\n}\n',
769 '"beta": 2',
770 ),
771 # trailing comment falls back to the enclosing object
772 (
773 b'{\n "alpha": 1\n // @req-id: need_001\n}\n',
774 '"alpha"',
775 ),
776 ],
777)
778def test_find_associated_scope_jsonc(code, result, init_jsonc_tree_sitter):
779 parser, query = init_jsonc_tree_sitter
780 comments = utils.extract_comments(code, parser, query)
781 node: TreeSitterNode | None = utils.find_associated_scope(
782 comments[0], CommentType.jsonc
783 )
784 assert node
785 assert node.text
786 jsonc_structure = node.text.decode("utf-8")
787 assert result in jsonc_structure
788
789
790@pytest.mark.parametrize(
791 ("code", "result"),
792 [
793 # comment above a POSIX-style function definition
794 (
795 b"""
796 # @req-id: need_001
797 greet() {
798 echo hi
799 }
800 """,
801 "greet()",
802 ),
803 # comment above the `function` keyword form
804 (
805 b"""
806 # @req-id: need_002
807 function greet {
808 echo hi
809 }
810 """,
811 "function greet",
812 ),
813 # comment inside a function body falls back to the enclosing function
814 (
815 b"""
816 greet() {
817 # @req-id: need_003
818 echo hi
819 }
820 """,
821 "greet()",
822 ),
823 ],
824)
825def test_find_associated_scope_bash(code, result, init_bash_tree_sitter):
826 parser, query = init_bash_tree_sitter
827 comments = utils.extract_comments(code, parser, query)
828 node: TreeSitterNode | None = utils.find_associated_scope(
829 comments[0], CommentType.bash
830 )
831 assert node
832 assert node.text
833 func_def = node.text.decode("utf-8")
834 assert func_def.startswith(result)
835
836
837@pytest.mark.parametrize(
838 ("code", "result"),
839 [
840 # comment above a function
841 (
842 b"""# @req-id: need_001
843function Start-Px {
844 param([string] $Config)
845}
846""",
847 "function Start-Px",
848 ),
849 # comment above a filter, PowerShell's pipeline function
850 (b"# @req-id: need_002\nfilter Get-Odd { $_ }\n", "filter Get-Odd"),
851 # comment after a function body's last statement falls back to the function
852 (
853 b"""function Start-Px {
854 Write-Host 'start'
855 # @req-id: need_003
856}
857""",
858 "function Start-Px",
859 ),
860 # comment above a class
861 (b"# @req-id: need_004\nclass Proxy { }\n", "class Proxy"),
862 # comment above a method inside a class
863 (
864 b"""class Proxy {
865 # @req-id: need_005
866 [string] Start([string] $c) { return $c }
867}
868""",
869 "[string] Start",
870 ),
871 # comment above a Pester It inside a Describe
872 (
873 b"""Describe 'px' {
874 # @req-id: need_006
875 It 'starts' {
876 1 | Should -Be 1
877 }
878}
879""",
880 "It 'starts'",
881 ),
882 # PowerShell is case-insensitive, and so are Pester's block names
883 (
884 b"""describe 'px' {
885 # @req-id: need_007
886 it 'lower' { }
887}
888""",
889 "it 'lower'",
890 ),
891 # comment above a Describe
892 (b"# @req-id: need_008\nDescribe 'px' { }\n", "Describe 'px'"),
893 # comment inside an It body falls back to the It
894 (
895 b"""Describe 'px' {
896 It 'starts' {
897 1 | Should -Be 1
898 # @req-id: need_009
899 }
900}
901""",
902 "It 'starts'",
903 ),
904 # a plain command after the comment is code: it belongs to the Describe it is
905 # in, not to the It past the command, as in every other language
906 (
907 b"""Describe 'px' {
908 # @req-id: need_012
909 BeforeAll { $x = 1 }
910 It 'a' { }
911}
912""",
913 "Describe 'px'",
914 ),
915 # a Context is a scope like an It
916 (
917 b"""Describe 'px' {
918 # @req-id: need_013
919 Context 'ctx' { }
920}
921""",
922 "Context 'ctx'",
923 ),
924 # a block called through Pester's module qualifier is a Pester block too
925 (
926 b"""Describe 'px' {
927 # @req-id: need_014
928 Pester\\It 'qualified' { }
929}
930""",
931 "Pester\\It 'qualified'",
932 ),
933 # comment above a property inside a class
934 (
935 b"""class Proxy {
936 # @req-id: need_015
937 [string] $Name
938 [void] Start() { }
939}
940""",
941 "[string] $Name",
942 ),
943 # comment above an enum
944 (b"# @req-id: need_016\nenum Mode { Direct }\n", "enum Mode"),
945 ],
946)
947def test_find_associated_scope_powershell(code, result, init_powershell_tree_sitter):
948 parser, query = init_powershell_tree_sitter
949 comments = utils.extract_comments(code, parser, query)
950 assert comments
951 node: TreeSitterNode | None = utils.find_associated_scope(
952 comments[0], CommentType.powershell
953 )
954 assert node
955 assert node.text
956 assert node.text.decode("utf-8").startswith(result)
957
958
959@pytest.mark.parametrize(
960 "command",
961 [
962 b"Write-Host 'x'",
963 # an It of another module is not Pester's
964 b"Other\\It 'x' { }",
965 # a plain command is code: the function past it is not the marker's
966 b"Set-StrictMode -Version Latest\nfunction Start-Px { }",
967 ],
968)
969def test_a_non_pester_command_is_no_scope(command, init_powershell_tree_sitter):
970 """Only Pester's blocks are scopes among commands: a marker above any other
971 command, at file level, binds to nothing, and not to an entity after it."""
972 parser, query = init_powershell_tree_sitter
973 comments = utils.extract_comments(
974 b"# @req-id: need_010\n" + command + b"\n", parser, query
975 )
976 assert comments
977 assert utils.find_associated_scope(comments[0], CommentType.powershell) is None
978
979
980def _powershell_attached(
981 source: str, parser: Parser, query: Query
982) -> tuple[str, str] | None:
983 """The first line of the entity the comment holding ``marker`` in the PowerShell
984 ``source`` is attached to, and the mechanism; ``None`` for nothing."""
985 comments = utils.extract_comments(source.encode(), parser, query) or []
986 marker = next(c for c in comments if b"marker" in (c.text or b""))
987 association = utils.associate(marker, CommentType.powershell)
988 if association is None or isinstance(association, utils.Unattached):
989 return None
990 first_line = (association.node.text or b"").decode().splitlines()[0]
991 return first_line.strip(), association.mechanism
992
993
994@pytest.mark.parametrize(
995 ("source", "expected"),
996 [
997 pytest.param(
998 "function Outer {\n <#\n .SYNOPSIS\n marker\n #>\n"
999 " function Inner { }\n}\n",
1000 ("function Outer {", "doc_comment"),
1001 id="help-block-before-a-nested-function",
1002 ),
1003 pytest.param(
1004 "function Outer {\n <# marker\n .synopsis #>\n [CmdletBinding()]\n"
1005 " param()\n Get-X\n}\n",
1006 ("function Outer {", "doc_comment"),
1007 id="help-block-before-param-lower-case-keyword",
1008 ),
1009 pytest.param(
1010 "filter Outer {\n <# .DESCRIPTION\n marker #>\n Get-X\n}\n",
1011 ("filter Outer {", "doc_comment"),
1012 id="help-block-in-a-filter",
1013 ),
1014 pytest.param(
1015 "function Outer { <# marker\n .SYNOPSIS #> Get-X }\n",
1016 ("function Outer { <# marker", "doc_comment"),
1017 id="help-block-on-the-function-row",
1018 ),
1019 pytest.param(
1020 "function Outer {\n # .SYNOPSIS\n # Does it.\n # marker\n"
1021 " function Inner { }\n}\n",
1022 ("function Outer {", "doc_comment"),
1023 id="help-hash-run-keyword-above-the-marker",
1024 ),
1025 pytest.param(
1026 "function Outer {\n # marker\n # .EXAMPLE\n function Inner { }\n}\n",
1027 ("function Outer {", "doc_comment"),
1028 id="help-hash-run-keyword-below-the-marker",
1029 ),
1030 # a plain comment first in a body keeps the rule of every language
1031 pytest.param(
1032 "function Outer {\n # marker\n function Inner { }\n}\n",
1033 ("function Inner { }", "leading"),
1034 id="plain-comment-before-a-nested-function",
1035 ),
1036 pytest.param(
1037 "function Outer {\n <# marker #>\n function Inner { }\n}\n",
1038 ("function Inner { }", "leading"),
1039 id="plain-block-before-a-nested-function",
1040 ),
1041 # a keyword that does not open a line is no help keyword
1042 pytest.param(
1043 "function Outer {\n <# see .SYNOPSIS marker #>\n function Inner { }\n}\n",
1044 ("function Inner { }", "leading"),
1045 id="keyword-inside-a-line",
1046 ),
1047 # a blank line ends a run of # comments: the help is another run
1048 pytest.param(
1049 "function Outer {\n # .SYNOPSIS\n\n # marker\n function Inner { }\n}\n",
1050 ("function Inner { }", "leading"),
1051 id="hash-run-broken-by-a-blank-line",
1052 ),
1053 # help after a statement is not first in the body
1054 pytest.param(
1055 "function Outer {\n $x = 1\n <# .SYNOPSIS\n marker #>\n"
1056 " function Inner { }\n}\n",
1057 ("function Inner { }", "leading"),
1058 id="help-after-a-statement",
1059 ),
1060 # only a function's body: a script block's help is a plain comment
1061 pytest.param(
1062 "Describe 'd' {\n <# .SYNOPSIS\n marker #>\n It 'a' { }\n}\n",
1063 ("It 'a' { }", "leading"),
1064 id="help-in-a-pester-block",
1065 ),
1066 # help before the function binds it as any comment does
1067 pytest.param(
1068 "<#\n.SYNOPSIS\nmarker\n#>\nfunction Outer { }\n",
1069 ("function Outer { }", "leading"),
1070 id="help-before-the-function",
1071 ),
1072 # help at the end of a body is inside it
1073 pytest.param(
1074 "function Outer {\n Get-X\n <# .SYNOPSIS\n marker #>\n}\n",
1075 ("function Outer {", "enclosing"),
1076 id="help-at-the-end-of-the-body",
1077 ),
1078 # a keyword line holds the keyword alone, or with its one argument
1079 pytest.param(
1080 "function Outer {\n # .NOTES: a detail, not help\n # marker\n"
1081 " function Inner { }\n}\n",
1082 ("function Inner { }", "leading"),
1083 id="keyword-followed-by-a-colon",
1084 ),
1085 pytest.param(
1086 "function Outer {\n # .Description = $x.Description\n # marker\n"
1087 " function Inner { }\n}\n",
1088 ("function Inner { }", "leading"),
1089 id="commented-out-assignment",
1090 ),
1091 pytest.param(
1092 "function Outer {\n <# .SYNOPSIS Starts it.\n marker #>\n"
1093 " function Inner { }\n}\n",
1094 ("function Inner { }", "leading"),
1095 id="argument-to-a-keyword-that-takes-none",
1096 ),
1097 pytest.param(
1098 "function Outer {\n <# .PARAMETER Path Extra\n marker #>\n"
1099 " function Inner { }\n}\n",
1100 ("function Inner { }", "leading"),
1101 id="two-arguments",
1102 ),
1103 pytest.param(
1104 "function Outer {\n <#\n .Parameter Path \n marker #>\n"
1105 " function Inner { }\n}\n",
1106 ("function Outer {", "doc_comment"),
1107 id="parameter-with-its-argument",
1108 ),
1109 ],
1110)
1111def test_powershell_help_first_in_a_body_documents_the_function(
1112 source, expected, init_powershell_tree_sitter
1113):
1114 """Comment-based help (a ``<# #>`` block, or a run of ``#`` lines, holding a help
1115 keyword line such as ``.SYNOPSIS``) first in a function's body documents that
1116 function: a marker in it is the function's
1117 documentation comment, even when a nested function follows. A plain comment there
1118 keeps the rule of every language, and binds the next statement's entity."""
1119 parser, query = init_powershell_tree_sitter
1120 assert _powershell_attached(source, parser, query) == expected
1121
1122
1123@pytest.mark.parametrize(
1124 ("source", "expected"),
1125 [
1126 pytest.param(
1127 "# marker\n[DscResource()]\nclass K { }\n",
1128 None,
1129 id="above-attribute-at-file-start",
1130 ),
1131 pytest.param(
1132 "function a { }\n# marker\n[DscResource()]\nclass K { }\n",
1133 None,
1134 id="above-attribute-after-code",
1135 ),
1136 pytest.param(
1137 'function a { }\n# marker\n[SuppressMessage("A", "")]\nclass K { }\n',
1138 None,
1139 id="above-attribute-with-arguments",
1140 ),
1141 pytest.param(
1142 "function a { }\n[Flags()]\nenum E { # marker\n A = 1\n}\n",
1143 ("enum E { # marker", "trailing"),
1144 id="trailing-the-enum-line",
1145 ),
1146 pytest.param(
1147 "function a { }\n[DscResource()]\nclass K { # marker\n}\n",
1148 ("class K { # marker", "trailing"),
1149 id="trailing-the-class-line",
1150 ),
1151 ],
1152)
1153def test_a_marker_above_a_powershell_attribute_binds_nothing(
1154 source, expected, init_powershell_tree_sitter
1155):
1156 """The grammar cannot read an attribute before a class or an enum: what it makes of
1157 one depends on the code around it (an ``ERROR`` node, a type literal statement
1158 holding one, a cast that takes the class in), so a marker above the attribute binds
1159 nothing, as the documentation says. A marker trailing the class's or the enum's
1160 line, with the attribute on a line of its own, binds it. A grammar that learns
1161 attributes fails this test: then the documented limitation goes."""
1162 parser, query = init_powershell_tree_sitter
1163 assert _powershell_attached(source, parser, query) == expected
1164
1165
1166@pytest.mark.parametrize(
1167 ("source", "expected"),
1168 [
1169 pytest.param(
1170 "function f { } # marker\n",
1171 ("function f { }", "trailing"),
1172 id="function",
1173 ),
1174 pytest.param(
1175 "Describe 'd' {\n It 'x' { } # marker\n}\n",
1176 ("It 'x' { }", "trailing"),
1177 id="it-in-describe",
1178 ),
1179 pytest.param(
1180 "Describe 'top' { } # marker\nfunction f { }\n",
1181 ("Describe 'top' { }", "trailing"),
1182 id="describe-at-file-level",
1183 ),
1184 pytest.param(
1185 "Describe 'd' {\n Context 'c' {\n } # marker\n}\n",
1186 ("Context 'c' {", "trailing"),
1187 id="context-closing-on-the-row",
1188 ),
1189 pytest.param(
1190 "Set-StrictMode -Version 3 # marker\nfunction f { }\n",
1191 None,
1192 id="plain-command",
1193 ),
1194 pytest.param(
1195 "Describe 'd' {\n Write-Host 'x' # marker\n}\n",
1196 ("Describe 'd' {", "enclosing"),
1197 id="plain-command-in-describe",
1198 ),
1199 ],
1200)
1201def test_a_trailing_powershell_marker_binds_the_entity_on_its_row(
1202 source, expected, init_powershell_tree_sitter
1203):
1204 """A trailing comment belongs to the entity that ends on its row, a Pester block
1205 included, which the grammar wraps in a pipeline; on a plain command's row it falls
1206 back to the enclosing entity, as in every other language."""
1207 parser, query = init_powershell_tree_sitter
1208 assert _powershell_attached(source, parser, query) == expected
1209
1210
1211@pytest.mark.parametrize(
1212 ("code", "result"),
1213 [
1214 (
1215 b"""
1216 // @req-id: need_001
1217 function dummyFunc1() {
1218 }
1219 """,
1220 "function dummyFunc1()",
1221 ),
1222 (
1223 b"""
1224 class DummyClass {
1225 // @req-id: need_001
1226 method1() {
1227 }
1228 }
1229 """,
1230 "method1()",
1231 ),
1232 # leading comment on an exported function must descend into
1233 # export_statement, not resolve to no scope
1234 (
1235 b"""
1236 // @req-id: need_001
1237 export function dummyFunc1() {
1238 }
1239 """,
1240 "function dummyFunc1()",
1241 ),
1242 # leading comment on an exported class
1243 (
1244 b"""
1245 // @req-id: need_001
1246 export class DummyClass {
1247 }
1248 """,
1249 "class DummyClass",
1250 ),
1251 # leading comment on an exported const arrow function
1252 (
1253 b"""
1254 // @req-id: need_001
1255 export const dummyFunc1 = () => {
1256 };
1257 """,
1258 "dummyFunc1",
1259 ),
1260 # arrow-function const still resolves to itself
1261 (
1262 b"""
1263 // @req-id: need_001
1264 const dummyFunc1 = () => {
1265 };
1266 """,
1267 "dummyFunc1",
1268 ),
1269 # JSX-returning component parses cleanly and resolves scope (.tsx content)
1270 (
1271 b"""
1272 // @req-id: need_001
1273 export function Button() {
1274 return <button>Click me</button>;
1275 }
1276 """,
1277 "function Button()",
1278 ),
1279 ],
1280)
1281def test_find_associated_scope_typescript(code, result, init_typescript_tree_sitter):
1282 parser, query = init_typescript_tree_sitter
1283 comments = utils.extract_comments(code, parser, query)
1284 node: TreeSitterNode | None = utils.find_associated_scope(
1285 comments[0], CommentType.ts
1286 )
1287 assert node
1288 assert node.text
1289 ts_def = node.text.decode("utf-8")
1290 assert result in ts_def
1291
1292
1293@pytest.mark.parametrize(
1294 ("code", "result"),
1295 [
1296 (
1297 b"""
1298 def dummy_func1():
1299 # @req-id: need_001
1300 pass
1301 """,
1302 "def dummy_func1()",
1303 ),
1304 (
1305 b"""
1306 def dummy_func1():
1307 '''@req-id: need_001'''
1308 pass
1309 """,
1310 "def dummy_func1()",
1311 ),
1312 (
1313 b"""
1314 def dummy_func1():
1315 def nested_dummy_func1():
1316 '''@req-id: need_001'''
1317 pass
1318 pass
1319 """,
1320 "def nested_dummy_func1()",
1321 ),
1322 (
1323 b"""
1324 def dummy_func1():
1325 '''@req-id: need_001'''
1326 def nested_dummy_func1():
1327 pass
1328 pass
1329 """,
1330 "def dummy_func1()",
1331 ),
1332 ],
1333)
1334def test_find_enclosing_scope_python(code, result, init_python_tree_sitter):
1335 parser, query = init_python_tree_sitter
1336 comments = utils.extract_comments(code, parser, query)
1337 node: TreeSitterNode | None = utils.find_enclosing_scope(
1338 comments[0], CommentType.python
1339 )
1340 assert node
1341 assert node.text
1342 func_def = node.text.decode("utf-8")
1343 assert result in func_def
1344
1345
1346@pytest.mark.parametrize(
1347 ("code", "result"),
1348 [
1349 (
1350 b"""
1351 # @req-id: need_001
1352 def dummy_func1():
1353 pass
1354 """,
1355 "def dummy_func1()",
1356 ),
1357 (
1358 b"""
1359 # @req-id: need_001
1360 # @req-id: need_002
1361 def dummy_func1():
1362 pass
1363 """,
1364 "def dummy_func1()",
1365 ),
1366 ],
1367)
1368def test_find_next_scope_python(code, result, init_python_tree_sitter):
1369 parser, query = init_python_tree_sitter
1370 comments = utils.extract_comments(code, parser, query)
1371 node: TreeSitterNode | None = utils.find_next_scope(comments[0], CommentType.python)
1372 assert node
1373 assert node.text
1374 func_def = node.text.decode("utf-8")
1375 assert result in func_def
1376
1377
1378@pytest.mark.parametrize(
1379 ("code", "result"),
1380 [
1381 (
1382 b"""
1383 // @req-id: need_001
1384 void dummy_func1(){
1385 }
1386 """,
1387 "void dummy_func1()",
1388 ),
1389 (
1390 b"""
1391 /* @req-id: need_001 */
1392 void dummy_func1(){
1393 }
1394 """,
1395 "void dummy_func1()",
1396 ),
1397 ],
1398)
1399def test_find_next_scope_cpp(code, result, init_cpp_tree_sitter):
1400 parser, query = init_cpp_tree_sitter
1401 comments = utils.extract_comments(code, parser, query)
1402 node: TreeSitterNode | None = utils.find_next_scope(comments[0], CommentType.cpp)
1403 assert node
1404 assert node.text
1405 func_def = node.text.decode("utf-8")
1406 assert result in func_def
1407
1408
1409@pytest.mark.parametrize(
1410 ("code", "result"),
1411 [
1412 (
1413 b"""
1414 // @req-id: need_001
1415 public class DummyClass1
1416 {
1417 }
1418 """,
1419 "public class DummyClass1",
1420 ),
1421 (
1422 b"""
1423
1424 public class DummyClass1
1425 {
1426 /* @req-id: need_001 */
1427 /* @req-id: need_002 */
1428 public void DummyFunc1()
1429 {
1430 }
1431 }
1432 """,
1433 "public void DummyFunc1",
1434 ),
1435 ],
1436)
1437def test_find_next_scope_csharp(code, result, init_csharp_tree_sitter):
1438 parser, query = init_csharp_tree_sitter
1439 comments = utils.extract_comments(code, parser, query)
1440 node: TreeSitterNode | None = utils.find_next_scope(comments[0], CommentType.cs)
1441 assert node
1442 assert node.text
1443 func_def = node.text.decode("utf-8")
1444 assert result in func_def
1445
1446
1447@pytest.mark.parametrize(
1448 ("code", "result"),
1449 [
1450 (
1451 b"""
1452 // @req-id: need_001
1453 function dummyFunc1() {
1454 }
1455 """,
1456 "function dummyFunc1()",
1457 ),
1458 (
1459 b"""
1460 // @req-id: need_001
1461 export function dummyFunc1() {
1462 }
1463 """,
1464 "function dummyFunc1()",
1465 ),
1466 ],
1467)
1468def test_find_next_scope_typescript(code, result, init_typescript_tree_sitter):
1469 parser, query = init_typescript_tree_sitter
1470 comments = utils.extract_comments(code, parser, query)
1471 node: TreeSitterNode | None = utils.find_next_scope(comments[0], CommentType.ts)
1472 assert node
1473 assert node.text
1474 func_def = node.text.decode("utf-8")
1475 assert result in func_def
1476
1477
1478def test_a_plain_const_breaks_adjacency_typescript(init_typescript_tree_sitter):
1479 """A plain (non-function) const between the comment and a function is code, not
1480 trivia: the comment is attached to neither, here to the file."""
1481 code = b"""
1482 // @req-id: need_001
1483 const helperFlag = true;
1484 function dummyFunc1() {
1485 }
1486 """
1487 parser, query = init_typescript_tree_sitter
1488 comments = utils.extract_comments(code, parser, query)
1489 assert comments
1490 assert utils.find_next_scope(comments[0], CommentType.ts) is None
1491 assert utils.find_associated_scope(comments[0], CommentType.ts) is None
1492
1493
1494def test_find_associated_scope_typescript_jsx_no_parse_error(
1495 init_typescript_tree_sitter,
1496):
1497 """A JSX-returning component must parse cleanly under the TSX grammar."""
1498 code = b"""
1499 // @req-id: need_001
1500 export function Button() {
1501 return <button>Click me</button>;
1502 }
1503 """
1504 parser, _ = init_typescript_tree_sitter
1505 tree = parser.parse(code)
1506 assert not tree.root_node.has_error
1507
1508
1509@pytest.mark.parametrize(
1510 ("name", "grammar"),
1511 [
1512 ("a.ts", "typescript"),
1513 ("a.mts", "typescript"),
1514 ("a.cts", "typescript"),
1515 ("A.TS", "typescript"),
1516 ("a.Mts", "typescript"),
1517 # a declaration file's suffix is ``.ts``: TypeScript, as any ``.ts`` file
1518 ("a.d.ts", "typescript"),
1519 ("a.tsx", "tsx"),
1520 ("a.js", "tsx"),
1521 ("a.jsx", "tsx"),
1522 ("a.mjs", "tsx"),
1523 ("a.cjs", "tsx"),
1524 ],
1525)
1526def test_ts_grammar_key(name: str, grammar: str) -> None:
1527 """TypeScript's own suffixes, in any case, get the TypeScript grammar, where a
1528 ``<T>x`` cast is legal; every other suffix of the family gets TSX."""
1529 assert utils.ts_grammar_key(Path(name)) == grammar
1530
1531
1532def test_typescript_ts_suffix_recovers_markers_around_angle_bracket_cast():
1533 """A ``.ts`` file must use the plain TypeScript grammar, not TSX.
1534
1535 ``<string>x`` is a legacy angle-bracket type assertion: valid TypeScript
1536 syntax, but JSX syntax under the TSX grammar. There it parses as a
1537 ``jsx_opening_element`` and swallows the rest of the file into a single
1538 ``jsx_text`` node, silently dropping every marker after it (``has_error``
1539 is also set). ``init_tree_sitter`` must pick the plain TypeScript grammar
1540 for a ``.ts`` path, via ``ts_grammar_key``, so markers both above and
1541 below the cast all survive.
1542 """
1543 code = b"""// @Top, IMPL_TOP
1544const v = <string>x;
1545// @Bottom, IMPL_BOTTOM
1546const y = 2;
1547// @Third, IMPL_THIRD
1548"""
1549 parser, query = utils.init_tree_sitter(CommentType.ts, Path("dummy.ts"))
1550 tree = parser.parse(code)
1551 assert not tree.root_node.has_error
1552
1553 comments = utils.extract_comments(code, parser, query)
1554 assert comments is not None
1555 comments.sort(key=lambda node: node.start_point.row)
1556 texts = [node.text.decode("utf-8") for node in comments if node.text]
1557 assert texts == [
1558 "// @Top, IMPL_TOP",
1559 "// @Bottom, IMPL_BOTTOM",
1560 "// @Third, IMPL_THIRD",
1561 ]
1562
1563
1564@pytest.mark.parametrize(
1565 ("code", "result"),
1566 [
1567 (
1568 b"""
1569 void dummy_func1(){
1570 // @req-id: need_001
1571 }
1572 """,
1573 "void dummy_func1()",
1574 ),
1575 (
1576 b"""
1577 void dummy_func1(){
1578 /* @req-id: need_001 */
1579 }
1580 """,
1581 "void dummy_func1()",
1582 ),
1583 ],
1584)
1585def test_find_enclosing_scope_cpp(code, result, init_cpp_tree_sitter):
1586 parser, query = init_cpp_tree_sitter
1587 comments = utils.extract_comments(code, parser, query)
1588 node: TreeSitterNode | None = utils.find_enclosing_scope(
1589 comments[0], CommentType.cpp
1590 )
1591 assert node
1592 assert node.text
1593 func_def = node.text.decode("utf-8")
1594 assert result in func_def
1595
1596
1597@pytest.mark.parametrize(
1598 ("code", "result"),
1599 [
1600 (
1601 b"""
1602 public class DummyClass1
1603 {
1604 // @req-id: need_001
1605 }
1606 """,
1607 "public class DummyClass1",
1608 ),
1609 (
1610 b"""
1611 public class DummyClass1
1612 {
1613 public void DummyFunc1()
1614 {
1615 /* @req-id: need_001 */
1616 }
1617 }
1618 """,
1619 "public void DummyFunc1()",
1620 ),
1621 (
1622 b"""
1623 public class DummyClass1
1624 {
1625 public string DummyProperty1
1626 {
1627 get
1628 {
1629 /* @req-id: need_001 */
1630 return "dummy";
1631 }
1632 }
1633 }
1634 """,
1635 "public string DummyProperty1",
1636 ),
1637 ],
1638)
1639def test_find_enclosing_scope_csharp(code, result, init_csharp_tree_sitter):
1640 parser, query = init_csharp_tree_sitter
1641 comments = utils.extract_comments(code, parser, query)
1642 node: TreeSitterNode | None = utils.find_enclosing_scope(
1643 comments[0], CommentType.cs
1644 )
1645 assert node
1646 assert node.text
1647 func_def = node.text.decode("utf-8")
1648 assert result in func_def
1649
1650
1651@pytest.mark.parametrize(
1652 ("code", "num_comments", "result"),
1653 [
1654 (
1655 b"""
1656 // @req-id: need_001
1657 void dummy_func1(){
1658 }
1659 """,
1660 1,
1661 "// @req-id: need_001",
1662 ),
1663 (
1664 b"""
1665 void dummy_func1(){
1666 // @req-id: need_001
1667 }
1668 """,
1669 1,
1670 "// @req-id: need_001",
1671 ),
1672 (
1673 b"""
1674 /* @req-id: need_001 */
1675 void dummy_func1(){
1676 }
1677 """,
1678 1,
1679 "/* @req-id: need_001 */",
1680 ),
1681 (
1682 b"""
1683 // @req-id: need_001
1684 //
1685 //
1686 void dummy_func1(){
1687 }
1688 """,
1689 3,
1690 "// @req-id: need_001",
1691 ),
1692 ],
1693)
1694def test_cpp_comment(code, num_comments, result, init_cpp_tree_sitter):
1695 parser, query = init_cpp_tree_sitter
1696 comments = utils.extract_comments(code, parser, query)
1697 assert len(comments) == num_comments
1698 comments.sort(key=lambda x: x.start_point.row)
1699 assert comments[0].text
1700 assert comments[0].text.decode("utf-8") == result
1701
1702
1703@pytest.mark.parametrize(
1704 ("code", "num_comments", "result"),
1705 [
1706 (
1707 b"""
1708 # @req-id: need_001
1709 def dummy_func1():
1710 pass
1711 """,
1712 1,
1713 "# @req-id: need_001",
1714 ),
1715 (
1716 b"""
1717 def dummy_func1():
1718 # @req-id: need_001
1719 pass
1720 """,
1721 1,
1722 "# @req-id: need_001",
1723 ),
1724 (
1725 b"""
1726 # single line comment
1727 # @req-id: need_001
1728 def dummy_func1():
1729 pass
1730 """,
1731 2,
1732 "# single line comment",
1733 ),
1734 (
1735 b"""
1736 def dummy_func1():
1737 '''
1738 @req-id: need_001
1739 '''
1740 pass
1741 """,
1742 1,
1743 "'''\n @req-id: need_001\n '''",
1744 ),
1745 (
1746 b"""
1747 def dummy_func1():
1748 text = '''@req-id: need_001, need_002, this docstring shall not be extracted as comment'''
1749 # @req-id: need_001
1750 pass
1751 """,
1752 1,
1753 "# @req-id: need_001",
1754 ),
1755 ],
1756)
1757def test_python_comment(code, num_comments, result, init_python_tree_sitter):
1758 parser, query = init_python_tree_sitter
1759 comments: list[TreeSitterNode] = utils.extract_comments(code, parser, query)
1760 comments.sort(key=lambda x: x.start_point.row)
1761 assert len(comments) == num_comments
1762 assert comments[0].text
1763 assert comments[0].text.decode("utf-8") == result
1764
1765
1766@pytest.mark.parametrize(
1767 ("code", "num_comments", "result"),
1768 [
1769 (
1770 b"""
1771 // @req-id: need_001
1772 void DummyFunc1(){
1773 }
1774 """,
1775 1,
1776 "// @req-id: need_001",
1777 ),
1778 (
1779 b"""
1780 void DummyFunc1(){
1781 // @req-id: need_001
1782 }
1783 """,
1784 1,
1785 "// @req-id: need_001",
1786 ),
1787 (
1788 b"""
1789 /* @req-id: need_001 */
1790 void DummyFunc1(){
1791 }
1792 """,
1793 1,
1794 "/* @req-id: need_001 */",
1795 ),
1796 (
1797 b"""
1798 // @req-id: need_001
1799 //
1800 //
1801 void DummyFunc1(){
1802 }
1803 """,
1804 3,
1805 "// @req-id: need_001",
1806 ),
1807 ],
1808)
1809def test_csharp_comment(code, num_comments, result, init_csharp_tree_sitter):
1810 parser, query = init_csharp_tree_sitter
1811 comments: list[TreeSitterNode] = utils.extract_comments(code, parser, query)
1812 comments.sort(key=lambda x: x.start_point.row)
1813 assert len(comments) == num_comments
1814 assert comments[0].text
1815 assert comments[0].text.decode("utf-8") == result
1816
1817
1818@pytest.mark.parametrize(
1819 ("code", "num_comments", "result"),
1820 [
1821 (
1822 b"""
1823 // @req-id: need_001
1824 function dummyFunc1() {
1825 }
1826 """,
1827 1,
1828 "// @req-id: need_001",
1829 ),
1830 (
1831 b"""
1832 /* @req-id: need_001 */
1833 const value = 1;
1834 """,
1835 1,
1836 "/* @req-id: need_001 */",
1837 ),
1838 ],
1839)
1840def test_typescript_comment(code, num_comments, result, init_typescript_tree_sitter):
1841 parser, query = init_typescript_tree_sitter
1842 comments: list[TreeSitterNode] = utils.extract_comments(code, parser, query)
1843 comments.sort(key=lambda x: x.start_point.row)
1844 assert len(comments) == num_comments
1845 assert comments[0].text
1846 assert comments[0].text.decode("utf-8") == result
1847
1848
1849@pytest.mark.parametrize(
1850 ("code", "num_comments", "result"),
1851 [
1852 (
1853 b"""
1854 # @req-id: need_001
1855 database:
1856 host: localhost
1857 """,
1858 1,
1859 "# @req-id: need_001",
1860 ),
1861 (
1862 b"""
1863 services:
1864 web:
1865 # @req-id: need_001
1866 image: nginx:latest
1867 """,
1868 1,
1869 "# @req-id: need_001",
1870 ),
1871 (
1872 b"""
1873 # Top level comment
1874 # @req-id: need_001
1875 version: "3.8"
1876 """,
1877 2,
1878 "# Top level comment",
1879 ),
1880 ],
1881)
1882def test_yaml_comment(code, num_comments, result, init_yaml_tree_sitter):
1883 parser, query = init_yaml_tree_sitter
1884 comments: list[TreeSitterNode] = utils.extract_comments(code, parser, query)
1885 comments.sort(key=lambda x: x.start_point.row)
1886 assert len(comments) == num_comments
1887 assert comments[0].text
1888 assert comments[0].text.decode("utf-8") == result
1889
1890
1891@pytest.mark.parametrize(
1892 ("code", "num_comments", "result"),
1893 [
1894 (
1895 b"""
1896 // @req-id: need_001
1897 func dummyFunc1() {
1898 }
1899 """,
1900 1,
1901 "// @req-id: need_001",
1902 ),
1903 (
1904 b"""
1905 func dummyFunc1() {
1906 // @req-id: need_001
1907 }
1908 """,
1909 1,
1910 "// @req-id: need_001",
1911 ),
1912 (
1913 b"""
1914 /* @req-id: need_001 */
1915 func dummyFunc1() {
1916 }
1917 """,
1918 1,
1919 "/* @req-id: need_001 */",
1920 ),
1921 (
1922 b"""
1923 // @req-id: need_001
1924 //
1925 //
1926 func dummyFunc1() {
1927 }
1928 """,
1929 3,
1930 "// @req-id: need_001",
1931 ),
1932 ],
1933)
1934def test_go_comment(code, num_comments, result, init_go_tree_sitter):
1935 parser, query = init_go_tree_sitter
1936 comments: list[TreeSitterNode] = utils.extract_comments(code, parser, query)
1937 comments.sort(key=lambda x: x.start_point.row)
1938 assert len(comments) == num_comments
1939 assert comments[0].text
1940 assert comments[0].text.decode("utf-8") == result
1941
1942
1943@pytest.mark.parametrize(
1944 ("code", "result"),
1945 [
1946 (
1947 b"""
1948 // @req-id: need_001
1949 func dummyFunc1() {
1950 }
1951 """,
1952 "func dummyFunc1()",
1953 ),
1954 (
1955 b"""
1956 func dummyFunc2() {
1957 }
1958 // @req-id: need_001
1959 func dummyFunc1() {
1960 }
1961 """,
1962 "func dummyFunc1()",
1963 ),
1964 (
1965 b"""
1966 /* @req-id: need_001 */
1967 func dummyFunc1() {
1968 }
1969 """,
1970 "func dummyFunc1()",
1971 ),
1972 ],
1973)
1974def test_find_associated_scope_go(code, result, init_go_tree_sitter):
1975 parser, query = init_go_tree_sitter
1976 comments = utils.extract_comments(code, parser, query)
1977 node: TreeSitterNode | None = utils.find_associated_scope(
1978 comments[0], CommentType.go
1979 )
1980 assert node
1981 assert node.text
1982 go_def = node.text.decode("utf-8")
1983 assert result in go_def
1984
1985
1986@pytest.mark.parametrize(
1987 ("code", "result"),
1988 [
1989 (
1990 b"""
1991 // @req-id: need_001
1992 func dummyFunc1() {
1993 }
1994 """,
1995 "func dummyFunc1()",
1996 ),
1997 (
1998 b"""
1999 // @req-id: need_001
2000 type DummyStruct struct {
2001 Field int
2002 }
2003 """,
2004 "type DummyStruct struct",
2005 ),
2006 ],
2007)
2008def test_find_next_scope_go(code, result, init_go_tree_sitter):
2009 parser, query = init_go_tree_sitter
2010 comments = utils.extract_comments(code, parser, query)
2011 node: TreeSitterNode | None = utils.find_next_scope(comments[0], CommentType.go)
2012 assert node
2013 assert node.text
2014 go_def = node.text.decode("utf-8")
2015 assert result in go_def
2016
2017
2018@pytest.mark.parametrize(
2019 ("code", "result"),
2020 [
2021 (
2022 b"""
2023 func dummyFunc1() {
2024 // @req-id: need_001
2025 }
2026 """,
2027 "func dummyFunc1()",
2028 ),
2029 (
2030 b"""
2031 func dummyFunc1() {
2032 /* @req-id: need_001 */
2033 }
2034 """,
2035 "func dummyFunc1()",
2036 ),
2037 ],
2038)
2039def test_find_enclosing_scope_go(code, result, init_go_tree_sitter):
2040 parser, query = init_go_tree_sitter
2041 comments = utils.extract_comments(code, parser, query)
2042 node: TreeSitterNode | None = utils.find_enclosing_scope(
2043 comments[0], CommentType.go
2044 )
2045 assert node
2046 assert node.text
2047 go_def = node.text.decode("utf-8")
2048 assert result in go_def
2049
2050
2051@pytest.mark.parametrize(
2052 ("git_url", "rev", "project_path", "filepath", "lineno", "result"),
2053 [
2054 (
2055 "git@github.com:useblocks/sphinx-codelinks.git",
2056 "beef1234",
2057 Path(__file__).parent.parent,
2058 # ABSOLUTE, and under `project_path`. `form_https_url` resolves the
2059 # filepath with `.absolute()`, i.e. against the CWD, so a relative one
2060 # here only agrees with `project_path` when pytest happens to run from
2061 # the package directory -- which it does not when the whole workspace's
2062 # suite is invoked from the repository root
2063 Path(__file__).parent.parent / "example" / "to" / "here",
2064 3,
2065 "https://github.com/useblocks/sphinx-codelinks/blob/beef1234/example/to/here#L3",
2066 )
2067 ],
2068)
2069def test_form_https_url(
2070 git_url, rev, project_path, filepath, lineno, result
2071): # need to have these args
2072 url = utils.form_https_url(git_url, rev, project_path, filepath, lineno=lineno)
2073 assert url == result
2074
2075
2076def get_git_path() -> str:
2077 """Get the path to the git executable."""
2078 git_path = shutil.which("git")
2079 if not git_path:
2080 raise FileNotFoundError("Git executable not found")
2081 if not Path(git_path).is_file():
2082 raise FileNotFoundError("Git executable path is invalid")
2083 return git_path
2084
2085
2086def init_git_repo(repo_path: Path, remote_url: str) -> Path:
2087 """Initialize a git repository for testing."""
2088 git_dir = repo_path / "test_repo"
2089 src_dir = git_dir / "src"
2090 src_dir.mkdir(parents=True)
2091
2092 git_path = get_git_path()
2093 if not git_path:
2094 raise FileNotFoundError("Git executable not found")
2095 if not Path(git_path).is_file():
2096 raise FileNotFoundError("Git executable path is invalid")
2097
2098 # Initialize git repo
2099 subprocess.run([git_path, "init"], cwd=git_dir, check=True, capture_output=True)
2100 subprocess.run(
2101 [git_path, "config", "user.email", "test@example.com"], cwd=git_dir, check=True
2102 )
2103 subprocess.run(
2104 [git_path, "config", "user.name", "Test User"], cwd=git_dir, check=True
2105 )
2106
2107 # Create a test file and commit
2108 test_file = src_dir / "test_file.py"
2109 test_file.write_text("# Test file\nprint('hello')\n")
2110 subprocess.run([git_path, "add", "."], cwd=git_dir, check=True)
2111 subprocess.run(
2112 [git_path, "commit", "-m", "Initial commit"], cwd=git_dir, check=True
2113 )
2114
2115 # Add a remote
2116 subprocess.run(
2117 [git_path, "remote", "add", "origin", remote_url],
2118 cwd=git_dir,
2119 check=True,
2120 )
2121
2122 return git_dir
2123
2124
2125@pytest.fixture(
2126 params=[
2127 ("test_repo_git", "git@github.com:test-user/test-repo.git"),
2128 ("test_repo_https", "https://github.com/test-user/test-repo.git"),
2129 ]
2130)
2131def git_repo(tmp_path: str, request: pytest.FixtureRequest) -> tuple[Path, str]:
2132 """Create git repos for testing."""
2133 repo_name, remote_url = request.param
2134 repo_path = Path(tmp_path) / repo_name
2135 repo_path = init_git_repo(repo_path, remote_url)
2136 return repo_path, remote_url
2137
2138
2139def get_current_commit_hash(git_dir: Path) -> str:
2140 """Get the current commit hash of the git repository."""
2141 git_path = get_git_path()
2142 result = subprocess.run(
2143 [git_path, "rev-parse", "HEAD"],
2144 cwd=git_dir,
2145 check=True,
2146 capture_output=True,
2147 text=True,
2148 )
2149
2150 return str(result.stdout.strip())
2151
2152
2153def test_locate_git_root(git_repo: tuple[Path, str]) -> None:
2154 repo_path = git_repo[0]
2155 src_dir = repo_path / "src"
2156 git_root = utils.locate_git_root(src_dir)
2157 assert git_root == repo_path
2158
2159
2160def test_get_remote_url(git_repo: tuple[Path, str]) -> None:
2161 repo_path, expected_url = git_repo
2162 remote_url = utils.get_remote_url(repo_path)
2163 assert remote_url == expected_url
2164
2165
2166def test_get_current_rev(git_repo: tuple[Path, str]) -> None:
2167 repo_path, _ = git_repo
2168 current_rev = get_current_commit_hash(repo_path)
2169 assert current_rev == utils.get_current_rev(repo_path)
2170
2171
2172@pytest.fixture(params=["detached", "branch"])
2173def git_worktree(
2174 tmp_path: Path, request: pytest.FixtureRequest
2175) -> tuple[Path, str, str]:
2176 """A LINKED WORKTREE of a scratch repository, in both shapes git makes.
2177
2178 A worktree is not a checkout with a `.git` directory: `.git` is a FILE holding
2179 `gitdir: <path>`, `config` lives in the main repository's git directory (named by
2180 the worktree's `commondir`), and only per-worktree state -- `HEAD` among it -- is in
2181 the worktree's own. `detached` puts a SHA in `HEAD`; `branch` puts `ref:
2182 refs/heads/...`, whose target is a shared ref in the common directory.
2183
2184 This repository is worked in worktrees as a matter of routine, so a helper that
2185 demands a `.git` directory fails for every developer while staying green in CI
2186 (useblocks/sphinx-codelinks#106).
2187 """
2188 remote_url = "https://github.com/test-user/test-repo.git"
2189 repo_path = init_git_repo(tmp_path / "main", remote_url)
2190 git_path = get_git_path()
2191 worktree_path = tmp_path / "worktree"
2192 add = [git_path, "worktree", "add"]
2193 add += (
2194 ["--detach", str(worktree_path), "HEAD"]
2195 if request.param == "detached"
2196 else [str(worktree_path), "-b", "a-worktree-branch"]
2197 )
2198 subprocess.run(add, cwd=repo_path, check=True, capture_output=True)
2199 assert (worktree_path / ".git").is_file(), "a linked worktree's .git is a file"
2200 return worktree_path, remote_url, get_current_commit_hash(repo_path)
2201
2202
2203def test_locate_git_root_worktree(git_worktree: tuple[Path, str, str]) -> None:
2204 worktree_path, _, _ = git_worktree
2205 assert utils.locate_git_root(worktree_path / "src") == worktree_path
2206
2207
2208def test_get_remote_url_worktree(git_worktree: tuple[Path, str, str]) -> None:
2209 worktree_path, remote_url, _ = git_worktree
2210 assert utils.get_remote_url(worktree_path) == remote_url
2211
2212
2213def test_get_current_rev_worktree(git_worktree: tuple[Path, str, str]) -> None:
2214 worktree_path, _, rev = git_worktree
2215 assert utils.get_current_rev(worktree_path) == rev
2216
2217
2218def test_get_current_rev_packed_refs(git_worktree: tuple[Path, str, str]) -> None:
2219 """`git gc` packs the loose refs away, and the branch ref is then in `packed-refs`.
2220
2221 Nothing unusual has to happen for this: `git gc` runs unattended on any long-lived
2222 checkout, and afterwards there is no `refs/heads/<branch>` FILE in either the
2223 worktree's git directory or the common one. It is the same failure shape as
2224 useblocks/sphinx-codelinks#106 reached by a different route -- and CI never sees it,
2225 because `actions/checkout` and Read the Docs both leave a detached HEAD.
2226 """
2227 worktree_path, _, rev = git_worktree
2228 git_path = get_git_path()
2229 subprocess.run(
2230 [git_path, "pack-refs", "--all"],
2231 cwd=worktree_path,
2232 check=True,
2233 capture_output=True,
2234 )
2235 git_dir = utils._git_dir(worktree_path)
2236 assert git_dir is not None
2237 common = utils._git_common_dir(git_dir)
2238 assert not list((common / "refs" / "heads").glob("*")), (
2239 "the loose refs should be gone"
2240 )
2241
2242 assert utils.get_current_rev(worktree_path) == rev
2243
2244
2245def test_get_current_rev_detached_head(tmp_path: Path) -> None:
2246 """In a detached HEAD (e.g. CI checkouts) .git/HEAD holds the commit SHA
2247 directly; get_current_rev returns it rather than warning and giving up."""
2248 git_root = tmp_path / "repo"
2249 (git_root / ".git").mkdir(parents=True)
2250 sha = "a1b2c3d4e5f60718293a4b5c6d7e8f9012345678"
2251 (git_root / ".git" / "HEAD").write_text(f"{sha}\n")
2252
2253 assert utils.get_current_rev(git_root) == sha
2254
2255
2256# ========== YAML-specific tests ==========
2257
2258
2259@pytest.mark.parametrize(
2260 ("code", "expected_structure"),
2261 [
2262 # Basic key-value pair
2263 (
2264 b"""
2265 # Comment before key
2266 database:
2267 host: localhost
2268 """,
2269 "database:",
2270 ),
2271 # Comment in nested structure
2272 (
2273 b"""
2274 services:
2275 web:
2276 # Comment before image
2277 image: nginx:latest
2278 """,
2279 "image: nginx:latest",
2280 ),
2281 # Comment before list item
2282 (
2283 b"""
2284 items:
2285 # Comment before list item
2286 - name: item1
2287 value: test
2288 """,
2289 "- name: item1",
2290 ),
2291 # Comment in document structure
2292 (
2293 b"""---
2294# Comment in document
2295version: "3.8"
2296services:
2297 app:
2298 build: .
2299 """,
2300 "version:",
2301 ),
2302 # Flow mapping structure
2303 (
2304 b"""
2305 # Comment before flow mapping
2306 config: {debug: true, port: 8080}
2307 """,
2308 "config:",
2309 ),
2310 ],
2311)
2312def test_find_yaml_next_structure(code, expected_structure, init_yaml_tree_sitter):
2313 """Test the find_yaml_next_structure function."""
2314 parser, query = init_yaml_tree_sitter
2315 comments = utils.extract_comments(code, parser, query)
2316 assert comments, "No comments found in the code"
2317
2318 next_structure = utils.find_yaml_next_structure(comments[0])
2319 assert next_structure, "No next structure found"
2320 structure_text = next_structure.text.decode("utf-8")
2321 assert expected_structure in structure_text
2322
2323
2324@pytest.mark.parametrize(
2325 ("code", "expected_structure"),
2326 [
2327 # Comment associated with key-value pair
2328 (
2329 b"""
2330 # Database configuration
2331 database:
2332 host: localhost
2333 port: 5432
2334 """,
2335 "database:",
2336 ),
2337 # Comment associated with nested structure
2338 (
2339 b"""
2340 services:
2341 web:
2342 # Web service image
2343 image: nginx:latest
2344 ports:
2345 - "80:80"
2346 """,
2347 "image: nginx:latest",
2348 ),
2349 # Comment associated with list item
2350 (
2351 b"""
2352 dependencies:
2353 # First dependency
2354 - name: redis
2355 version: "6.0"
2356 - name: postgres
2357 version: "13"
2358 """,
2359 "- name: redis",
2360 ),
2361 # Comment inside parent structure
2362 (
2363 b"""
2364 app:
2365 # Internal comment
2366 name: myapp
2367 version: "1.0"
2368 """,
2369 "name: myapp",
2370 ),
2371 ],
2372)
2373def test_find_yaml_associated_structure(
2374 code, expected_structure, init_yaml_tree_sitter
2375):
2376 """Test the find_yaml_associated_structure function."""
2377 parser, query = init_yaml_tree_sitter
2378 comments = utils.extract_comments(code, parser, query)
2379 assert comments, "No comments found in the code"
2380
2381 associated_structure = utils.find_yaml_associated_structure(comments[0])
2382 assert associated_structure, "No associated structure found"
2383 structure_text = associated_structure.text.decode("utf-8")
2384 assert expected_structure in structure_text
2385
2386
2387@pytest.mark.parametrize(
2388 ("code", "expected_results"),
2389 [
2390 # Multiple comments in sequence
2391 (
2392 b"""
2393 # First comment
2394 # Second comment
2395 database:
2396 host: localhost
2397 """,
2398 ["database:", "database:"], # Both comments should associate with database
2399 ),
2400 # Comments at different nesting levels
2401 (
2402 b"""
2403 # Top level comment
2404 services:
2405 web:
2406 # Nested comment
2407 image: nginx:latest
2408 """,
2409 ["services:", "image: nginx:latest"],
2410 ),
2411 ],
2412)
2413def test_multiple_yaml_comments(code, expected_results, init_yaml_tree_sitter):
2414 """Test handling of multiple YAML comments in the same file."""
2415 parser, query = init_yaml_tree_sitter
2416 comments = utils.extract_comments(code, parser, query)
2417 comments.sort(key=lambda x: x.start_point.row)
2418
2419 assert len(comments) == len(expected_results), (
2420 f"Expected {len(expected_results)} comments, found {len(comments)}"
2421 )
2422
2423 for i, comment in enumerate(comments):
2424 associated_structure = utils.find_yaml_associated_structure(comment)
2425 assert associated_structure, f"No associated structure found for comment {i}"
2426 structure_text = associated_structure.text.decode("utf-8")
2427 assert expected_results[i] in structure_text
2428
2429
2430@pytest.mark.parametrize(
2431 ("code", "has_structure"),
2432 [
2433 # Comment at end of file with no following structure
2434 (
2435 b"""
2436database:
2437 host: localhost
2438# End of file comment
2439 """,
2440 True, # This will actually find the parent database structure
2441 ),
2442 # Comment with only whitespace after
2443 (
2444 b"""
2445 # Lonely comment
2446
2447
2448 """,
2449 False,
2450 ),
2451 # Comment before valid structure
2452 (
2453 b"""
2454 # Valid comment
2455 key: value
2456 """,
2457 True,
2458 ),
2459 ],
2460)
2461def test_yaml_edge_cases(code, has_structure, init_yaml_tree_sitter):
2462 """Test edge cases in YAML comment processing."""
2463 parser, query = init_yaml_tree_sitter
2464 comments = utils.extract_comments(code, parser, query)
2465
2466 if comments:
2467 structure = utils.find_yaml_associated_structure(comments[0])
2468 if has_structure:
2469 assert structure, "Expected to find associated structure"
2470 else:
2471 assert structure is None, "Expected no associated structure"
2472 else:
2473 assert not has_structure, "No comments found but structure was expected"
2474
2475
2476@pytest.mark.parametrize(
2477 ("code", "expected_structures"),
2478 [
2479 # Simpler nested YAML structure
2480 (
2481 b"""# Global configuration
2482version: "3.8"
2483
2484# Services section
2485services:
2486 web:
2487 image: nginx:latest
2488 # Port configuration
2489 ports:
2490 - "80:80"
2491 """,
2492 [
2493 "version:", # Global configuration
2494 "services:", # Services section
2495 '- "80:80"', # Port configuration
2496 ],
2497 ),
2498 ],
2499)
2500def test_complex_yaml_structure(code, expected_structures, init_yaml_tree_sitter):
2501 """Test complex nested YAML structures with multiple comments."""
2502 parser, query = init_yaml_tree_sitter
2503 comments = utils.extract_comments(code, parser, query)
2504 comments.sort(key=lambda x: x.start_point.row)
2505
2506 assert len(comments) == len(expected_structures), (
2507 f"Expected {len(expected_structures)} comments, found {len(comments)}"
2508 )
2509
2510 for i, comment in enumerate(comments):
2511 associated_structure = utils.find_yaml_associated_structure(comment)
2512 assert associated_structure, f"No associated structure found for comment {i}"
2513 structure_text = associated_structure.text.decode("utf-8")
2514 assert expected_structures[i] in structure_text, (
2515 f"Expected '{expected_structures[i]}' in structure text: '{structure_text}'"
2516 )
2517
2518
2519@pytest.mark.parametrize(
2520 ("code", "expected_type"),
2521 [
2522 # Block mapping pair
2523 (
2524 b"""
2525 # Comment
2526 key: value
2527 """,
2528 "block_mapping_pair",
2529 ),
2530 # Block sequence item
2531 (
2532 b"""
2533 items:
2534 # Comment
2535 - item1
2536 """,
2537 "block_sequence_item",
2538 ),
2539 # Nested block mapping
2540 (
2541 b"""
2542 services:
2543 # Comment
2544 web:
2545 image: nginx
2546 """,
2547 "block_mapping_pair",
2548 ),
2549 ],
2550)
2551def test_yaml_structure_types(code, expected_type, init_yaml_tree_sitter):
2552 """Test that YAML structures return the correct node types."""
2553 parser, query = init_yaml_tree_sitter
2554 comments = utils.extract_comments(code, parser, query)
2555 assert comments, "No comments found"
2556
2557 structure = utils.find_yaml_associated_structure(comments[0])
2558 assert structure, "No associated structure found"
2559 assert structure.type == expected_type, (
2560 f"Expected type {expected_type}, got {structure.type}"
2561 )
2562
2563
2564def test_yaml_document_structure(init_yaml_tree_sitter):
2565 """Test YAML document structure handling."""
2566 code = b"""---
2567# Document comment
2568apiVersion: v1
2569kind: ConfigMap
2570metadata:
2571 name: my-config
2572data:
2573 # Data comment
2574 config.yml: |
2575 setting: value
2576 """
2577
2578 parser, query = init_yaml_tree_sitter
2579 comments = utils.extract_comments(code, parser, query)
2580 comments.sort(key=lambda x: x.start_point.row)
2581
2582 # Should find both comments
2583 assert len(comments) >= 2, f"Expected at least 2 comments, found {len(comments)}"
2584
2585 # First comment should associate with apiVersion
2586 first_structure = utils.find_yaml_associated_structure(comments[0])
2587 assert first_structure, "No structure found for first comment"
2588 first_text = first_structure.text.decode("utf-8")
2589 assert "apiVersion:" in first_text
2590
2591 # Second comment should associate with config.yml
2592 second_structure = utils.find_yaml_associated_structure(comments[1])
2593 assert second_structure, "No structure found for second comment"
2594 second_text = second_structure.text.decode("utf-8")
2595 assert "config.yml:" in second_text
2596
2597
2598def test_yaml_inline_comments_current_behavior(init_yaml_tree_sitter):
2599 """Test improved behavior of inline comments in YAML after the fix."""
2600 code = b"""key1: value1 # inline comment about key1
2601key2: value2
2602key3: value3 # inline comment about key3
2603"""
2604
2605 parser, query = init_yaml_tree_sitter
2606 comments = utils.extract_comments(code, parser, query)
2607 comments.sort(key=lambda x: x.start_point.row)
2608
2609 assert len(comments) == 2, f"Expected 2 comments, found {len(comments)}"
2610
2611 # Fixed behavior: inline comment about key1 now correctly associates with key1
2612 first_structure = utils.find_yaml_associated_structure(comments[0])
2613 assert first_structure, "No structure found for first comment"
2614 first_text = first_structure.text.decode("utf-8")
2615 assert "key1:" in first_text, f"Expected 'key1:' in '{first_text}'"
2616
2617 # Fixed behavior: inline comment about key3 now correctly associates with key3
2618 second_structure = utils.find_yaml_associated_structure(comments[1])
2619 assert second_structure, "No structure found for second comment"
2620 second_text = second_structure.text.decode("utf-8")
2621 assert "key3:" in second_text, f"Expected 'key3:' in '{second_text}'"
2622
2623
2624@pytest.mark.parametrize(
2625 ("code", "expected_associations"),
2626 [
2627 # Basic inline comment case
2628 (
2629 b"""key1: value1 # comment about key1
2630key2: value2
2631 """,
2632 ["key1:"], # Now correctly associates with key1
2633 ),
2634 # Multiple inline comments
2635 (
2636 b"""database:
2637 host: localhost # production server
2638 port: 5432 # default postgres port
2639 user: admin
2640 """,
2641 [
2642 "host: localhost",
2643 "port: 5432",
2644 ], # Now correctly associates with the right structures
2645 ),
2646 ],
2647)
2648def test_yaml_inline_comments_fixed_behavior(
2649 code, expected_associations, init_yaml_tree_sitter
2650):
2651 """Test that inline comments now correctly associate with the structure they comment on."""
2652 parser, query = init_yaml_tree_sitter
2653 comments = utils.extract_comments(code, parser, query)
2654 comments.sort(key=lambda x: x.start_point.row)
2655
2656 assert len(comments) == len(expected_associations), (
2657 f"Expected {len(expected_associations)} comments, found {len(comments)}"
2658 )
2659
2660 for i, comment in enumerate(comments):
2661 structure = utils.find_yaml_associated_structure(comment)
2662 assert structure, f"No structure found for comment {i}"
2663 structure_text = structure.text.decode("utf-8")
2664 assert expected_associations[i] in structure_text, (
2665 f"Expected '{expected_associations[i]}' in structure text: '{structure_text}'"
2666 )
2667
2668
2669@pytest.mark.parametrize(
2670 ("code", "expected_associations"),
2671 [
2672 # Inline comments with list items
2673 (
2674 b"""items:
2675 - name: item1 # first item
2676 - name: item2 # second item
2677 """,
2678 [
2679 "name: item1",
2680 "name: item2",
2681 ], # The inline comment finds the key-value pair within the list item
2682 ),
2683 # Mixed inline and block comments
2684 (
2685 b"""# Block comment for database
2686database:
2687 host: localhost # inline comment for host
2688 port: 5432
2689 # Block comment for user
2690 user: admin
2691 """,
2692 ["database:", "host: localhost", "user: admin"],
2693 ),
2694 # Inline comments in nested structures
2695 (
2696 b"""services:
2697 web:
2698 image: nginx # web server image
2699 ports:
2700 - "80:80" # http port
2701 """,
2702 ["image: nginx", '- "80:80"'],
2703 ),
2704 ],
2705)
2706def test_yaml_inline_comments_comprehensive(
2707 code, expected_associations, init_yaml_tree_sitter
2708):
2709 """Comprehensive test for inline comment behavior in various YAML structures."""
2710 parser, query = init_yaml_tree_sitter
2711 comments = utils.extract_comments(code, parser, query)
2712 comments.sort(key=lambda x: x.start_point.row)
2713
2714 assert len(comments) == len(expected_associations), (
2715 f"Expected {len(expected_associations)} comments, found {len(comments)}"
2716 )
2717
2718 for i, comment in enumerate(comments):
2719 structure = utils.find_yaml_associated_structure(comment)
2720 assert structure, (
2721 f"No structure found for comment {i}: '{comment.text.decode('utf-8')}'"
2722 )
2723 structure_text = structure.text.decode("utf-8")
2724 assert expected_associations[i] in structure_text, (
2725 f"Comment {i} '{comment.text.decode('utf-8')}' -> Expected '{expected_associations[i]}' in '{structure_text}'"
2726 )
2727
2728
2729@pytest.mark.parametrize(
2730 ("text", "expected"),
2731 [
2732 pytest.param("a@file.com", False, id="address"),
2733 pytest.param("x@module", False, id="no-left-boundary"),
2734 pytest.param("* @file", True, id="after-star"),
2735 pytest.param("\\file widget.h", True, id="backslash-at-start"),
2736 pytest.param("@filename", False, id="longer-word"),
2737 pytest.param("@file_x", False, id="underscore"),
2738 pytest.param("/// @fileoverview Widgets.", True, id="fileoverview"),
2739 pytest.param("@fileͅ x", True, id="non-ascii-mark-ends-the-word"),
2740 ],
2741)
2742def test_documents_file(text: str, expected: bool) -> None:
2743 """A file tag counts at the start of the text or after whitespace, ``*``, ``/`` or
2744 ``!``, and only as a whole ASCII word: an address or a longer word never counts,
2745 and a non-ASCII mark ends the word rather than extending it."""
2746 assert utils.documents_file(text) is expected
2747
2748
2749@pytest.mark.parametrize(
2750 "line",
2751 [
2752 ".COMPONENT",
2753 ".ROLE",
2754 ".FUNCTIONALITY",
2755 ".FORWARDHELPTARGETNAME Get-Item",
2756 ".FORWARDHELPCATEGORY Cmdlet",
2757 ".REMOTEHELPRUNSPACE $session",
2758 ".EXTERNALHELP Px-help.xml",
2759 ".forwardhelptargetname",
2760 ],
2761)
2762def test_every_powershell_help_keyword_makes_help(line, init_powershell_tree_sitter):
2763 """Each of PowerShell's comment-based-help keywords, the rarer ones included, makes
2764 a comment first in a function's body that function's help."""
2765 parser, query = init_powershell_tree_sitter
2766 source = f"function Outer {{\n <#\n {line}\n marker\n #>\n function Inner {{ }}\n}}\n"
2767 assert _powershell_attached(source, parser, query) == (
2768 "function Outer {",
2769 "doc_comment",
2770 )
2771
2772
2773@pytest.mark.parametrize(
2774 ("source", "expected"),
2775 [
2776 pytest.param(
2777 "Describe 'outer' {\n # marker\n Describe 'x' { } | Out-Null\n}\n",
2778 ("Describe 'x' { }", "leading"),
2779 id="leading-entity-first",
2780 ),
2781 pytest.param(
2782 "Describe 'outer' {\n It 'tx' { } | Out-Null # marker\n}\n",
2783 ("It 'tx' { }", "trailing"),
2784 id="trailing-entity-first",
2785 ),
2786 pytest.param(
2787 "Describe 'outer' {\n # marker\n Get-Thing | It 'y' { }\n}\n",
2788 ("It 'y' { }", "leading"),
2789 id="leading-entity-after-a-command",
2790 ),
2791 pytest.param(
2792 "Describe 'outer' {\n # marker\n It 'a' { } && It 'b' { }\n}\n",
2793 ("It 'a' { }", "leading"),
2794 id="leading-chain-of-two-entities",
2795 ),
2796 # the comment is in ForEach-Object's block, not in the Describe before it
2797 pytest.param(
2798 "Describe 'outer' {\n Describe 'x' { } | ForEach-Object {\n"
2799 " # marker\n $_\n }\n}\n",
2800 ("Describe 'outer' {", "enclosing"),
2801 id="enclosing-past-a-pipeline-entity-it-is-not-in",
2802 ),
2803 pytest.param(
2804 "Describe 'outer' {\n # marker\n Get-Thing | Out-Null\n}\n",
2805 ("Describe 'outer' {", "enclosing"),
2806 id="pipeline-of-no-entity",
2807 ),
2808 ],
2809)
2810def test_a_powershell_pipeline_is_its_first_entity(
2811 source, expected, init_powershell_tree_sitter
2812):
2813 """A pipeline (or a chain of them) is the first of its commands, in source order,
2814 that is an entity, on the leading and the trailing path alike; without one it is no
2815 entity. A comment inside a command of a pipeline is in that command, never in the
2816 pipeline's entity when it is another command."""
2817 parser, query = init_powershell_tree_sitter
2818 assert _powershell_attached(source, parser, query) == expected