[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
61def test_every_comment_type_is_wired():
62 """A comment type is a ``CommentType`` member plus an entry in each of several
63 tables; a member one of them misses fails here, rather than as a ``KeyError`` in
64 discovery or as no scope at all."""
65 members = set(CommentType)
66 assert set(COMMENT_FILETYPE) == {member.value for member in members}
67 # YAML and JSONC bind to data structures, not scopes (find_associated_scope)
68 assert set(utils.SCOPE_NODE_TYPES) == members - {
69 CommentType.yaml,
70 CommentType.jsonc,
71 }
72 assert set(utils.SCOPE_CONTAINER_TYPES) <= set(utils.SCOPE_NODE_TYPES)
73 for comment_type, predicates in utils.SCOPE_PREDICATES.items():
74 assert set(predicates) <= utils.SCOPE_NODE_TYPES[comment_type]
75 for member in members:
76 utils.init_tree_sitter(member)
77
78
79@pytest.mark.parametrize(
80 ("code", "result"),
81 [
82 (
83 b"""
84 // @req-id: need_001
85 void dummy_func1(){
86 }
87 """,
88 "void dummy_func1()",
89 ),
90 (
91 b"""
92 void dummy_func2(){
93 }
94 // @req-id: need_001
95 void dummy_func1(){
96 }
97 """,
98 "void dummy_func1()",
99 ),
100 (
101 b"""
102 void dummy_func1(){
103 a = 1;
104 /* @req-id: need_001 */
105 }
106 """,
107 "void dummy_func1()",
108 ),
109 (
110 b"""
111 void dummy_func1(){
112 // @req-id: need_001
113 a = 1;
114 }
115 void dummy_func2(){
116 }
117 """,
118 "void dummy_func1()",
119 ),
120 ],
121)
122def test_find_associated_scope_cpp(code, result, init_cpp_tree_sitter):
123 parser, query = init_cpp_tree_sitter
124 comments = utils.extract_comments(code, parser, query)
125 node: TreeSitterNode | None = utils.find_associated_scope(
126 comments[0], CommentType.cpp
127 )
128 assert node
129 assert node.text
130 func_def = node.text.decode("utf-8")
131 assert result in func_def
132
133
134@pytest.mark.parametrize(
135 ("code", "result"),
136 [
137 (
138 b"""
139 def dummy_func1():
140 # @req-id: need_001
141 pass
142 """,
143 "def dummy_func1()",
144 ),
145 (
146 b"""
147 def dummy_func1():
148 # @req-id: need_002
149 def dummy_func2():
150 pass
151 pass
152 """,
153 "def dummy_func2()",
154 ),
155 (
156 b"""
157 def dummy_func1():
158 '''@req-id: need_002'''
159 def nested_dummy_func():
160 pass
161 pass
162 """,
163 "def dummy_func1()",
164 ),
165 (
166 b"""
167 def dummy_func1():
168 def nested_dummy_func():
169 '''@req-id: need_002'''
170 pass
171 pass
172 """,
173 "def nested_dummy_func()",
174 ),
175 (
176 b"""
177 def dummy_func1():
178 def nested_dummy_func():
179 # @req-id: need_002
180 pass
181 pass
182 """,
183 "def nested_dummy_func()",
184 ),
185 (
186 b"""
187 def dummy_func1():
188 def nested_dummy_func():
189 pass
190 # @req-id: need_002
191 pass
192 """,
193 "def dummy_func1()",
194 ),
195 ],
196)
197def test_find_associated_scope_python(code, result, init_python_tree_sitter):
198 parser, query = init_python_tree_sitter
199 comments = utils.extract_comments(code, parser, query)
200 node: TreeSitterNode | None = utils.find_associated_scope(
201 comments[0], CommentType.python
202 )
203 assert node
204 assert node.text
205 func_def = node.text.decode("utf-8")
206 assert func_def.startswith(result)
207
208
209@pytest.mark.parametrize(
210 ("code", "result"),
211 [
212 (
213 b"""
214 // @req-id: need_001
215 public class DummyClass1
216 {
217 }
218 """,
219 "public class DummyClass1",
220 ),
221 (
222 b"""
223 public class DummyClass2
224 {
225 // @req-id: need_001
226 public void DummyFunc2()
227 {
228 }
229 }
230 """,
231 "public void DummyFunc2",
232 ),
233 (
234 b"""
235 public class DummyClass3
236 {
237 // @req-id: need_001
238 public string Property1 { get; set; }
239 }
240 """,
241 "public string Property1",
242 ),
243 ],
244)
245def test_find_associated_scope_csharp(code, result, init_csharp_tree_sitter):
246 parser, query = init_csharp_tree_sitter
247 comments = utils.extract_comments(code, parser, query)
248 node: TreeSitterNode | None = utils.find_associated_scope(
249 comments[0], CommentType.cs
250 )
251 assert node
252 assert node.text
253 func_def = node.text.decode("utf-8")
254 assert func_def.startswith(result)
255
256
257@pytest.mark.parametrize(
258 ("code", "result"),
259 [
260 (
261 b"""
262 # @req-id: need_001
263 database:
264 host: localhost
265 port: 5432
266 """,
267 "database:",
268 ),
269 (
270 b"""
271 services:
272 web:
273 # @req-id: need_002
274 image: nginx:latest
275 ports:
276 - "80:80"
277 """,
278 "image: nginx:latest",
279 ),
280 (
281 b"""
282 # @req-id: need_003
283 version: "3.8"
284 services:
285 app:
286 build: .
287 """,
288 "version:",
289 ),
290 (
291 b"""
292 items:
293 # @req-id: need_004
294 - name: item1
295 value: test
296 - name: item2
297 value: test2
298 """,
299 "- name: item1",
300 ),
301 ],
302)
303def test_find_associated_scope_yaml(code, result, init_yaml_tree_sitter):
304 parser, query = init_yaml_tree_sitter
305 comments = utils.extract_comments(code, parser, query)
306 node: TreeSitterNode | None = utils.find_associated_scope(
307 comments[0], CommentType.yaml
308 )
309 assert node
310 assert node.text
311 yaml_structure = node.text.decode("utf-8")
312 assert result in yaml_structure
313
314
315@pytest.mark.parametrize(
316 ("code", "result"),
317 [
318 (
319 b"""
320 // @req-id: need_001
321 fn dummy_func1() {
322 }
323 """,
324 "fn dummy_func1()",
325 ),
326 (
327 b"""
328 fn dummy_func2() {
329 }
330 // @req-id: need_001
331 fn dummy_func1() {
332 }
333 """,
334 "fn dummy_func1()",
335 ),
336 (
337 b"""
338 fn dummy_func1() {
339 let a = 1;
340 /* @req-id: need_001 */
341 }
342 """,
343 "fn dummy_func1()",
344 ),
345 (
346 b"""
347 fn dummy_func1() {
348 // @req-id: need_001
349 let a = 1;
350 }
351 fn dummy_func2() {
352 }
353 """,
354 "fn dummy_func1()",
355 ),
356 (
357 b"""
358 /// @req-id: need_001
359 fn dummy_func1() {
360 }
361 """,
362 "fn dummy_func1()",
363 ),
364 (
365 b"""
366 struct MyStruct {
367 // @req-id: need_001
368 field: i32,
369 }
370 """,
371 "struct MyStruct",
372 ),
373 ],
374)
375def test_find_associated_scope_rust(code, result, init_rust_tree_sitter):
376 parser, query = init_rust_tree_sitter
377 comments = utils.extract_comments(code, parser, query)
378 node: TreeSitterNode | None = utils.find_associated_scope(
379 comments[0], CommentType.rust
380 )
381 assert node
382 assert node.text
383 rust_def = node.text.decode("utf-8")
384 assert result in rust_def
385
386
387@pytest.mark.parametrize(
388 ("code", "result"),
389 [
390 # leading comment is associated with the following key/value pair
391 (
392 b'{\n // @req-id: need_001\n "alpha": 1\n}\n',
393 '"alpha": 1',
394 ),
395 # inline comment is associated with the array item on the same row
396 (
397 b'{\n "items": [\n "first", // @req-id: need_001\n "second"\n ]\n}\n',
398 '"first"',
399 ),
400 # inline comment is associated with the pair on the same row
401 (
402 b'{\n "alpha": 1, // @req-id: need_001\n "beta": 2\n}\n',
403 '"alpha": 1',
404 ),
405 # block comment is associated with the following pair
406 (
407 b'{\n /* @req-id: need_001 */\n "beta": 2\n}\n',
408 '"beta": 2',
409 ),
410 # trailing comment falls back to the enclosing object
411 (
412 b'{\n "alpha": 1\n // @req-id: need_001\n}\n',
413 '"alpha"',
414 ),
415 ],
416)
417def test_find_associated_scope_jsonc(code, result, init_jsonc_tree_sitter):
418 parser, query = init_jsonc_tree_sitter
419 comments = utils.extract_comments(code, parser, query)
420 node: TreeSitterNode | None = utils.find_associated_scope(
421 comments[0], CommentType.jsonc
422 )
423 assert node
424 assert node.text
425 jsonc_structure = node.text.decode("utf-8")
426 assert result in jsonc_structure
427
428
429@pytest.mark.parametrize(
430 ("code", "result"),
431 [
432 # comment above a POSIX-style function definition
433 (
434 b"""
435 # @req-id: need_001
436 greet() {
437 echo hi
438 }
439 """,
440 "greet()",
441 ),
442 # comment above the `function` keyword form
443 (
444 b"""
445 # @req-id: need_002
446 function greet {
447 echo hi
448 }
449 """,
450 "function greet",
451 ),
452 # comment inside a function body falls back to the enclosing function
453 (
454 b"""
455 greet() {
456 # @req-id: need_003
457 echo hi
458 }
459 """,
460 "greet()",
461 ),
462 ],
463)
464def test_find_associated_scope_bash(code, result, init_bash_tree_sitter):
465 parser, query = init_bash_tree_sitter
466 comments = utils.extract_comments(code, parser, query)
467 node: TreeSitterNode | None = utils.find_associated_scope(
468 comments[0], CommentType.bash
469 )
470 assert node
471 assert node.text
472 func_def = node.text.decode("utf-8")
473 assert func_def.startswith(result)
474
475
476@pytest.mark.parametrize(
477 ("code", "result"),
478 [
479 (
480 b"""
481 // @req-id: need_001
482 function dummyFunc1() {
483 }
484 """,
485 "function dummyFunc1()",
486 ),
487 (
488 b"""
489 class DummyClass {
490 // @req-id: need_001
491 method1() {
492 }
493 }
494 """,
495 "method1()",
496 ),
497 # leading comment on an exported function must descend into
498 # export_statement, not resolve to no scope
499 (
500 b"""
501 // @req-id: need_001
502 export function dummyFunc1() {
503 }
504 """,
505 "function dummyFunc1()",
506 ),
507 # leading comment on an exported class
508 (
509 b"""
510 // @req-id: need_001
511 export class DummyClass {
512 }
513 """,
514 "class DummyClass",
515 ),
516 # leading comment on an exported const arrow function
517 (
518 b"""
519 // @req-id: need_001
520 export const dummyFunc1 = () => {
521 };
522 """,
523 "dummyFunc1",
524 ),
525 # a plain (non-function) const between the comment and the function it
526 # documents must not steal the association
527 (
528 b"""
529 // @req-id: need_001
530 const helperFlag = true;
531 function dummyFunc1() {
532 }
533 """,
534 "function dummyFunc1()",
535 ),
536 # arrow-function const still resolves to itself
537 (
538 b"""
539 // @req-id: need_001
540 const dummyFunc1 = () => {
541 };
542 """,
543 "dummyFunc1",
544 ),
545 # JSX-returning component parses cleanly and resolves scope (.tsx content)
546 (
547 b"""
548 // @req-id: need_001
549 export function Button() {
550 return <button>Click me</button>;
551 }
552 """,
553 "function Button()",
554 ),
555 ],
556)
557def test_find_associated_scope_typescript(code, result, init_typescript_tree_sitter):
558 parser, query = init_typescript_tree_sitter
559 comments = utils.extract_comments(code, parser, query)
560 node: TreeSitterNode | None = utils.find_associated_scope(
561 comments[0], CommentType.ts
562 )
563 assert node
564 assert node.text
565 ts_def = node.text.decode("utf-8")
566 assert result in ts_def
567
568
569@pytest.mark.parametrize(
570 ("code", "result"),
571 [
572 (
573 b"""
574 def dummy_func1():
575 # @req-id: need_001
576 pass
577 """,
578 "def dummy_func1()",
579 ),
580 (
581 b"""
582 def dummy_func1():
583 '''@req-id: need_001'''
584 pass
585 """,
586 "def dummy_func1()",
587 ),
588 (
589 b"""
590 def dummy_func1():
591 def nested_dummy_func1():
592 '''@req-id: need_001'''
593 pass
594 pass
595 """,
596 "def nested_dummy_func1()",
597 ),
598 (
599 b"""
600 def dummy_func1():
601 '''@req-id: need_001'''
602 def nested_dummy_func1():
603 pass
604 pass
605 """,
606 "def dummy_func1()",
607 ),
608 ],
609)
610def test_find_enclosing_scope_python(code, result, init_python_tree_sitter):
611 parser, query = init_python_tree_sitter
612 comments = utils.extract_comments(code, parser, query)
613 node: TreeSitterNode | None = utils.find_enclosing_scope(
614 comments[0], CommentType.python
615 )
616 assert node
617 assert node.text
618 func_def = node.text.decode("utf-8")
619 assert result in func_def
620
621
622@pytest.mark.parametrize(
623 ("code", "result"),
624 [
625 (
626 b"""
627 # @req-id: need_001
628 def dummy_func1():
629 pass
630 """,
631 "def dummy_func1()",
632 ),
633 (
634 b"""
635 # @req-id: need_001
636 # @req-id: need_002
637 def dummy_func1():
638 pass
639 """,
640 "def dummy_func1()",
641 ),
642 ],
643)
644def test_find_next_scope_python(code, result, init_python_tree_sitter):
645 parser, query = init_python_tree_sitter
646 comments = utils.extract_comments(code, parser, query)
647 node: TreeSitterNode | None = utils.find_next_scope(comments[0], CommentType.python)
648 assert node
649 assert node.text
650 func_def = node.text.decode("utf-8")
651 assert result in func_def
652
653
654@pytest.mark.parametrize(
655 ("code", "result"),
656 [
657 (
658 b"""
659 // @req-id: need_001
660 void dummy_func1(){
661 }
662 """,
663 "void dummy_func1()",
664 ),
665 (
666 b"""
667 /* @req-id: need_001 */
668 void dummy_func1(){
669 }
670 """,
671 "void dummy_func1()",
672 ),
673 ],
674)
675def test_find_next_scope_cpp(code, result, init_cpp_tree_sitter):
676 parser, query = init_cpp_tree_sitter
677 comments = utils.extract_comments(code, parser, query)
678 node: TreeSitterNode | None = utils.find_next_scope(comments[0], CommentType.cpp)
679 assert node
680 assert node.text
681 func_def = node.text.decode("utf-8")
682 assert result in func_def
683
684
685@pytest.mark.parametrize(
686 ("code", "result"),
687 [
688 (
689 b"""
690 // @req-id: need_001
691 public class DummyClass1
692 {
693 }
694 """,
695 "public class DummyClass1",
696 ),
697 (
698 b"""
699
700 public class DummyClass1
701 {
702 /* @req-id: need_001 */
703 /* @req-id: need_002 */
704 public void DummyFunc1()
705 {
706 }
707 }
708 """,
709 "public void DummyFunc1",
710 ),
711 ],
712)
713def test_find_next_scope_csharp(code, result, init_csharp_tree_sitter):
714 parser, query = init_csharp_tree_sitter
715 comments = utils.extract_comments(code, parser, query)
716 node: TreeSitterNode | None = utils.find_next_scope(comments[0], CommentType.cs)
717 assert node
718 assert node.text
719 func_def = node.text.decode("utf-8")
720 assert result in func_def
721
722
723@pytest.mark.parametrize(
724 ("code", "result"),
725 [
726 (
727 b"""
728 // @req-id: need_001
729 function dummyFunc1() {
730 }
731 """,
732 "function dummyFunc1()",
733 ),
734 (
735 b"""
736 // @req-id: need_001
737 export function dummyFunc1() {
738 }
739 """,
740 "function dummyFunc1()",
741 ),
742 (
743 b"""
744 // @req-id: need_001
745 const helperFlag = true;
746 function dummyFunc1() {
747 }
748 """,
749 "function dummyFunc1()",
750 ),
751 ],
752)
753def test_find_next_scope_typescript(code, result, init_typescript_tree_sitter):
754 parser, query = init_typescript_tree_sitter
755 comments = utils.extract_comments(code, parser, query)
756 node: TreeSitterNode | None = utils.find_next_scope(comments[0], CommentType.ts)
757 assert node
758 assert node.text
759 func_def = node.text.decode("utf-8")
760 assert result in func_def
761
762
763def test_find_associated_scope_typescript_jsx_no_parse_error(
764 init_typescript_tree_sitter,
765):
766 """A JSX-returning component must parse cleanly under the TSX grammar."""
767 code = b"""
768 // @req-id: need_001
769 export function Button() {
770 return <button>Click me</button>;
771 }
772 """
773 parser, _ = init_typescript_tree_sitter
774 tree = parser.parse(code)
775 assert not tree.root_node.has_error
776
777
778@pytest.mark.parametrize(
779 ("name", "grammar"),
780 [
781 ("a.ts", "typescript"),
782 ("a.mts", "typescript"),
783 ("a.cts", "typescript"),
784 ("A.TS", "typescript"),
785 ("a.Mts", "typescript"),
786 # a declaration file's suffix is ``.ts``: TypeScript, as any ``.ts`` file
787 ("a.d.ts", "typescript"),
788 ("a.tsx", "tsx"),
789 ("a.js", "tsx"),
790 ("a.jsx", "tsx"),
791 ("a.mjs", "tsx"),
792 ("a.cjs", "tsx"),
793 ],
794)
795def test_ts_grammar_key(name: str, grammar: str) -> None:
796 """TypeScript's own suffixes, in any case, get the TypeScript grammar, where a
797 ``<T>x`` cast is legal; every other suffix of the family gets TSX."""
798 assert utils.ts_grammar_key(Path(name)) == grammar
799
800
801def test_typescript_ts_suffix_recovers_markers_around_angle_bracket_cast():
802 """A ``.ts`` file must use the plain TypeScript grammar, not TSX.
803
804 ``<string>x`` is a legacy angle-bracket type assertion: valid TypeScript
805 syntax, but JSX syntax under the TSX grammar. There it parses as a
806 ``jsx_opening_element`` and swallows the rest of the file into a single
807 ``jsx_text`` node, silently dropping every marker after it (``has_error``
808 is also set). ``init_tree_sitter`` must pick the plain TypeScript grammar
809 for a ``.ts`` path, via ``ts_grammar_key``, so markers both above and
810 below the cast all survive.
811 """
812 code = b"""// @Top, IMPL_TOP
813const v = <string>x;
814// @Bottom, IMPL_BOTTOM
815const y = 2;
816// @Third, IMPL_THIRD
817"""
818 parser, query = utils.init_tree_sitter(CommentType.ts, Path("dummy.ts"))
819 tree = parser.parse(code)
820 assert not tree.root_node.has_error
821
822 comments = utils.extract_comments(code, parser, query)
823 assert comments is not None
824 comments.sort(key=lambda node: node.start_point.row)
825 texts = [node.text.decode("utf-8") for node in comments if node.text]
826 assert texts == [
827 "// @Top, IMPL_TOP",
828 "// @Bottom, IMPL_BOTTOM",
829 "// @Third, IMPL_THIRD",
830 ]
831
832
833@pytest.mark.parametrize(
834 ("code", "result"),
835 [
836 (
837 b"""
838 void dummy_func1(){
839 // @req-id: need_001
840 }
841 """,
842 "void dummy_func1()",
843 ),
844 (
845 b"""
846 void dummy_func1(){
847 /* @req-id: need_001 */
848 }
849 """,
850 "void dummy_func1()",
851 ),
852 ],
853)
854def test_find_enclosing_scope_cpp(code, result, init_cpp_tree_sitter):
855 parser, query = init_cpp_tree_sitter
856 comments = utils.extract_comments(code, parser, query)
857 node: TreeSitterNode | None = utils.find_enclosing_scope(
858 comments[0], CommentType.cpp
859 )
860 assert node
861 assert node.text
862 func_def = node.text.decode("utf-8")
863 assert result in func_def
864
865
866@pytest.mark.parametrize(
867 ("code", "result"),
868 [
869 (
870 b"""
871 public class DummyClass1
872 {
873 // @req-id: need_001
874 }
875 """,
876 "public class DummyClass1",
877 ),
878 (
879 b"""
880 public class DummyClass1
881 {
882 public void DummyFunc1()
883 {
884 /* @req-id: need_001 */
885 }
886 }
887 """,
888 "public void DummyFunc1()",
889 ),
890 (
891 b"""
892 public class DummyClass1
893 {
894 public string DummyProperty1
895 {
896 get
897 {
898 /* @req-id: need_001 */
899 return "dummy";
900 }
901 }
902 }
903 """,
904 "public string DummyProperty1",
905 ),
906 ],
907)
908def test_find_enclosing_scope_csharp(code, result, init_csharp_tree_sitter):
909 parser, query = init_csharp_tree_sitter
910 comments = utils.extract_comments(code, parser, query)
911 node: TreeSitterNode | None = utils.find_enclosing_scope(
912 comments[0], CommentType.cs
913 )
914 assert node
915 assert node.text
916 func_def = node.text.decode("utf-8")
917 assert result in func_def
918
919
920@pytest.mark.parametrize(
921 ("code", "num_comments", "result"),
922 [
923 (
924 b"""
925 // @req-id: need_001
926 void dummy_func1(){
927 }
928 """,
929 1,
930 "// @req-id: need_001",
931 ),
932 (
933 b"""
934 void dummy_func1(){
935 // @req-id: need_001
936 }
937 """,
938 1,
939 "// @req-id: need_001",
940 ),
941 (
942 b"""
943 /* @req-id: need_001 */
944 void dummy_func1(){
945 }
946 """,
947 1,
948 "/* @req-id: need_001 */",
949 ),
950 (
951 b"""
952 // @req-id: need_001
953 //
954 //
955 void dummy_func1(){
956 }
957 """,
958 3,
959 "// @req-id: need_001",
960 ),
961 ],
962)
963def test_cpp_comment(code, num_comments, result, init_cpp_tree_sitter):
964 parser, query = init_cpp_tree_sitter
965 comments = utils.extract_comments(code, parser, query)
966 assert len(comments) == num_comments
967 comments.sort(key=lambda x: x.start_point.row)
968 assert comments[0].text
969 assert comments[0].text.decode("utf-8") == result
970
971
972@pytest.mark.parametrize(
973 ("code", "num_comments", "result"),
974 [
975 (
976 b"""
977 # @req-id: need_001
978 def dummy_func1():
979 pass
980 """,
981 1,
982 "# @req-id: need_001",
983 ),
984 (
985 b"""
986 def dummy_func1():
987 # @req-id: need_001
988 pass
989 """,
990 1,
991 "# @req-id: need_001",
992 ),
993 (
994 b"""
995 # single line comment
996 # @req-id: need_001
997 def dummy_func1():
998 pass
999 """,
1000 2,
1001 "# single line comment",
1002 ),
1003 (
1004 b"""
1005 def dummy_func1():
1006 '''
1007 @req-id: need_001
1008 '''
1009 pass
1010 """,
1011 1,
1012 "'''\n @req-id: need_001\n '''",
1013 ),
1014 (
1015 b"""
1016 def dummy_func1():
1017 text = '''@req-id: need_001, need_002, this docstring shall not be extracted as comment'''
1018 # @req-id: need_001
1019 pass
1020 """,
1021 1,
1022 "# @req-id: need_001",
1023 ),
1024 ],
1025)
1026def test_python_comment(code, num_comments, result, init_python_tree_sitter):
1027 parser, query = init_python_tree_sitter
1028 comments: list[TreeSitterNode] = utils.extract_comments(code, parser, query)
1029 comments.sort(key=lambda x: x.start_point.row)
1030 assert len(comments) == num_comments
1031 assert comments[0].text
1032 assert comments[0].text.decode("utf-8") == result
1033
1034
1035@pytest.mark.parametrize(
1036 ("code", "num_comments", "result"),
1037 [
1038 (
1039 b"""
1040 // @req-id: need_001
1041 void DummyFunc1(){
1042 }
1043 """,
1044 1,
1045 "// @req-id: need_001",
1046 ),
1047 (
1048 b"""
1049 void DummyFunc1(){
1050 // @req-id: need_001
1051 }
1052 """,
1053 1,
1054 "// @req-id: need_001",
1055 ),
1056 (
1057 b"""
1058 /* @req-id: need_001 */
1059 void DummyFunc1(){
1060 }
1061 """,
1062 1,
1063 "/* @req-id: need_001 */",
1064 ),
1065 (
1066 b"""
1067 // @req-id: need_001
1068 //
1069 //
1070 void DummyFunc1(){
1071 }
1072 """,
1073 3,
1074 "// @req-id: need_001",
1075 ),
1076 ],
1077)
1078def test_csharp_comment(code, num_comments, result, init_csharp_tree_sitter):
1079 parser, query = init_csharp_tree_sitter
1080 comments: list[TreeSitterNode] = utils.extract_comments(code, parser, query)
1081 comments.sort(key=lambda x: x.start_point.row)
1082 assert len(comments) == num_comments
1083 assert comments[0].text
1084 assert comments[0].text.decode("utf-8") == result
1085
1086
1087@pytest.mark.parametrize(
1088 ("code", "num_comments", "result"),
1089 [
1090 (
1091 b"""
1092 // @req-id: need_001
1093 function dummyFunc1() {
1094 }
1095 """,
1096 1,
1097 "// @req-id: need_001",
1098 ),
1099 (
1100 b"""
1101 /* @req-id: need_001 */
1102 const value = 1;
1103 """,
1104 1,
1105 "/* @req-id: need_001 */",
1106 ),
1107 ],
1108)
1109def test_typescript_comment(code, num_comments, result, init_typescript_tree_sitter):
1110 parser, query = init_typescript_tree_sitter
1111 comments: list[TreeSitterNode] = utils.extract_comments(code, parser, query)
1112 comments.sort(key=lambda x: x.start_point.row)
1113 assert len(comments) == num_comments
1114 assert comments[0].text
1115 assert comments[0].text.decode("utf-8") == result
1116
1117
1118@pytest.mark.parametrize(
1119 ("code", "num_comments", "result"),
1120 [
1121 (
1122 b"""
1123 # @req-id: need_001
1124 database:
1125 host: localhost
1126 """,
1127 1,
1128 "# @req-id: need_001",
1129 ),
1130 (
1131 b"""
1132 services:
1133 web:
1134 # @req-id: need_001
1135 image: nginx:latest
1136 """,
1137 1,
1138 "# @req-id: need_001",
1139 ),
1140 (
1141 b"""
1142 # Top level comment
1143 # @req-id: need_001
1144 version: "3.8"
1145 """,
1146 2,
1147 "# Top level comment",
1148 ),
1149 ],
1150)
1151def test_yaml_comment(code, num_comments, result, init_yaml_tree_sitter):
1152 parser, query = init_yaml_tree_sitter
1153 comments: list[TreeSitterNode] = utils.extract_comments(code, parser, query)
1154 comments.sort(key=lambda x: x.start_point.row)
1155 assert len(comments) == num_comments
1156 assert comments[0].text
1157 assert comments[0].text.decode("utf-8") == result
1158
1159
1160@pytest.mark.parametrize(
1161 ("code", "num_comments", "result"),
1162 [
1163 (
1164 b"""
1165 // @req-id: need_001
1166 func dummyFunc1() {
1167 }
1168 """,
1169 1,
1170 "// @req-id: need_001",
1171 ),
1172 (
1173 b"""
1174 func dummyFunc1() {
1175 // @req-id: need_001
1176 }
1177 """,
1178 1,
1179 "// @req-id: need_001",
1180 ),
1181 (
1182 b"""
1183 /* @req-id: need_001 */
1184 func dummyFunc1() {
1185 }
1186 """,
1187 1,
1188 "/* @req-id: need_001 */",
1189 ),
1190 (
1191 b"""
1192 // @req-id: need_001
1193 //
1194 //
1195 func dummyFunc1() {
1196 }
1197 """,
1198 3,
1199 "// @req-id: need_001",
1200 ),
1201 ],
1202)
1203def test_go_comment(code, num_comments, result, init_go_tree_sitter):
1204 parser, query = init_go_tree_sitter
1205 comments: list[TreeSitterNode] = utils.extract_comments(code, parser, query)
1206 comments.sort(key=lambda x: x.start_point.row)
1207 assert len(comments) == num_comments
1208 assert comments[0].text
1209 assert comments[0].text.decode("utf-8") == result
1210
1211
1212@pytest.mark.parametrize(
1213 ("code", "result"),
1214 [
1215 (
1216 b"""
1217 // @req-id: need_001
1218 func dummyFunc1() {
1219 }
1220 """,
1221 "func dummyFunc1()",
1222 ),
1223 (
1224 b"""
1225 func dummyFunc2() {
1226 }
1227 // @req-id: need_001
1228 func dummyFunc1() {
1229 }
1230 """,
1231 "func dummyFunc1()",
1232 ),
1233 (
1234 b"""
1235 /* @req-id: need_001 */
1236 func dummyFunc1() {
1237 }
1238 """,
1239 "func dummyFunc1()",
1240 ),
1241 ],
1242)
1243def test_find_associated_scope_go(code, result, init_go_tree_sitter):
1244 parser, query = init_go_tree_sitter
1245 comments = utils.extract_comments(code, parser, query)
1246 node: TreeSitterNode | None = utils.find_associated_scope(
1247 comments[0], CommentType.go
1248 )
1249 assert node
1250 assert node.text
1251 go_def = node.text.decode("utf-8")
1252 assert result in go_def
1253
1254
1255@pytest.mark.parametrize(
1256 ("code", "result"),
1257 [
1258 (
1259 b"""
1260 // @req-id: need_001
1261 func dummyFunc1() {
1262 }
1263 """,
1264 "func dummyFunc1()",
1265 ),
1266 (
1267 b"""
1268 // @req-id: need_001
1269 type DummyStruct struct {
1270 Field int
1271 }
1272 """,
1273 "type DummyStruct struct",
1274 ),
1275 ],
1276)
1277def test_find_next_scope_go(code, result, init_go_tree_sitter):
1278 parser, query = init_go_tree_sitter
1279 comments = utils.extract_comments(code, parser, query)
1280 node: TreeSitterNode | None = utils.find_next_scope(comments[0], CommentType.go)
1281 assert node
1282 assert node.text
1283 go_def = node.text.decode("utf-8")
1284 assert result in go_def
1285
1286
1287@pytest.mark.parametrize(
1288 ("code", "result"),
1289 [
1290 (
1291 b"""
1292 func dummyFunc1() {
1293 // @req-id: need_001
1294 }
1295 """,
1296 "func dummyFunc1()",
1297 ),
1298 (
1299 b"""
1300 func dummyFunc1() {
1301 /* @req-id: need_001 */
1302 }
1303 """,
1304 "func dummyFunc1()",
1305 ),
1306 ],
1307)
1308def test_find_enclosing_scope_go(code, result, init_go_tree_sitter):
1309 parser, query = init_go_tree_sitter
1310 comments = utils.extract_comments(code, parser, query)
1311 node: TreeSitterNode | None = utils.find_enclosing_scope(
1312 comments[0], CommentType.go
1313 )
1314 assert node
1315 assert node.text
1316 go_def = node.text.decode("utf-8")
1317 assert result in go_def
1318
1319
1320@pytest.mark.parametrize(
1321 ("git_url", "rev", "project_path", "filepath", "lineno", "result"),
1322 [
1323 (
1324 "git@github.com:useblocks/sphinx-codelinks.git",
1325 "beef1234",
1326 Path(__file__).parent.parent,
1327 # ABSOLUTE, and under `project_path`. `form_https_url` resolves the
1328 # filepath with `.absolute()`, i.e. against the CWD, so a relative one
1329 # here only agrees with `project_path` when pytest happens to run from
1330 # the package directory -- which it does not when the whole workspace's
1331 # suite is invoked from the repository root
1332 Path(__file__).parent.parent / "example" / "to" / "here",
1333 3,
1334 "https://github.com/useblocks/sphinx-codelinks/blob/beef1234/example/to/here#L3",
1335 )
1336 ],
1337)
1338def test_form_https_url(
1339 git_url, rev, project_path, filepath, lineno, result
1340): # need to have these args
1341 url = utils.form_https_url(git_url, rev, project_path, filepath, lineno=lineno)
1342 assert url == result
1343
1344
1345def get_git_path() -> str:
1346 """Get the path to the git executable."""
1347 git_path = shutil.which("git")
1348 if not git_path:
1349 raise FileNotFoundError("Git executable not found")
1350 if not Path(git_path).is_file():
1351 raise FileNotFoundError("Git executable path is invalid")
1352 return git_path
1353
1354
1355def init_git_repo(repo_path: Path, remote_url: str) -> Path:
1356 """Initialize a git repository for testing."""
1357 git_dir = repo_path / "test_repo"
1358 src_dir = git_dir / "src"
1359 src_dir.mkdir(parents=True)
1360
1361 git_path = get_git_path()
1362 if not git_path:
1363 raise FileNotFoundError("Git executable not found")
1364 if not Path(git_path).is_file():
1365 raise FileNotFoundError("Git executable path is invalid")
1366
1367 # Initialize git repo
1368 subprocess.run([git_path, "init"], cwd=git_dir, check=True, capture_output=True)
1369 subprocess.run(
1370 [git_path, "config", "user.email", "test@example.com"], cwd=git_dir, check=True
1371 )
1372 subprocess.run(
1373 [git_path, "config", "user.name", "Test User"], cwd=git_dir, check=True
1374 )
1375
1376 # Create a test file and commit
1377 test_file = src_dir / "test_file.py"
1378 test_file.write_text("# Test file\nprint('hello')\n")
1379 subprocess.run([git_path, "add", "."], cwd=git_dir, check=True)
1380 subprocess.run(
1381 [git_path, "commit", "-m", "Initial commit"], cwd=git_dir, check=True
1382 )
1383
1384 # Add a remote
1385 subprocess.run(
1386 [git_path, "remote", "add", "origin", remote_url],
1387 cwd=git_dir,
1388 check=True,
1389 )
1390
1391 return git_dir
1392
1393
1394@pytest.fixture(
1395 params=[
1396 ("test_repo_git", "git@github.com:test-user/test-repo.git"),
1397 ("test_repo_https", "https://github.com/test-user/test-repo.git"),
1398 ]
1399)
1400def git_repo(tmp_path: str, request: pytest.FixtureRequest) -> tuple[Path, str]:
1401 """Create git repos for testing."""
1402 repo_name, remote_url = request.param
1403 repo_path = Path(tmp_path) / repo_name
1404 repo_path = init_git_repo(repo_path, remote_url)
1405 return repo_path, remote_url
1406
1407
1408def get_current_commit_hash(git_dir: Path) -> str:
1409 """Get the current commit hash of the git repository."""
1410 git_path = get_git_path()
1411 result = subprocess.run(
1412 [git_path, "rev-parse", "HEAD"],
1413 cwd=git_dir,
1414 check=True,
1415 capture_output=True,
1416 text=True,
1417 )
1418
1419 return str(result.stdout.strip())
1420
1421
1422def test_locate_git_root(git_repo: tuple[Path, str]) -> None:
1423 repo_path = git_repo[0]
1424 src_dir = repo_path / "src"
1425 git_root = utils.locate_git_root(src_dir)
1426 assert git_root == repo_path
1427
1428
1429def test_get_remote_url(git_repo: tuple[Path, str]) -> None:
1430 repo_path, expected_url = git_repo
1431 remote_url = utils.get_remote_url(repo_path)
1432 assert remote_url == expected_url
1433
1434
1435def test_get_current_rev(git_repo: tuple[Path, str]) -> None:
1436 repo_path, _ = git_repo
1437 current_rev = get_current_commit_hash(repo_path)
1438 assert current_rev == utils.get_current_rev(repo_path)
1439
1440
1441@pytest.fixture(params=["detached", "branch"])
1442def git_worktree(
1443 tmp_path: Path, request: pytest.FixtureRequest
1444) -> tuple[Path, str, str]:
1445 """A LINKED WORKTREE of a scratch repository, in both shapes git makes.
1446
1447 A worktree is not a checkout with a `.git` directory: `.git` is a FILE holding
1448 `gitdir: <path>`, `config` lives in the main repository's git directory (named by
1449 the worktree's `commondir`), and only per-worktree state -- `HEAD` among it -- is in
1450 the worktree's own. `detached` puts a SHA in `HEAD`; `branch` puts `ref:
1451 refs/heads/...`, whose target is a shared ref in the common directory.
1452
1453 This repository is worked in worktrees as a matter of routine, so a helper that
1454 demands a `.git` directory fails for every developer while staying green in CI
1455 (useblocks/sphinx-codelinks#106).
1456 """
1457 remote_url = "https://github.com/test-user/test-repo.git"
1458 repo_path = init_git_repo(tmp_path / "main", remote_url)
1459 git_path = get_git_path()
1460 worktree_path = tmp_path / "worktree"
1461 add = [git_path, "worktree", "add"]
1462 add += (
1463 ["--detach", str(worktree_path), "HEAD"]
1464 if request.param == "detached"
1465 else [str(worktree_path), "-b", "a-worktree-branch"]
1466 )
1467 subprocess.run(add, cwd=repo_path, check=True, capture_output=True)
1468 assert (worktree_path / ".git").is_file(), "a linked worktree's .git is a file"
1469 return worktree_path, remote_url, get_current_commit_hash(repo_path)
1470
1471
1472def test_locate_git_root_worktree(git_worktree: tuple[Path, str, str]) -> None:
1473 worktree_path, _, _ = git_worktree
1474 assert utils.locate_git_root(worktree_path / "src") == worktree_path
1475
1476
1477def test_get_remote_url_worktree(git_worktree: tuple[Path, str, str]) -> None:
1478 worktree_path, remote_url, _ = git_worktree
1479 assert utils.get_remote_url(worktree_path) == remote_url
1480
1481
1482def test_get_current_rev_worktree(git_worktree: tuple[Path, str, str]) -> None:
1483 worktree_path, _, rev = git_worktree
1484 assert utils.get_current_rev(worktree_path) == rev
1485
1486
1487def test_get_current_rev_packed_refs(git_worktree: tuple[Path, str, str]) -> None:
1488 """`git gc` packs the loose refs away, and the branch ref is then in `packed-refs`.
1489
1490 Nothing unusual has to happen for this: `git gc` runs unattended on any long-lived
1491 checkout, and afterwards there is no `refs/heads/<branch>` FILE in either the
1492 worktree's git directory or the common one. It is the same failure shape as
1493 useblocks/sphinx-codelinks#106 reached by a different route -- and CI never sees it,
1494 because `actions/checkout` and Read the Docs both leave a detached HEAD.
1495 """
1496 worktree_path, _, rev = git_worktree
1497 git_path = get_git_path()
1498 subprocess.run(
1499 [git_path, "pack-refs", "--all"],
1500 cwd=worktree_path,
1501 check=True,
1502 capture_output=True,
1503 )
1504 git_dir = utils._git_dir(worktree_path)
1505 assert git_dir is not None
1506 common = utils._git_common_dir(git_dir)
1507 assert not list((common / "refs" / "heads").glob("*")), (
1508 "the loose refs should be gone"
1509 )
1510
1511 assert utils.get_current_rev(worktree_path) == rev
1512
1513
1514def test_get_current_rev_detached_head(tmp_path: Path) -> None:
1515 """In a detached HEAD (e.g. CI checkouts) .git/HEAD holds the commit SHA
1516 directly; get_current_rev returns it rather than warning and giving up."""
1517 git_root = tmp_path / "repo"
1518 (git_root / ".git").mkdir(parents=True)
1519 sha = "a1b2c3d4e5f60718293a4b5c6d7e8f9012345678"
1520 (git_root / ".git" / "HEAD").write_text(f"{sha}\n")
1521
1522 assert utils.get_current_rev(git_root) == sha
1523
1524
1525# ========== YAML-specific tests ==========
1526
1527
1528@pytest.mark.parametrize(
1529 ("code", "expected_structure"),
1530 [
1531 # Basic key-value pair
1532 (
1533 b"""
1534 # Comment before key
1535 database:
1536 host: localhost
1537 """,
1538 "database:",
1539 ),
1540 # Comment in nested structure
1541 (
1542 b"""
1543 services:
1544 web:
1545 # Comment before image
1546 image: nginx:latest
1547 """,
1548 "image: nginx:latest",
1549 ),
1550 # Comment before list item
1551 (
1552 b"""
1553 items:
1554 # Comment before list item
1555 - name: item1
1556 value: test
1557 """,
1558 "- name: item1",
1559 ),
1560 # Comment in document structure
1561 (
1562 b"""---
1563# Comment in document
1564version: "3.8"
1565services:
1566 app:
1567 build: .
1568 """,
1569 "version:",
1570 ),
1571 # Flow mapping structure
1572 (
1573 b"""
1574 # Comment before flow mapping
1575 config: {debug: true, port: 8080}
1576 """,
1577 "config:",
1578 ),
1579 ],
1580)
1581def test_find_yaml_next_structure(code, expected_structure, init_yaml_tree_sitter):
1582 """Test the find_yaml_next_structure function."""
1583 parser, query = init_yaml_tree_sitter
1584 comments = utils.extract_comments(code, parser, query)
1585 assert comments, "No comments found in the code"
1586
1587 next_structure = utils.find_yaml_next_structure(comments[0])
1588 assert next_structure, "No next structure found"
1589 structure_text = next_structure.text.decode("utf-8")
1590 assert expected_structure in structure_text
1591
1592
1593@pytest.mark.parametrize(
1594 ("code", "expected_structure"),
1595 [
1596 # Comment associated with key-value pair
1597 (
1598 b"""
1599 # Database configuration
1600 database:
1601 host: localhost
1602 port: 5432
1603 """,
1604 "database:",
1605 ),
1606 # Comment associated with nested structure
1607 (
1608 b"""
1609 services:
1610 web:
1611 # Web service image
1612 image: nginx:latest
1613 ports:
1614 - "80:80"
1615 """,
1616 "image: nginx:latest",
1617 ),
1618 # Comment associated with list item
1619 (
1620 b"""
1621 dependencies:
1622 # First dependency
1623 - name: redis
1624 version: "6.0"
1625 - name: postgres
1626 version: "13"
1627 """,
1628 "- name: redis",
1629 ),
1630 # Comment inside parent structure
1631 (
1632 b"""
1633 app:
1634 # Internal comment
1635 name: myapp
1636 version: "1.0"
1637 """,
1638 "name: myapp",
1639 ),
1640 ],
1641)
1642def test_find_yaml_associated_structure(
1643 code, expected_structure, init_yaml_tree_sitter
1644):
1645 """Test the find_yaml_associated_structure function."""
1646 parser, query = init_yaml_tree_sitter
1647 comments = utils.extract_comments(code, parser, query)
1648 assert comments, "No comments found in the code"
1649
1650 associated_structure = utils.find_yaml_associated_structure(comments[0])
1651 assert associated_structure, "No associated structure found"
1652 structure_text = associated_structure.text.decode("utf-8")
1653 assert expected_structure in structure_text
1654
1655
1656@pytest.mark.parametrize(
1657 ("code", "expected_results"),
1658 [
1659 # Multiple comments in sequence
1660 (
1661 b"""
1662 # First comment
1663 # Second comment
1664 database:
1665 host: localhost
1666 """,
1667 ["database:", "database:"], # Both comments should associate with database
1668 ),
1669 # Comments at different nesting levels
1670 (
1671 b"""
1672 # Top level comment
1673 services:
1674 web:
1675 # Nested comment
1676 image: nginx:latest
1677 """,
1678 ["services:", "image: nginx:latest"],
1679 ),
1680 ],
1681)
1682def test_multiple_yaml_comments(code, expected_results, init_yaml_tree_sitter):
1683 """Test handling of multiple YAML comments in the same file."""
1684 parser, query = init_yaml_tree_sitter
1685 comments = utils.extract_comments(code, parser, query)
1686 comments.sort(key=lambda x: x.start_point.row)
1687
1688 assert len(comments) == len(expected_results), (
1689 f"Expected {len(expected_results)} comments, found {len(comments)}"
1690 )
1691
1692 for i, comment in enumerate(comments):
1693 associated_structure = utils.find_yaml_associated_structure(comment)
1694 assert associated_structure, f"No associated structure found for comment {i}"
1695 structure_text = associated_structure.text.decode("utf-8")
1696 assert expected_results[i] in structure_text
1697
1698
1699@pytest.mark.parametrize(
1700 ("code", "has_structure"),
1701 [
1702 # Comment at end of file with no following structure
1703 (
1704 b"""
1705database:
1706 host: localhost
1707# End of file comment
1708 """,
1709 True, # This will actually find the parent database structure
1710 ),
1711 # Comment with only whitespace after
1712 (
1713 b"""
1714 # Lonely comment
1715
1716
1717 """,
1718 False,
1719 ),
1720 # Comment before valid structure
1721 (
1722 b"""
1723 # Valid comment
1724 key: value
1725 """,
1726 True,
1727 ),
1728 ],
1729)
1730def test_yaml_edge_cases(code, has_structure, init_yaml_tree_sitter):
1731 """Test edge cases in YAML comment processing."""
1732 parser, query = init_yaml_tree_sitter
1733 comments = utils.extract_comments(code, parser, query)
1734
1735 if comments:
1736 structure = utils.find_yaml_associated_structure(comments[0])
1737 if has_structure:
1738 assert structure, "Expected to find associated structure"
1739 else:
1740 assert structure is None, "Expected no associated structure"
1741 else:
1742 assert not has_structure, "No comments found but structure was expected"
1743
1744
1745@pytest.mark.parametrize(
1746 ("code", "expected_structures"),
1747 [
1748 # Simpler nested YAML structure
1749 (
1750 b"""# Global configuration
1751version: "3.8"
1752
1753# Services section
1754services:
1755 web:
1756 image: nginx:latest
1757 # Port configuration
1758 ports:
1759 - "80:80"
1760 """,
1761 [
1762 "version:", # Global configuration
1763 "services:", # Services section
1764 '- "80:80"', # Port configuration
1765 ],
1766 ),
1767 ],
1768)
1769def test_complex_yaml_structure(code, expected_structures, init_yaml_tree_sitter):
1770 """Test complex nested YAML structures with multiple comments."""
1771 parser, query = init_yaml_tree_sitter
1772 comments = utils.extract_comments(code, parser, query)
1773 comments.sort(key=lambda x: x.start_point.row)
1774
1775 assert len(comments) == len(expected_structures), (
1776 f"Expected {len(expected_structures)} comments, found {len(comments)}"
1777 )
1778
1779 for i, comment in enumerate(comments):
1780 associated_structure = utils.find_yaml_associated_structure(comment)
1781 assert associated_structure, f"No associated structure found for comment {i}"
1782 structure_text = associated_structure.text.decode("utf-8")
1783 assert expected_structures[i] in structure_text, (
1784 f"Expected '{expected_structures[i]}' in structure text: '{structure_text}'"
1785 )
1786
1787
1788@pytest.mark.parametrize(
1789 ("code", "expected_type"),
1790 [
1791 # Block mapping pair
1792 (
1793 b"""
1794 # Comment
1795 key: value
1796 """,
1797 "block_mapping_pair",
1798 ),
1799 # Block sequence item
1800 (
1801 b"""
1802 items:
1803 # Comment
1804 - item1
1805 """,
1806 "block_sequence_item",
1807 ),
1808 # Nested block mapping
1809 (
1810 b"""
1811 services:
1812 # Comment
1813 web:
1814 image: nginx
1815 """,
1816 "block_mapping_pair",
1817 ),
1818 ],
1819)
1820def test_yaml_structure_types(code, expected_type, init_yaml_tree_sitter):
1821 """Test that YAML structures return the correct node types."""
1822 parser, query = init_yaml_tree_sitter
1823 comments = utils.extract_comments(code, parser, query)
1824 assert comments, "No comments found"
1825
1826 structure = utils.find_yaml_associated_structure(comments[0])
1827 assert structure, "No associated structure found"
1828 assert structure.type == expected_type, (
1829 f"Expected type {expected_type}, got {structure.type}"
1830 )
1831
1832
1833def test_yaml_document_structure(init_yaml_tree_sitter):
1834 """Test YAML document structure handling."""
1835 code = b"""---
1836# Document comment
1837apiVersion: v1
1838kind: ConfigMap
1839metadata:
1840 name: my-config
1841data:
1842 # Data comment
1843 config.yml: |
1844 setting: value
1845 """
1846
1847 parser, query = init_yaml_tree_sitter
1848 comments = utils.extract_comments(code, parser, query)
1849 comments.sort(key=lambda x: x.start_point.row)
1850
1851 # Should find both comments
1852 assert len(comments) >= 2, f"Expected at least 2 comments, found {len(comments)}"
1853
1854 # First comment should associate with apiVersion
1855 first_structure = utils.find_yaml_associated_structure(comments[0])
1856 assert first_structure, "No structure found for first comment"
1857 first_text = first_structure.text.decode("utf-8")
1858 assert "apiVersion:" in first_text
1859
1860 # Second comment should associate with config.yml
1861 second_structure = utils.find_yaml_associated_structure(comments[1])
1862 assert second_structure, "No structure found for second comment"
1863 second_text = second_structure.text.decode("utf-8")
1864 assert "config.yml:" in second_text
1865
1866
1867def test_yaml_inline_comments_current_behavior(init_yaml_tree_sitter):
1868 """Test improved behavior of inline comments in YAML after the fix."""
1869 code = b"""key1: value1 # inline comment about key1
1870key2: value2
1871key3: value3 # inline comment about key3
1872"""
1873
1874 parser, query = init_yaml_tree_sitter
1875 comments = utils.extract_comments(code, parser, query)
1876 comments.sort(key=lambda x: x.start_point.row)
1877
1878 assert len(comments) == 2, f"Expected 2 comments, found {len(comments)}"
1879
1880 # Fixed behavior: inline comment about key1 now correctly associates with key1
1881 first_structure = utils.find_yaml_associated_structure(comments[0])
1882 assert first_structure, "No structure found for first comment"
1883 first_text = first_structure.text.decode("utf-8")
1884 assert "key1:" in first_text, f"Expected 'key1:' in '{first_text}'"
1885
1886 # Fixed behavior: inline comment about key3 now correctly associates with key3
1887 second_structure = utils.find_yaml_associated_structure(comments[1])
1888 assert second_structure, "No structure found for second comment"
1889 second_text = second_structure.text.decode("utf-8")
1890 assert "key3:" in second_text, f"Expected 'key3:' in '{second_text}'"
1891
1892
1893@pytest.mark.parametrize(
1894 ("code", "expected_associations"),
1895 [
1896 # Basic inline comment case
1897 (
1898 b"""key1: value1 # comment about key1
1899key2: value2
1900 """,
1901 ["key1:"], # Now correctly associates with key1
1902 ),
1903 # Multiple inline comments
1904 (
1905 b"""database:
1906 host: localhost # production server
1907 port: 5432 # default postgres port
1908 user: admin
1909 """,
1910 [
1911 "host: localhost",
1912 "port: 5432",
1913 ], # Now correctly associates with the right structures
1914 ),
1915 ],
1916)
1917def test_yaml_inline_comments_fixed_behavior(
1918 code, expected_associations, init_yaml_tree_sitter
1919):
1920 """Test that inline comments now correctly associate with the structure they comment on."""
1921 parser, query = init_yaml_tree_sitter
1922 comments = utils.extract_comments(code, parser, query)
1923 comments.sort(key=lambda x: x.start_point.row)
1924
1925 assert len(comments) == len(expected_associations), (
1926 f"Expected {len(expected_associations)} comments, found {len(comments)}"
1927 )
1928
1929 for i, comment in enumerate(comments):
1930 structure = utils.find_yaml_associated_structure(comment)
1931 assert structure, f"No structure found for comment {i}"
1932 structure_text = structure.text.decode("utf-8")
1933 assert expected_associations[i] in structure_text, (
1934 f"Expected '{expected_associations[i]}' in structure text: '{structure_text}'"
1935 )
1936
1937
1938@pytest.mark.parametrize(
1939 ("code", "expected_associations"),
1940 [
1941 # Inline comments with list items
1942 (
1943 b"""items:
1944 - name: item1 # first item
1945 - name: item2 # second item
1946 """,
1947 [
1948 "name: item1",
1949 "name: item2",
1950 ], # The inline comment finds the key-value pair within the list item
1951 ),
1952 # Mixed inline and block comments
1953 (
1954 b"""# Block comment for database
1955database:
1956 host: localhost # inline comment for host
1957 port: 5432
1958 # Block comment for user
1959 user: admin
1960 """,
1961 ["database:", "host: localhost", "user: admin"],
1962 ),
1963 # Inline comments in nested structures
1964 (
1965 b"""services:
1966 web:
1967 image: nginx # web server image
1968 ports:
1969 - "80:80" # http port
1970 """,
1971 ["image: nginx", '- "80:80"'],
1972 ),
1973 ],
1974)
1975def test_yaml_inline_comments_comprehensive(
1976 code, expected_associations, init_yaml_tree_sitter
1977):
1978 """Comprehensive test for inline comment behavior in various YAML structures."""
1979 parser, query = init_yaml_tree_sitter
1980 comments = utils.extract_comments(code, parser, query)
1981 comments.sort(key=lambda x: x.start_point.row)
1982
1983 assert len(comments) == len(expected_associations), (
1984 f"Expected {len(expected_associations)} comments, found {len(comments)}"
1985 )
1986
1987 for i, comment in enumerate(comments):
1988 structure = utils.find_yaml_associated_structure(comment)
1989 assert structure, (
1990 f"No structure found for comment {i}: '{comment.text.decode('utf-8')}'"
1991 )
1992 structure_text = structure.text.decode("utf-8")
1993 assert expected_associations[i] in structure_text, (
1994 f"Comment {i} '{comment.text.decode('utf-8')}' -> Expected '{expected_associations[i]}' in '{structure_text}'"
1995 )