[docs] 1# @Test suite for tree-sitter parsing utilities and language support, TEST_LANG_1, test, [IMPL_LANG_1, IMPL_EXTR_1, IMPL_RST_1]
2from pathlib import Path
3import shutil
4import subprocess
5
6import pytest
7from tree_sitter import Language, Parser, Query
8from tree_sitter import Node as TreeSitterNode
9import tree_sitter_bash
10import tree_sitter_c_sharp
11import tree_sitter_cpp
12import tree_sitter_go
13import tree_sitter_json
14import tree_sitter_python
15import tree_sitter_rust
16import tree_sitter_yaml
17
18from sphinx_codelinks.analyse import utils
19from sphinx_codelinks.config import UNIX_NEWLINE
20from sphinx_codelinks.source_discover.config import CommentType
21
22
23@pytest.fixture(scope="session")
24def init_cpp_tree_sitter() -> tuple[Parser, Query]:
25 parsed_language = Language(tree_sitter_cpp.language())
26 query = Query(parsed_language, utils.CPP_QUERY)
27 parser = Parser(parsed_language)
28 return parser, query
29
30
31@pytest.fixture(scope="session")
32def init_python_tree_sitter() -> tuple[Parser, Query]:
33 parsed_language = Language(tree_sitter_python.language())
34 query = Query(parsed_language, utils.PYTHON_QUERY)
35 parser = Parser(parsed_language)
36 return parser, query
37
38
39@pytest.fixture(scope="session")
40def init_csharp_tree_sitter() -> tuple[Parser, Query]:
41 parsed_language = Language(tree_sitter_c_sharp.language())
42 query = Query(parsed_language, utils.C_SHARP_QUERY)
43 parser = Parser(parsed_language)
44 return parser, query
45
46
47@pytest.fixture(scope="session")
48def init_yaml_tree_sitter() -> tuple[Parser, Query]:
49 parsed_language = Language(tree_sitter_yaml.language())
50 query = Query(parsed_language, utils.YAML_QUERY)
51 parser = Parser(parsed_language)
52 return parser, query
53
54
55@pytest.fixture(scope="session")
56def init_rust_tree_sitter() -> tuple[Parser, Query]:
57 parsed_language = Language(tree_sitter_rust.language())
58 query = Query(parsed_language, utils.RUST_QUERY)
59 parser = Parser(parsed_language)
60 return parser, query
61
62
63@pytest.fixture(scope="session")
64def init_go_tree_sitter() -> tuple[Parser, Query]:
65 parsed_language = Language(tree_sitter_go.language())
66 query = Query(parsed_language, utils.GO_QUERY)
67 parser = Parser(parsed_language)
68 return parser, query
69
70
71@pytest.fixture(scope="session")
72def init_jsonc_tree_sitter() -> tuple[Parser, Query]:
73 parsed_language = Language(tree_sitter_json.language())
74 query = Query(parsed_language, utils.JSONC_QUERY)
75 parser = Parser(parsed_language)
76 return parser, query
77
78
79@pytest.fixture(scope="session")
80def init_bash_tree_sitter() -> tuple[Parser, Query]:
81 parsed_language = Language(tree_sitter_bash.language())
82 query = Query(parsed_language, utils.BASH_QUERY)
83 parser = Parser(parsed_language)
84 return parser, query
85
86
87@pytest.mark.parametrize(
88 ("code", "result"),
89 [
90 (
91 b"""
92 // @req-id: need_001
93 void dummy_func1(){
94 }
95 """,
96 "void dummy_func1()",
97 ),
98 (
99 b"""
100 void dummy_func2(){
101 }
102 // @req-id: need_001
103 void dummy_func1(){
104 }
105 """,
106 "void dummy_func1()",
107 ),
108 (
109 b"""
110 void dummy_func1(){
111 a = 1;
112 /* @req-id: need_001 */
113 }
114 """,
115 "void dummy_func1()",
116 ),
117 (
118 b"""
119 void dummy_func1(){
120 // @req-id: need_001
121 a = 1;
122 }
123 void dummy_func2(){
124 }
125 """,
126 "void dummy_func1()",
127 ),
128 ],
129)
130def test_find_associated_scope_cpp(code, result, init_cpp_tree_sitter):
131 parser, query = init_cpp_tree_sitter
132 comments = utils.extract_comments(code, parser, query)
133 node: TreeSitterNode | None = utils.find_associated_scope(
134 comments[0], CommentType.cpp
135 )
136 assert node
137 assert node.text
138 func_def = node.text.decode("utf-8")
139 assert result in func_def
140
141
142@pytest.mark.parametrize(
143 ("code", "result"),
144 [
145 (
146 b"""
147 def dummy_func1():
148 # @req-id: need_001
149 pass
150 """,
151 "def dummy_func1()",
152 ),
153 (
154 b"""
155 def dummy_func1():
156 # @req-id: need_002
157 def dummy_func2():
158 pass
159 pass
160 """,
161 "def dummy_func2()",
162 ),
163 (
164 b"""
165 def dummy_func1():
166 '''@req-id: need_002'''
167 def nested_dummy_func():
168 pass
169 pass
170 """,
171 "def dummy_func1()",
172 ),
173 (
174 b"""
175 def dummy_func1():
176 def nested_dummy_func():
177 '''@req-id: need_002'''
178 pass
179 pass
180 """,
181 "def nested_dummy_func()",
182 ),
183 (
184 b"""
185 def dummy_func1():
186 def nested_dummy_func():
187 # @req-id: need_002
188 pass
189 pass
190 """,
191 "def nested_dummy_func()",
192 ),
193 (
194 b"""
195 def dummy_func1():
196 def nested_dummy_func():
197 pass
198 # @req-id: need_002
199 pass
200 """,
201 "def dummy_func1()",
202 ),
203 ],
204)
205def test_find_associated_scope_python(code, result, init_python_tree_sitter):
206 parser, query = init_python_tree_sitter
207 comments = utils.extract_comments(code, parser, query)
208 node: TreeSitterNode | None = utils.find_associated_scope(
209 comments[0], CommentType.python
210 )
211 assert node
212 assert node.text
213 func_def = node.text.decode("utf-8")
214 assert func_def.startswith(result)
215
216
217@pytest.mark.parametrize(
218 ("code", "result"),
219 [
220 (
221 b"""
222 // @req-id: need_001
223 public class DummyClass1
224 {
225 }
226 """,
227 "public class DummyClass1",
228 ),
229 (
230 b"""
231 public class DummyClass2
232 {
233 // @req-id: need_001
234 public void DummyFunc2()
235 {
236 }
237 }
238 """,
239 "public void DummyFunc2",
240 ),
241 (
242 b"""
243 public class DummyClass3
244 {
245 // @req-id: need_001
246 public string Property1 { get; set; }
247 }
248 """,
249 "public string Property1",
250 ),
251 ],
252)
253def test_find_associated_scope_csharp(code, result, init_csharp_tree_sitter):
254 parser, query = init_csharp_tree_sitter
255 comments = utils.extract_comments(code, parser, query)
256 node: TreeSitterNode | None = utils.find_associated_scope(
257 comments[0], CommentType.cs
258 )
259 assert node
260 assert node.text
261 func_def = node.text.decode("utf-8")
262 assert func_def.startswith(result)
263
264
265@pytest.mark.parametrize(
266 ("code", "result"),
267 [
268 (
269 b"""
270 # @req-id: need_001
271 database:
272 host: localhost
273 port: 5432
274 """,
275 "database:",
276 ),
277 (
278 b"""
279 services:
280 web:
281 # @req-id: need_002
282 image: nginx:latest
283 ports:
284 - "80:80"
285 """,
286 "image: nginx:latest",
287 ),
288 (
289 b"""
290 # @req-id: need_003
291 version: "3.8"
292 services:
293 app:
294 build: .
295 """,
296 "version:",
297 ),
298 (
299 b"""
300 items:
301 # @req-id: need_004
302 - name: item1
303 value: test
304 - name: item2
305 value: test2
306 """,
307 "- name: item1",
308 ),
309 ],
310)
311def test_find_associated_scope_yaml(code, result, init_yaml_tree_sitter):
312 parser, query = init_yaml_tree_sitter
313 comments = utils.extract_comments(code, parser, query)
314 node: TreeSitterNode | None = utils.find_associated_scope(
315 comments[0], CommentType.yaml
316 )
317 assert node
318 assert node.text
319 yaml_structure = node.text.decode("utf-8")
320 assert result in yaml_structure
321
322
323@pytest.mark.parametrize(
324 ("code", "result"),
325 [
326 (
327 b"""
328 // @req-id: need_001
329 fn dummy_func1() {
330 }
331 """,
332 "fn dummy_func1()",
333 ),
334 (
335 b"""
336 fn dummy_func2() {
337 }
338 // @req-id: need_001
339 fn dummy_func1() {
340 }
341 """,
342 "fn dummy_func1()",
343 ),
344 (
345 b"""
346 fn dummy_func1() {
347 let a = 1;
348 /* @req-id: need_001 */
349 }
350 """,
351 "fn dummy_func1()",
352 ),
353 (
354 b"""
355 fn dummy_func1() {
356 // @req-id: need_001
357 let a = 1;
358 }
359 fn dummy_func2() {
360 }
361 """,
362 "fn dummy_func1()",
363 ),
364 (
365 b"""
366 /// @req-id: need_001
367 fn dummy_func1() {
368 }
369 """,
370 "fn dummy_func1()",
371 ),
372 (
373 b"""
374 struct MyStruct {
375 // @req-id: need_001
376 field: i32,
377 }
378 """,
379 "struct MyStruct",
380 ),
381 ],
382)
383def test_find_associated_scope_rust(code, result, init_rust_tree_sitter):
384 parser, query = init_rust_tree_sitter
385 comments = utils.extract_comments(code, parser, query)
386 node: TreeSitterNode | None = utils.find_associated_scope(
387 comments[0], CommentType.rust
388 )
389 assert node
390 assert node.text
391 rust_def = node.text.decode("utf-8")
392 assert result in rust_def
393
394
395@pytest.mark.parametrize(
396 ("code", "result"),
397 [
398 # leading comment is associated with the following key/value pair
399 (
400 b'{\n // @req-id: need_001\n "alpha": 1\n}\n',
401 '"alpha": 1',
402 ),
403 # inline comment is associated with the array item on the same row
404 (
405 b'{\n "items": [\n "first", // @req-id: need_001\n "second"\n ]\n}\n',
406 '"first"',
407 ),
408 # inline comment is associated with the pair on the same row
409 (
410 b'{\n "alpha": 1, // @req-id: need_001\n "beta": 2\n}\n',
411 '"alpha": 1',
412 ),
413 # block comment is associated with the following pair
414 (
415 b'{\n /* @req-id: need_001 */\n "beta": 2\n}\n',
416 '"beta": 2',
417 ),
418 # trailing comment falls back to the enclosing object
419 (
420 b'{\n "alpha": 1\n // @req-id: need_001\n}\n',
421 '"alpha"',
422 ),
423 ],
424)
425def test_find_associated_scope_jsonc(code, result, init_jsonc_tree_sitter):
426 parser, query = init_jsonc_tree_sitter
427 comments = utils.extract_comments(code, parser, query)
428 node: TreeSitterNode | None = utils.find_associated_scope(
429 comments[0], CommentType.jsonc
430 )
431 assert node
432 assert node.text
433 jsonc_structure = node.text.decode("utf-8")
434 assert result in jsonc_structure
435
436
437@pytest.mark.parametrize(
438 ("code", "result"),
439 [
440 # comment above a POSIX-style function definition
441 (
442 b"""
443 # @req-id: need_001
444 greet() {
445 echo hi
446 }
447 """,
448 "greet()",
449 ),
450 # comment above the `function` keyword form
451 (
452 b"""
453 # @req-id: need_002
454 function greet {
455 echo hi
456 }
457 """,
458 "function greet",
459 ),
460 # comment inside a function body falls back to the enclosing function
461 (
462 b"""
463 greet() {
464 # @req-id: need_003
465 echo hi
466 }
467 """,
468 "greet()",
469 ),
470 ],
471)
472def test_find_associated_scope_bash(code, result, init_bash_tree_sitter):
473 parser, query = init_bash_tree_sitter
474 comments = utils.extract_comments(code, parser, query)
475 node: TreeSitterNode | None = utils.find_associated_scope(
476 comments[0], CommentType.bash
477 )
478 assert node
479 assert node.text
480 func_def = node.text.decode("utf-8")
481 assert func_def.startswith(result)
482
483
484@pytest.mark.parametrize(
485 ("code", "result"),
486 [
487 (
488 b"""
489 def dummy_func1():
490 # @req-id: need_001
491 pass
492 """,
493 "def dummy_func1()",
494 ),
495 (
496 b"""
497 def dummy_func1():
498 '''@req-id: need_001'''
499 pass
500 """,
501 "def dummy_func1()",
502 ),
503 (
504 b"""
505 def dummy_func1():
506 def nested_dummy_func1():
507 '''@req-id: need_001'''
508 pass
509 pass
510 """,
511 "def nested_dummy_func1()",
512 ),
513 (
514 b"""
515 def dummy_func1():
516 '''@req-id: need_001'''
517 def nested_dummy_func1():
518 pass
519 pass
520 """,
521 "def dummy_func1()",
522 ),
523 ],
524)
525def test_find_enclosing_scope_python(code, result, init_python_tree_sitter):
526 parser, query = init_python_tree_sitter
527 comments = utils.extract_comments(code, parser, query)
528 node: TreeSitterNode | None = utils.find_enclosing_scope(
529 comments[0], CommentType.python
530 )
531 assert node
532 assert node.text
533 func_def = node.text.decode("utf-8")
534 assert result in func_def
535
536
537@pytest.mark.parametrize(
538 ("code", "result"),
539 [
540 (
541 b"""
542 # @req-id: need_001
543 def dummy_func1():
544 pass
545 """,
546 "def dummy_func1()",
547 ),
548 (
549 b"""
550 # @req-id: need_001
551 # @req-id: need_002
552 def dummy_func1():
553 pass
554 """,
555 "def dummy_func1()",
556 ),
557 ],
558)
559def test_find_next_scope_python(code, result, init_python_tree_sitter):
560 parser, query = init_python_tree_sitter
561 comments = utils.extract_comments(code, parser, query)
562 node: TreeSitterNode | None = utils.find_next_scope(comments[0], CommentType.python)
563 assert node
564 assert node.text
565 func_def = node.text.decode("utf-8")
566 assert result in func_def
567
568
569@pytest.mark.parametrize(
570 ("code", "result"),
571 [
572 (
573 b"""
574 // @req-id: need_001
575 void dummy_func1(){
576 }
577 """,
578 "void dummy_func1()",
579 ),
580 (
581 b"""
582 /* @req-id: need_001 */
583 void dummy_func1(){
584 }
585 """,
586 "void dummy_func1()",
587 ),
588 ],
589)
590def test_find_next_scope_cpp(code, result, init_cpp_tree_sitter):
591 parser, query = init_cpp_tree_sitter
592 comments = utils.extract_comments(code, parser, query)
593 node: TreeSitterNode | None = utils.find_next_scope(comments[0], CommentType.cpp)
594 assert node
595 assert node.text
596 func_def = node.text.decode("utf-8")
597 assert result in func_def
598
599
600@pytest.mark.parametrize(
601 ("code", "result"),
602 [
603 (
604 b"""
605 // @req-id: need_001
606 public class DummyClass1
607 {
608 }
609 """,
610 "public class DummyClass1",
611 ),
612 (
613 b"""
614
615 public class DummyClass1
616 {
617 /* @req-id: need_001 */
618 /* @req-id: need_002 */
619 public void DummyFunc1()
620 {
621 }
622 }
623 """,
624 "public void DummyFunc1",
625 ),
626 ],
627)
628def test_find_next_scope_csharp(code, result, init_csharp_tree_sitter):
629 parser, query = init_csharp_tree_sitter
630 comments = utils.extract_comments(code, parser, query)
631 node: TreeSitterNode | None = utils.find_next_scope(comments[0], CommentType.cs)
632 assert node
633 assert node.text
634 func_def = node.text.decode("utf-8")
635 assert result in func_def
636
637
638@pytest.mark.parametrize(
639 ("code", "result"),
640 [
641 (
642 b"""
643 void dummy_func1(){
644 // @req-id: need_001
645 }
646 """,
647 "void dummy_func1()",
648 ),
649 (
650 b"""
651 void dummy_func1(){
652 /* @req-id: need_001 */
653 }
654 """,
655 "void dummy_func1()",
656 ),
657 ],
658)
659def test_find_enclosing_scope_cpp(code, result, init_cpp_tree_sitter):
660 parser, query = init_cpp_tree_sitter
661 comments = utils.extract_comments(code, parser, query)
662 node: TreeSitterNode | None = utils.find_enclosing_scope(
663 comments[0], CommentType.cpp
664 )
665 assert node
666 assert node.text
667 func_def = node.text.decode("utf-8")
668 assert result in func_def
669
670
671@pytest.mark.parametrize(
672 ("code", "result"),
673 [
674 (
675 b"""
676 public class DummyClass1
677 {
678 // @req-id: need_001
679 }
680 """,
681 "public class DummyClass1",
682 ),
683 (
684 b"""
685 public class DummyClass1
686 {
687 public void DummyFunc1()
688 {
689 /* @req-id: need_001 */
690 }
691 }
692 """,
693 "public void DummyFunc1()",
694 ),
695 (
696 b"""
697 public class DummyClass1
698 {
699 public string DummyProperty1
700 {
701 get
702 {
703 /* @req-id: need_001 */
704 return "dummy";
705 }
706 }
707 }
708 """,
709 "public string DummyProperty1",
710 ),
711 ],
712)
713def test_find_enclosing_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_enclosing_scope(
717 comments[0], CommentType.cs
718 )
719 assert node
720 assert node.text
721 func_def = node.text.decode("utf-8")
722 assert result in func_def
723
724
725@pytest.mark.parametrize(
726 ("code", "num_comments", "result"),
727 [
728 (
729 b"""
730 // @req-id: need_001
731 void dummy_func1(){
732 }
733 """,
734 1,
735 "// @req-id: need_001",
736 ),
737 (
738 b"""
739 void dummy_func1(){
740 // @req-id: need_001
741 }
742 """,
743 1,
744 "// @req-id: need_001",
745 ),
746 (
747 b"""
748 /* @req-id: need_001 */
749 void dummy_func1(){
750 }
751 """,
752 1,
753 "/* @req-id: need_001 */",
754 ),
755 (
756 b"""
757 // @req-id: need_001
758 //
759 //
760 void dummy_func1(){
761 }
762 """,
763 3,
764 "// @req-id: need_001",
765 ),
766 ],
767)
768def test_cpp_comment(code, num_comments, result, init_cpp_tree_sitter):
769 parser, query = init_cpp_tree_sitter
770 comments = utils.extract_comments(code, parser, query)
771 assert len(comments) == num_comments
772 comments.sort(key=lambda x: x.start_point.row)
773 assert comments[0].text
774 assert comments[0].text.decode("utf-8") == result
775
776
777@pytest.mark.parametrize(
778 ("code", "num_comments", "result"),
779 [
780 (
781 b"""
782 # @req-id: need_001
783 def dummy_func1():
784 pass
785 """,
786 1,
787 "# @req-id: need_001",
788 ),
789 (
790 b"""
791 def dummy_func1():
792 # @req-id: need_001
793 pass
794 """,
795 1,
796 "# @req-id: need_001",
797 ),
798 (
799 b"""
800 # single line comment
801 # @req-id: need_001
802 def dummy_func1():
803 pass
804 """,
805 2,
806 "# single line comment",
807 ),
808 (
809 b"""
810 def dummy_func1():
811 '''
812 @req-id: need_001
813 '''
814 pass
815 """,
816 1,
817 "'''\n @req-id: need_001\n '''",
818 ),
819 (
820 b"""
821 def dummy_func1():
822 text = '''@req-id: need_001, need_002, this docstring shall not be extracted as comment'''
823 # @req-id: need_001
824 pass
825 """,
826 1,
827 "# @req-id: need_001",
828 ),
829 ],
830)
831def test_python_comment(code, num_comments, result, init_python_tree_sitter):
832 parser, query = init_python_tree_sitter
833 comments: list[TreeSitterNode] = utils.extract_comments(code, parser, query)
834 comments.sort(key=lambda x: x.start_point.row)
835 assert len(comments) == num_comments
836 assert comments[0].text
837 assert comments[0].text.decode("utf-8") == result
838
839
840@pytest.mark.parametrize(
841 ("code", "num_comments", "result"),
842 [
843 (
844 b"""
845 // @req-id: need_001
846 void DummyFunc1(){
847 }
848 """,
849 1,
850 "// @req-id: need_001",
851 ),
852 (
853 b"""
854 void DummyFunc1(){
855 // @req-id: need_001
856 }
857 """,
858 1,
859 "// @req-id: need_001",
860 ),
861 (
862 b"""
863 /* @req-id: need_001 */
864 void DummyFunc1(){
865 }
866 """,
867 1,
868 "/* @req-id: need_001 */",
869 ),
870 (
871 b"""
872 // @req-id: need_001
873 //
874 //
875 void DummyFunc1(){
876 }
877 """,
878 3,
879 "// @req-id: need_001",
880 ),
881 ],
882)
883def test_csharp_comment(code, num_comments, result, init_csharp_tree_sitter):
884 parser, query = init_csharp_tree_sitter
885 comments: list[TreeSitterNode] = utils.extract_comments(code, parser, query)
886 comments.sort(key=lambda x: x.start_point.row)
887 assert len(comments) == num_comments
888 assert comments[0].text
889 assert comments[0].text.decode("utf-8") == result
890
891
892@pytest.mark.parametrize(
893 ("code", "num_comments", "result"),
894 [
895 (
896 b"""
897 # @req-id: need_001
898 database:
899 host: localhost
900 """,
901 1,
902 "# @req-id: need_001",
903 ),
904 (
905 b"""
906 services:
907 web:
908 # @req-id: need_001
909 image: nginx:latest
910 """,
911 1,
912 "# @req-id: need_001",
913 ),
914 (
915 b"""
916 # Top level comment
917 # @req-id: need_001
918 version: "3.8"
919 """,
920 2,
921 "# Top level comment",
922 ),
923 ],
924)
925def test_yaml_comment(code, num_comments, result, init_yaml_tree_sitter):
926 parser, query = init_yaml_tree_sitter
927 comments: list[TreeSitterNode] = utils.extract_comments(code, parser, query)
928 comments.sort(key=lambda x: x.start_point.row)
929 assert len(comments) == num_comments
930 assert comments[0].text
931 assert comments[0].text.decode("utf-8") == result
932
933
934@pytest.mark.parametrize(
935 ("code", "num_comments", "result"),
936 [
937 (
938 b"""
939 // @req-id: need_001
940 func dummyFunc1() {
941 }
942 """,
943 1,
944 "// @req-id: need_001",
945 ),
946 (
947 b"""
948 func dummyFunc1() {
949 // @req-id: need_001
950 }
951 """,
952 1,
953 "// @req-id: need_001",
954 ),
955 (
956 b"""
957 /* @req-id: need_001 */
958 func dummyFunc1() {
959 }
960 """,
961 1,
962 "/* @req-id: need_001 */",
963 ),
964 (
965 b"""
966 // @req-id: need_001
967 //
968 //
969 func dummyFunc1() {
970 }
971 """,
972 3,
973 "// @req-id: need_001",
974 ),
975 ],
976)
977def test_go_comment(code, num_comments, result, init_go_tree_sitter):
978 parser, query = init_go_tree_sitter
979 comments: list[TreeSitterNode] = utils.extract_comments(code, parser, query)
980 comments.sort(key=lambda x: x.start_point.row)
981 assert len(comments) == num_comments
982 assert comments[0].text
983 assert comments[0].text.decode("utf-8") == result
984
985
986@pytest.mark.parametrize(
987 ("code", "result"),
988 [
989 (
990 b"""
991 // @req-id: need_001
992 func dummyFunc1() {
993 }
994 """,
995 "func dummyFunc1()",
996 ),
997 (
998 b"""
999 func dummyFunc2() {
1000 }
1001 // @req-id: need_001
1002 func dummyFunc1() {
1003 }
1004 """,
1005 "func dummyFunc1()",
1006 ),
1007 (
1008 b"""
1009 /* @req-id: need_001 */
1010 func dummyFunc1() {
1011 }
1012 """,
1013 "func dummyFunc1()",
1014 ),
1015 ],
1016)
1017def test_find_associated_scope_go(code, result, init_go_tree_sitter):
1018 parser, query = init_go_tree_sitter
1019 comments = utils.extract_comments(code, parser, query)
1020 node: TreeSitterNode | None = utils.find_associated_scope(
1021 comments[0], CommentType.go
1022 )
1023 assert node
1024 assert node.text
1025 go_def = node.text.decode("utf-8")
1026 assert result in go_def
1027
1028
1029@pytest.mark.parametrize(
1030 ("code", "result"),
1031 [
1032 (
1033 b"""
1034 // @req-id: need_001
1035 func dummyFunc1() {
1036 }
1037 """,
1038 "func dummyFunc1()",
1039 ),
1040 (
1041 b"""
1042 // @req-id: need_001
1043 type DummyStruct struct {
1044 Field int
1045 }
1046 """,
1047 "type DummyStruct struct",
1048 ),
1049 ],
1050)
1051def test_find_next_scope_go(code, result, init_go_tree_sitter):
1052 parser, query = init_go_tree_sitter
1053 comments = utils.extract_comments(code, parser, query)
1054 node: TreeSitterNode | None = utils.find_next_scope(comments[0], CommentType.go)
1055 assert node
1056 assert node.text
1057 go_def = node.text.decode("utf-8")
1058 assert result in go_def
1059
1060
1061@pytest.mark.parametrize(
1062 ("code", "result"),
1063 [
1064 (
1065 b"""
1066 func dummyFunc1() {
1067 // @req-id: need_001
1068 }
1069 """,
1070 "func dummyFunc1()",
1071 ),
1072 (
1073 b"""
1074 func dummyFunc1() {
1075 /* @req-id: need_001 */
1076 }
1077 """,
1078 "func dummyFunc1()",
1079 ),
1080 ],
1081)
1082def test_find_enclosing_scope_go(code, result, init_go_tree_sitter):
1083 parser, query = init_go_tree_sitter
1084 comments = utils.extract_comments(code, parser, query)
1085 node: TreeSitterNode | None = utils.find_enclosing_scope(
1086 comments[0], CommentType.go
1087 )
1088 assert node
1089 assert node.text
1090 go_def = node.text.decode("utf-8")
1091 assert result in go_def
1092
1093
1094@pytest.mark.parametrize(
1095 ("git_url", "rev", "project_path", "filepath", "lineno", "result"),
1096 [
1097 (
1098 "git@github.com:useblocks/sphinx-codelinks.git",
1099 "beef1234",
1100 Path(__file__).parent.parent,
1101 Path("example") / "to" / "here",
1102 3,
1103 "https://github.com/useblocks/sphinx-codelinks/blob/beef1234/example/to/here#L3",
1104 )
1105 ],
1106)
1107def test_form_https_url(git_url, rev, project_path, filepath, lineno, result): # noqa: PLR0913 # need to have these args
1108 url = utils.form_https_url(git_url, rev, project_path, filepath, lineno=lineno)
1109 assert url == result
1110
1111
1112def get_git_path() -> str:
1113 """Get the path to the git executable."""
1114 git_path = shutil.which("git")
1115 if not git_path:
1116 raise FileNotFoundError("Git executable not found")
1117 if not Path(git_path).is_file():
1118 raise FileNotFoundError("Git executable path is invalid")
1119 return git_path
1120
1121
1122def init_git_repo(repo_path: Path, remote_url: str) -> Path:
1123 """Initialize a git repository for testing."""
1124 git_dir = repo_path / "test_repo"
1125 src_dir = git_dir / "src"
1126 src_dir.mkdir(parents=True)
1127
1128 git_path = get_git_path()
1129 if not git_path:
1130 raise FileNotFoundError("Git executable not found")
1131 if not Path(git_path).is_file():
1132 raise FileNotFoundError("Git executable path is invalid")
1133
1134 # Initialize git repo
1135 subprocess.run([git_path, "init"], cwd=git_dir, check=True, capture_output=True) # noqa: S603
1136 subprocess.run( # noqa: S603
1137 [git_path, "config", "user.email", "test@example.com"], cwd=git_dir, check=True
1138 )
1139 subprocess.run( # noqa: S603
1140 [git_path, "config", "user.name", "Test User"], cwd=git_dir, check=True
1141 )
1142
1143 # Create a test file and commit
1144 test_file = src_dir / "test_file.py"
1145 test_file.write_text("# Test file\nprint('hello')\n")
1146 subprocess.run([git_path, "add", "."], cwd=git_dir, check=True) # noqa: S603
1147 subprocess.run( # noqa: S603
1148 [git_path, "commit", "-m", "Initial commit"], cwd=git_dir, check=True
1149 )
1150
1151 # Add a remote
1152 subprocess.run( # noqa: S603
1153 [git_path, "remote", "add", "origin", remote_url],
1154 cwd=git_dir,
1155 check=True,
1156 )
1157
1158 return git_dir
1159
1160
1161@pytest.fixture(
1162 params=[
1163 ("test_repo_git", "git@github.com:test-user/test-repo.git"),
1164 ("test_repo_https", "https://github.com/test-user/test-repo.git"),
1165 ]
1166)
1167def git_repo(tmp_path: str, request: pytest.FixtureRequest) -> tuple[Path, str]:
1168 """Create git repos for testing."""
1169 repo_name, remote_url = request.param
1170 repo_path = Path(tmp_path) / repo_name
1171 repo_path = init_git_repo(repo_path, remote_url)
1172 return repo_path, remote_url
1173
1174
1175def get_current_commit_hash(git_dir: Path) -> str:
1176 """Get the current commit hash of the git repository."""
1177 git_path = get_git_path()
1178 result = subprocess.run( # noqa: S603
1179 [git_path, "rev-parse", "HEAD"],
1180 cwd=git_dir,
1181 check=True,
1182 capture_output=True,
1183 text=True,
1184 )
1185
1186 return str(result.stdout.strip())
1187
1188
1189def test_locate_git_root(git_repo: tuple[Path, str]) -> None:
1190 repo_path = git_repo[0]
1191 src_dir = repo_path / "src"
1192 git_root = utils.locate_git_root(src_dir)
1193 assert git_root == repo_path
1194
1195
1196def test_get_remote_url(git_repo: tuple[Path, str]) -> None:
1197 repo_path, expected_url = git_repo
1198 remote_url = utils.get_remote_url(repo_path)
1199 assert remote_url == expected_url
1200
1201
1202def test_get_current_rev(git_repo: tuple[Path, str]) -> None:
1203 repo_path, _ = git_repo
1204 current_rev = get_current_commit_hash(repo_path)
1205 assert current_rev == utils.get_current_rev(repo_path)
1206
1207
1208def test_get_current_rev_detached_head(tmp_path: Path) -> None:
1209 """In a detached HEAD (e.g. CI checkouts) .git/HEAD holds the commit SHA
1210 directly; get_current_rev returns it rather than warning and giving up."""
1211 git_root = tmp_path / "repo"
1212 (git_root / ".git").mkdir(parents=True)
1213 sha = "a1b2c3d4e5f60718293a4b5c6d7e8f9012345678"
1214 (git_root / ".git" / "HEAD").write_text(f"{sha}\n")
1215
1216 assert utils.get_current_rev(git_root) == sha
1217
1218
1219@pytest.mark.parametrize(
1220 ("text", "leading_sequences", "result"),
1221 [
1222 (
1223 """
1224* some text in a comment
1225* some text in a comment
1226*
1227""",
1228 ["*"],
1229 """
1230 some text in a comment
1231 some text in a comment
1232
1233""",
1234 ),
1235 ],
1236)
1237def test_remove_leading_sequences(text, leading_sequences, result):
1238 clean_text = utils.remove_leading_sequences(text, leading_sequences)
1239 assert clean_text == result
1240
1241
1242@pytest.mark.parametrize(
1243 ("text", "rst_markers", "rst_text", "positions"),
1244 [
1245 (
1246 """
1247@rst
1248.. impl:: multiline rst text
1249 :id: IMPL_71
1250@endrst
1251""",
1252 ["@rst", "@endrst"],
1253 f""".. impl:: multiline rst text{UNIX_NEWLINE} :id: IMPL_71{UNIX_NEWLINE}""",
1254 {"row_offset": 1, "start_idx": 6, "end_idx": 51},
1255 ),
1256 (
1257 """
1258@rst.. impl:: oneline rst text@endrst
1259""",
1260 ["@rst", "@endrst"],
1261 """.. impl:: oneline rst text""",
1262 {"row_offset": 0, "start_idx": 5, "end_idx": 31},
1263 ),
1264 ],
1265)
1266def test_extract_rst(text, rst_markers, rst_text, positions):
1267 extracted_rst = utils.extract_rst(text, rst_markers[0], rst_markers[1])
1268 assert extracted_rst is not None
1269 assert extracted_rst["rst_text"] == rst_text
1270 assert extracted_rst["start_idx"] == positions["start_idx"]
1271 assert extracted_rst["end_idx"] == positions["end_idx"]
1272
1273
1274# ========== YAML-specific tests ==========
1275
1276
1277@pytest.mark.parametrize(
1278 ("code", "expected_structure"),
1279 [
1280 # Basic key-value pair
1281 (
1282 b"""
1283 # Comment before key
1284 database:
1285 host: localhost
1286 """,
1287 "database:",
1288 ),
1289 # Comment in nested structure
1290 (
1291 b"""
1292 services:
1293 web:
1294 # Comment before image
1295 image: nginx:latest
1296 """,
1297 "image: nginx:latest",
1298 ),
1299 # Comment before list item
1300 (
1301 b"""
1302 items:
1303 # Comment before list item
1304 - name: item1
1305 value: test
1306 """,
1307 "- name: item1",
1308 ),
1309 # Comment in document structure
1310 (
1311 b"""---
1312# Comment in document
1313version: "3.8"
1314services:
1315 app:
1316 build: .
1317 """,
1318 "version:",
1319 ),
1320 # Flow mapping structure
1321 (
1322 b"""
1323 # Comment before flow mapping
1324 config: {debug: true, port: 8080}
1325 """,
1326 "config:",
1327 ),
1328 ],
1329)
1330def test_find_yaml_next_structure(code, expected_structure, init_yaml_tree_sitter):
1331 """Test the find_yaml_next_structure function."""
1332 parser, query = init_yaml_tree_sitter
1333 comments = utils.extract_comments(code, parser, query)
1334 assert comments, "No comments found in the code"
1335
1336 next_structure = utils.find_yaml_next_structure(comments[0])
1337 assert next_structure, "No next structure found"
1338 structure_text = next_structure.text.decode("utf-8")
1339 assert expected_structure in structure_text
1340
1341
1342@pytest.mark.parametrize(
1343 ("code", "expected_structure"),
1344 [
1345 # Comment associated with key-value pair
1346 (
1347 b"""
1348 # Database configuration
1349 database:
1350 host: localhost
1351 port: 5432
1352 """,
1353 "database:",
1354 ),
1355 # Comment associated with nested structure
1356 (
1357 b"""
1358 services:
1359 web:
1360 # Web service image
1361 image: nginx:latest
1362 ports:
1363 - "80:80"
1364 """,
1365 "image: nginx:latest",
1366 ),
1367 # Comment associated with list item
1368 (
1369 b"""
1370 dependencies:
1371 # First dependency
1372 - name: redis
1373 version: "6.0"
1374 - name: postgres
1375 version: "13"
1376 """,
1377 "- name: redis",
1378 ),
1379 # Comment inside parent structure
1380 (
1381 b"""
1382 app:
1383 # Internal comment
1384 name: myapp
1385 version: "1.0"
1386 """,
1387 "name: myapp",
1388 ),
1389 ],
1390)
1391def test_find_yaml_associated_structure(
1392 code, expected_structure, init_yaml_tree_sitter
1393):
1394 """Test the find_yaml_associated_structure function."""
1395 parser, query = init_yaml_tree_sitter
1396 comments = utils.extract_comments(code, parser, query)
1397 assert comments, "No comments found in the code"
1398
1399 associated_structure = utils.find_yaml_associated_structure(comments[0])
1400 assert associated_structure, "No associated structure found"
1401 structure_text = associated_structure.text.decode("utf-8")
1402 assert expected_structure in structure_text
1403
1404
1405@pytest.mark.parametrize(
1406 ("code", "expected_results"),
1407 [
1408 # Multiple comments in sequence
1409 (
1410 b"""
1411 # First comment
1412 # Second comment
1413 database:
1414 host: localhost
1415 """,
1416 ["database:", "database:"], # Both comments should associate with database
1417 ),
1418 # Comments at different nesting levels
1419 (
1420 b"""
1421 # Top level comment
1422 services:
1423 web:
1424 # Nested comment
1425 image: nginx:latest
1426 """,
1427 ["services:", "image: nginx:latest"],
1428 ),
1429 ],
1430)
1431def test_multiple_yaml_comments(code, expected_results, init_yaml_tree_sitter):
1432 """Test handling of multiple YAML comments in the same file."""
1433 parser, query = init_yaml_tree_sitter
1434 comments = utils.extract_comments(code, parser, query)
1435 comments.sort(key=lambda x: x.start_point.row)
1436
1437 assert len(comments) == len(expected_results), (
1438 f"Expected {len(expected_results)} comments, found {len(comments)}"
1439 )
1440
1441 for i, comment in enumerate(comments):
1442 associated_structure = utils.find_yaml_associated_structure(comment)
1443 assert associated_structure, f"No associated structure found for comment {i}"
1444 structure_text = associated_structure.text.decode("utf-8")
1445 assert expected_results[i] in structure_text
1446
1447
1448@pytest.mark.parametrize(
1449 ("code", "has_structure"),
1450 [
1451 # Comment at end of file with no following structure
1452 (
1453 b"""
1454database:
1455 host: localhost
1456# End of file comment
1457 """,
1458 True, # This will actually find the parent database structure
1459 ),
1460 # Comment with only whitespace after
1461 (
1462 b"""
1463 # Lonely comment
1464
1465
1466 """,
1467 False,
1468 ),
1469 # Comment before valid structure
1470 (
1471 b"""
1472 # Valid comment
1473 key: value
1474 """,
1475 True,
1476 ),
1477 ],
1478)
1479def test_yaml_edge_cases(code, has_structure, init_yaml_tree_sitter):
1480 """Test edge cases in YAML comment processing."""
1481 parser, query = init_yaml_tree_sitter
1482 comments = utils.extract_comments(code, parser, query)
1483
1484 if comments:
1485 structure = utils.find_yaml_associated_structure(comments[0])
1486 if has_structure:
1487 assert structure, "Expected to find associated structure"
1488 else:
1489 assert structure is None, "Expected no associated structure"
1490 else:
1491 assert not has_structure, "No comments found but structure was expected"
1492
1493
1494@pytest.mark.parametrize(
1495 ("code", "expected_structures"),
1496 [
1497 # Simpler nested YAML structure
1498 (
1499 b"""# Global configuration
1500version: "3.8"
1501
1502# Services section
1503services:
1504 web:
1505 image: nginx:latest
1506 # Port configuration
1507 ports:
1508 - "80:80"
1509 """,
1510 [
1511 "version:", # Global configuration
1512 "services:", # Services section
1513 '- "80:80"', # Port configuration
1514 ],
1515 ),
1516 ],
1517)
1518def test_complex_yaml_structure(code, expected_structures, init_yaml_tree_sitter):
1519 """Test complex nested YAML structures with multiple comments."""
1520 parser, query = init_yaml_tree_sitter
1521 comments = utils.extract_comments(code, parser, query)
1522 comments.sort(key=lambda x: x.start_point.row)
1523
1524 assert len(comments) == len(expected_structures), (
1525 f"Expected {len(expected_structures)} comments, found {len(comments)}"
1526 )
1527
1528 for i, comment in enumerate(comments):
1529 associated_structure = utils.find_yaml_associated_structure(comment)
1530 assert associated_structure, f"No associated structure found for comment {i}"
1531 structure_text = associated_structure.text.decode("utf-8")
1532 assert expected_structures[i] in structure_text, (
1533 f"Expected '{expected_structures[i]}' in structure text: '{structure_text}'"
1534 )
1535
1536
1537@pytest.mark.parametrize(
1538 ("code", "expected_type"),
1539 [
1540 # Block mapping pair
1541 (
1542 b"""
1543 # Comment
1544 key: value
1545 """,
1546 "block_mapping_pair",
1547 ),
1548 # Block sequence item
1549 (
1550 b"""
1551 items:
1552 # Comment
1553 - item1
1554 """,
1555 "block_sequence_item",
1556 ),
1557 # Nested block mapping
1558 (
1559 b"""
1560 services:
1561 # Comment
1562 web:
1563 image: nginx
1564 """,
1565 "block_mapping_pair",
1566 ),
1567 ],
1568)
1569def test_yaml_structure_types(code, expected_type, init_yaml_tree_sitter):
1570 """Test that YAML structures return the correct node types."""
1571 parser, query = init_yaml_tree_sitter
1572 comments = utils.extract_comments(code, parser, query)
1573 assert comments, "No comments found"
1574
1575 structure = utils.find_yaml_associated_structure(comments[0])
1576 assert structure, "No associated structure found"
1577 assert structure.type == expected_type, (
1578 f"Expected type {expected_type}, got {structure.type}"
1579 )
1580
1581
1582def test_yaml_document_structure(init_yaml_tree_sitter):
1583 """Test YAML document structure handling."""
1584 code = b"""---
1585# Document comment
1586apiVersion: v1
1587kind: ConfigMap
1588metadata:
1589 name: my-config
1590data:
1591 # Data comment
1592 config.yml: |
1593 setting: value
1594 """
1595
1596 parser, query = init_yaml_tree_sitter
1597 comments = utils.extract_comments(code, parser, query)
1598 comments.sort(key=lambda x: x.start_point.row)
1599
1600 # Should find both comments
1601 assert len(comments) >= 2, f"Expected at least 2 comments, found {len(comments)}"
1602
1603 # First comment should associate with apiVersion
1604 first_structure = utils.find_yaml_associated_structure(comments[0])
1605 assert first_structure, "No structure found for first comment"
1606 first_text = first_structure.text.decode("utf-8")
1607 assert "apiVersion:" in first_text
1608
1609 # Second comment should associate with config.yml
1610 second_structure = utils.find_yaml_associated_structure(comments[1])
1611 assert second_structure, "No structure found for second comment"
1612 second_text = second_structure.text.decode("utf-8")
1613 assert "config.yml:" in second_text
1614
1615
1616def test_yaml_inline_comments_current_behavior(init_yaml_tree_sitter):
1617 """Test improved behavior of inline comments in YAML after the fix."""
1618 code = b"""key1: value1 # inline comment about key1
1619key2: value2
1620key3: value3 # inline comment about key3
1621"""
1622
1623 parser, query = init_yaml_tree_sitter
1624 comments = utils.extract_comments(code, parser, query)
1625 comments.sort(key=lambda x: x.start_point.row)
1626
1627 assert len(comments) == 2, f"Expected 2 comments, found {len(comments)}"
1628
1629 # Fixed behavior: inline comment about key1 now correctly associates with key1
1630 first_structure = utils.find_yaml_associated_structure(comments[0])
1631 assert first_structure, "No structure found for first comment"
1632 first_text = first_structure.text.decode("utf-8")
1633 assert "key1:" in first_text, f"Expected 'key1:' in '{first_text}'"
1634
1635 # Fixed behavior: inline comment about key3 now correctly associates with key3
1636 second_structure = utils.find_yaml_associated_structure(comments[1])
1637 assert second_structure, "No structure found for second comment"
1638 second_text = second_structure.text.decode("utf-8")
1639 assert "key3:" in second_text, f"Expected 'key3:' in '{second_text}'"
1640
1641
1642@pytest.mark.parametrize(
1643 ("code", "expected_associations"),
1644 [
1645 # Basic inline comment case
1646 (
1647 b"""key1: value1 # comment about key1
1648key2: value2
1649 """,
1650 ["key1:"], # Now correctly associates with key1
1651 ),
1652 # Multiple inline comments
1653 (
1654 b"""database:
1655 host: localhost # production server
1656 port: 5432 # default postgres port
1657 user: admin
1658 """,
1659 [
1660 "host: localhost",
1661 "port: 5432",
1662 ], # Now correctly associates with the right structures
1663 ),
1664 ],
1665)
1666def test_yaml_inline_comments_fixed_behavior(
1667 code, expected_associations, init_yaml_tree_sitter
1668):
1669 """Test that inline comments now correctly associate with the structure they comment on."""
1670 parser, query = init_yaml_tree_sitter
1671 comments = utils.extract_comments(code, parser, query)
1672 comments.sort(key=lambda x: x.start_point.row)
1673
1674 assert len(comments) == len(expected_associations), (
1675 f"Expected {len(expected_associations)} comments, found {len(comments)}"
1676 )
1677
1678 for i, comment in enumerate(comments):
1679 structure = utils.find_yaml_associated_structure(comment)
1680 assert structure, f"No structure found for comment {i}"
1681 structure_text = structure.text.decode("utf-8")
1682 assert expected_associations[i] in structure_text, (
1683 f"Expected '{expected_associations[i]}' in structure text: '{structure_text}'"
1684 )
1685
1686
1687@pytest.mark.parametrize(
1688 ("code", "expected_associations"),
1689 [
1690 # Inline comments with list items
1691 (
1692 b"""items:
1693 - name: item1 # first item
1694 - name: item2 # second item
1695 """,
1696 [
1697 "name: item1",
1698 "name: item2",
1699 ], # The inline comment finds the key-value pair within the list item
1700 ),
1701 # Mixed inline and block comments
1702 (
1703 b"""# Block comment for database
1704database:
1705 host: localhost # inline comment for host
1706 port: 5432
1707 # Block comment for user
1708 user: admin
1709 """,
1710 ["database:", "host: localhost", "user: admin"],
1711 ),
1712 # Inline comments in nested structures
1713 (
1714 b"""services:
1715 web:
1716 image: nginx # web server image
1717 ports:
1718 - "80:80" # http port
1719 """,
1720 ["image: nginx", '- "80:80"'],
1721 ),
1722 ],
1723)
1724def test_yaml_inline_comments_comprehensive(
1725 code, expected_associations, init_yaml_tree_sitter
1726):
1727 """Comprehensive test for inline comment behavior in various YAML structures."""
1728 parser, query = init_yaml_tree_sitter
1729 comments = utils.extract_comments(code, parser, query)
1730 comments.sort(key=lambda x: x.start_point.row)
1731
1732 assert len(comments) == len(expected_associations), (
1733 f"Expected {len(expected_associations)} comments, found {len(comments)}"
1734 )
1735
1736 for i, comment in enumerate(comments):
1737 structure = utils.find_yaml_associated_structure(comment)
1738 assert structure, (
1739 f"No structure found for comment {i}: '{comment.text.decode('utf-8')}'"
1740 )
1741 structure_text = structure.text.decode("utf-8")
1742 assert expected_associations[i] in structure_text, (
1743 f"Comment {i} '{comment.text.decode('utf-8')}' -> Expected '{expected_associations[i]}' in '{structure_text}'"
1744 )