[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]
2import shutil
3import subprocess
4from pathlib import Path
5
6import pytest
7import tree_sitter_bash
8import tree_sitter_c_sharp
9import tree_sitter_cpp
10import tree_sitter_go
11import tree_sitter_json
12import tree_sitter_python
13import tree_sitter_rust
14import tree_sitter_yaml
15from tree_sitter import Language, Parser, Query
16from tree_sitter import Node as TreeSitterNode
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(
1108 git_url, rev, project_path, filepath, lineno, result
1109): # need to have these args
1110 url = utils.form_https_url(git_url, rev, project_path, filepath, lineno=lineno)
1111 assert url == result
1112
1113
1114def get_git_path() -> str:
1115 """Get the path to the git executable."""
1116 git_path = shutil.which("git")
1117 if not git_path:
1118 raise FileNotFoundError("Git executable not found")
1119 if not Path(git_path).is_file():
1120 raise FileNotFoundError("Git executable path is invalid")
1121 return git_path
1122
1123
1124def init_git_repo(repo_path: Path, remote_url: str) -> Path:
1125 """Initialize a git repository for testing."""
1126 git_dir = repo_path / "test_repo"
1127 src_dir = git_dir / "src"
1128 src_dir.mkdir(parents=True)
1129
1130 git_path = get_git_path()
1131 if not git_path:
1132 raise FileNotFoundError("Git executable not found")
1133 if not Path(git_path).is_file():
1134 raise FileNotFoundError("Git executable path is invalid")
1135
1136 # Initialize git repo
1137 subprocess.run([git_path, "init"], cwd=git_dir, check=True, capture_output=True)
1138 subprocess.run(
1139 [git_path, "config", "user.email", "test@example.com"], cwd=git_dir, check=True
1140 )
1141 subprocess.run(
1142 [git_path, "config", "user.name", "Test User"], cwd=git_dir, check=True
1143 )
1144
1145 # Create a test file and commit
1146 test_file = src_dir / "test_file.py"
1147 test_file.write_text("# Test file\nprint('hello')\n")
1148 subprocess.run([git_path, "add", "."], cwd=git_dir, check=True)
1149 subprocess.run(
1150 [git_path, "commit", "-m", "Initial commit"], cwd=git_dir, check=True
1151 )
1152
1153 # Add a remote
1154 subprocess.run(
1155 [git_path, "remote", "add", "origin", remote_url],
1156 cwd=git_dir,
1157 check=True,
1158 )
1159
1160 return git_dir
1161
1162
1163@pytest.fixture(
1164 params=[
1165 ("test_repo_git", "git@github.com:test-user/test-repo.git"),
1166 ("test_repo_https", "https://github.com/test-user/test-repo.git"),
1167 ]
1168)
1169def git_repo(tmp_path: str, request: pytest.FixtureRequest) -> tuple[Path, str]:
1170 """Create git repos for testing."""
1171 repo_name, remote_url = request.param
1172 repo_path = Path(tmp_path) / repo_name
1173 repo_path = init_git_repo(repo_path, remote_url)
1174 return repo_path, remote_url
1175
1176
1177def get_current_commit_hash(git_dir: Path) -> str:
1178 """Get the current commit hash of the git repository."""
1179 git_path = get_git_path()
1180 result = subprocess.run(
1181 [git_path, "rev-parse", "HEAD"],
1182 cwd=git_dir,
1183 check=True,
1184 capture_output=True,
1185 text=True,
1186 )
1187
1188 return str(result.stdout.strip())
1189
1190
1191def test_locate_git_root(git_repo: tuple[Path, str]) -> None:
1192 repo_path = git_repo[0]
1193 src_dir = repo_path / "src"
1194 git_root = utils.locate_git_root(src_dir)
1195 assert git_root == repo_path
1196
1197
1198def test_get_remote_url(git_repo: tuple[Path, str]) -> None:
1199 repo_path, expected_url = git_repo
1200 remote_url = utils.get_remote_url(repo_path)
1201 assert remote_url == expected_url
1202
1203
1204def test_get_current_rev(git_repo: tuple[Path, str]) -> None:
1205 repo_path, _ = git_repo
1206 current_rev = get_current_commit_hash(repo_path)
1207 assert current_rev == utils.get_current_rev(repo_path)
1208
1209
1210def test_get_current_rev_detached_head(tmp_path: Path) -> None:
1211 """In a detached HEAD (e.g. CI checkouts) .git/HEAD holds the commit SHA
1212 directly; get_current_rev returns it rather than warning and giving up."""
1213 git_root = tmp_path / "repo"
1214 (git_root / ".git").mkdir(parents=True)
1215 sha = "a1b2c3d4e5f60718293a4b5c6d7e8f9012345678"
1216 (git_root / ".git" / "HEAD").write_text(f"{sha}\n")
1217
1218 assert utils.get_current_rev(git_root) == sha
1219
1220
1221@pytest.mark.parametrize(
1222 ("text", "leading_sequences", "result"),
1223 [
1224 (
1225 """
1226* some text in a comment
1227* some text in a comment
1228*
1229""",
1230 ["*"],
1231 """
1232 some text in a comment
1233 some text in a comment
1234
1235""",
1236 ),
1237 ],
1238)
1239def test_remove_leading_sequences(text, leading_sequences, result):
1240 clean_text = utils.remove_leading_sequences(text, leading_sequences)
1241 assert clean_text == result
1242
1243
1244@pytest.mark.parametrize(
1245 ("text", "rst_markers", "rst_text", "positions"),
1246 [
1247 (
1248 """
1249@rst
1250.. impl:: multiline rst text
1251 :id: IMPL_71
1252@endrst
1253""",
1254 ["@rst", "@endrst"],
1255 f""".. impl:: multiline rst text{UNIX_NEWLINE} :id: IMPL_71{UNIX_NEWLINE}""",
1256 {"row_offset": 1, "start_idx": 6, "end_idx": 51},
1257 ),
1258 (
1259 """
1260@rst.. impl:: oneline rst text@endrst
1261""",
1262 ["@rst", "@endrst"],
1263 """.. impl:: oneline rst text""",
1264 {"row_offset": 0, "start_idx": 5, "end_idx": 31},
1265 ),
1266 ],
1267)
1268def test_extract_rst(text, rst_markers, rst_text, positions):
1269 extracted_rst = utils.extract_rst(text, rst_markers[0], rst_markers[1])
1270 assert extracted_rst is not None
1271 assert extracted_rst["rst_text"] == rst_text
1272 assert extracted_rst["start_idx"] == positions["start_idx"]
1273 assert extracted_rst["end_idx"] == positions["end_idx"]
1274
1275
1276# ========== YAML-specific tests ==========
1277
1278
1279@pytest.mark.parametrize(
1280 ("code", "expected_structure"),
1281 [
1282 # Basic key-value pair
1283 (
1284 b"""
1285 # Comment before key
1286 database:
1287 host: localhost
1288 """,
1289 "database:",
1290 ),
1291 # Comment in nested structure
1292 (
1293 b"""
1294 services:
1295 web:
1296 # Comment before image
1297 image: nginx:latest
1298 """,
1299 "image: nginx:latest",
1300 ),
1301 # Comment before list item
1302 (
1303 b"""
1304 items:
1305 # Comment before list item
1306 - name: item1
1307 value: test
1308 """,
1309 "- name: item1",
1310 ),
1311 # Comment in document structure
1312 (
1313 b"""---
1314# Comment in document
1315version: "3.8"
1316services:
1317 app:
1318 build: .
1319 """,
1320 "version:",
1321 ),
1322 # Flow mapping structure
1323 (
1324 b"""
1325 # Comment before flow mapping
1326 config: {debug: true, port: 8080}
1327 """,
1328 "config:",
1329 ),
1330 ],
1331)
1332def test_find_yaml_next_structure(code, expected_structure, init_yaml_tree_sitter):
1333 """Test the find_yaml_next_structure function."""
1334 parser, query = init_yaml_tree_sitter
1335 comments = utils.extract_comments(code, parser, query)
1336 assert comments, "No comments found in the code"
1337
1338 next_structure = utils.find_yaml_next_structure(comments[0])
1339 assert next_structure, "No next structure found"
1340 structure_text = next_structure.text.decode("utf-8")
1341 assert expected_structure in structure_text
1342
1343
1344@pytest.mark.parametrize(
1345 ("code", "expected_structure"),
1346 [
1347 # Comment associated with key-value pair
1348 (
1349 b"""
1350 # Database configuration
1351 database:
1352 host: localhost
1353 port: 5432
1354 """,
1355 "database:",
1356 ),
1357 # Comment associated with nested structure
1358 (
1359 b"""
1360 services:
1361 web:
1362 # Web service image
1363 image: nginx:latest
1364 ports:
1365 - "80:80"
1366 """,
1367 "image: nginx:latest",
1368 ),
1369 # Comment associated with list item
1370 (
1371 b"""
1372 dependencies:
1373 # First dependency
1374 - name: redis
1375 version: "6.0"
1376 - name: postgres
1377 version: "13"
1378 """,
1379 "- name: redis",
1380 ),
1381 # Comment inside parent structure
1382 (
1383 b"""
1384 app:
1385 # Internal comment
1386 name: myapp
1387 version: "1.0"
1388 """,
1389 "name: myapp",
1390 ),
1391 ],
1392)
1393def test_find_yaml_associated_structure(
1394 code, expected_structure, init_yaml_tree_sitter
1395):
1396 """Test the find_yaml_associated_structure function."""
1397 parser, query = init_yaml_tree_sitter
1398 comments = utils.extract_comments(code, parser, query)
1399 assert comments, "No comments found in the code"
1400
1401 associated_structure = utils.find_yaml_associated_structure(comments[0])
1402 assert associated_structure, "No associated structure found"
1403 structure_text = associated_structure.text.decode("utf-8")
1404 assert expected_structure in structure_text
1405
1406
1407@pytest.mark.parametrize(
1408 ("code", "expected_results"),
1409 [
1410 # Multiple comments in sequence
1411 (
1412 b"""
1413 # First comment
1414 # Second comment
1415 database:
1416 host: localhost
1417 """,
1418 ["database:", "database:"], # Both comments should associate with database
1419 ),
1420 # Comments at different nesting levels
1421 (
1422 b"""
1423 # Top level comment
1424 services:
1425 web:
1426 # Nested comment
1427 image: nginx:latest
1428 """,
1429 ["services:", "image: nginx:latest"],
1430 ),
1431 ],
1432)
1433def test_multiple_yaml_comments(code, expected_results, init_yaml_tree_sitter):
1434 """Test handling of multiple YAML comments in the same file."""
1435 parser, query = init_yaml_tree_sitter
1436 comments = utils.extract_comments(code, parser, query)
1437 comments.sort(key=lambda x: x.start_point.row)
1438
1439 assert len(comments) == len(expected_results), (
1440 f"Expected {len(expected_results)} comments, found {len(comments)}"
1441 )
1442
1443 for i, comment in enumerate(comments):
1444 associated_structure = utils.find_yaml_associated_structure(comment)
1445 assert associated_structure, f"No associated structure found for comment {i}"
1446 structure_text = associated_structure.text.decode("utf-8")
1447 assert expected_results[i] in structure_text
1448
1449
1450@pytest.mark.parametrize(
1451 ("code", "has_structure"),
1452 [
1453 # Comment at end of file with no following structure
1454 (
1455 b"""
1456database:
1457 host: localhost
1458# End of file comment
1459 """,
1460 True, # This will actually find the parent database structure
1461 ),
1462 # Comment with only whitespace after
1463 (
1464 b"""
1465 # Lonely comment
1466
1467
1468 """,
1469 False,
1470 ),
1471 # Comment before valid structure
1472 (
1473 b"""
1474 # Valid comment
1475 key: value
1476 """,
1477 True,
1478 ),
1479 ],
1480)
1481def test_yaml_edge_cases(code, has_structure, init_yaml_tree_sitter):
1482 """Test edge cases in YAML comment processing."""
1483 parser, query = init_yaml_tree_sitter
1484 comments = utils.extract_comments(code, parser, query)
1485
1486 if comments:
1487 structure = utils.find_yaml_associated_structure(comments[0])
1488 if has_structure:
1489 assert structure, "Expected to find associated structure"
1490 else:
1491 assert structure is None, "Expected no associated structure"
1492 else:
1493 assert not has_structure, "No comments found but structure was expected"
1494
1495
1496@pytest.mark.parametrize(
1497 ("code", "expected_structures"),
1498 [
1499 # Simpler nested YAML structure
1500 (
1501 b"""# Global configuration
1502version: "3.8"
1503
1504# Services section
1505services:
1506 web:
1507 image: nginx:latest
1508 # Port configuration
1509 ports:
1510 - "80:80"
1511 """,
1512 [
1513 "version:", # Global configuration
1514 "services:", # Services section
1515 '- "80:80"', # Port configuration
1516 ],
1517 ),
1518 ],
1519)
1520def test_complex_yaml_structure(code, expected_structures, init_yaml_tree_sitter):
1521 """Test complex nested YAML structures with multiple comments."""
1522 parser, query = init_yaml_tree_sitter
1523 comments = utils.extract_comments(code, parser, query)
1524 comments.sort(key=lambda x: x.start_point.row)
1525
1526 assert len(comments) == len(expected_structures), (
1527 f"Expected {len(expected_structures)} comments, found {len(comments)}"
1528 )
1529
1530 for i, comment in enumerate(comments):
1531 associated_structure = utils.find_yaml_associated_structure(comment)
1532 assert associated_structure, f"No associated structure found for comment {i}"
1533 structure_text = associated_structure.text.decode("utf-8")
1534 assert expected_structures[i] in structure_text, (
1535 f"Expected '{expected_structures[i]}' in structure text: '{structure_text}'"
1536 )
1537
1538
1539@pytest.mark.parametrize(
1540 ("code", "expected_type"),
1541 [
1542 # Block mapping pair
1543 (
1544 b"""
1545 # Comment
1546 key: value
1547 """,
1548 "block_mapping_pair",
1549 ),
1550 # Block sequence item
1551 (
1552 b"""
1553 items:
1554 # Comment
1555 - item1
1556 """,
1557 "block_sequence_item",
1558 ),
1559 # Nested block mapping
1560 (
1561 b"""
1562 services:
1563 # Comment
1564 web:
1565 image: nginx
1566 """,
1567 "block_mapping_pair",
1568 ),
1569 ],
1570)
1571def test_yaml_structure_types(code, expected_type, init_yaml_tree_sitter):
1572 """Test that YAML structures return the correct node types."""
1573 parser, query = init_yaml_tree_sitter
1574 comments = utils.extract_comments(code, parser, query)
1575 assert comments, "No comments found"
1576
1577 structure = utils.find_yaml_associated_structure(comments[0])
1578 assert structure, "No associated structure found"
1579 assert structure.type == expected_type, (
1580 f"Expected type {expected_type}, got {structure.type}"
1581 )
1582
1583
1584def test_yaml_document_structure(init_yaml_tree_sitter):
1585 """Test YAML document structure handling."""
1586 code = b"""---
1587# Document comment
1588apiVersion: v1
1589kind: ConfigMap
1590metadata:
1591 name: my-config
1592data:
1593 # Data comment
1594 config.yml: |
1595 setting: value
1596 """
1597
1598 parser, query = init_yaml_tree_sitter
1599 comments = utils.extract_comments(code, parser, query)
1600 comments.sort(key=lambda x: x.start_point.row)
1601
1602 # Should find both comments
1603 assert len(comments) >= 2, f"Expected at least 2 comments, found {len(comments)}"
1604
1605 # First comment should associate with apiVersion
1606 first_structure = utils.find_yaml_associated_structure(comments[0])
1607 assert first_structure, "No structure found for first comment"
1608 first_text = first_structure.text.decode("utf-8")
1609 assert "apiVersion:" in first_text
1610
1611 # Second comment should associate with config.yml
1612 second_structure = utils.find_yaml_associated_structure(comments[1])
1613 assert second_structure, "No structure found for second comment"
1614 second_text = second_structure.text.decode("utf-8")
1615 assert "config.yml:" in second_text
1616
1617
1618def test_yaml_inline_comments_current_behavior(init_yaml_tree_sitter):
1619 """Test improved behavior of inline comments in YAML after the fix."""
1620 code = b"""key1: value1 # inline comment about key1
1621key2: value2
1622key3: value3 # inline comment about key3
1623"""
1624
1625 parser, query = init_yaml_tree_sitter
1626 comments = utils.extract_comments(code, parser, query)
1627 comments.sort(key=lambda x: x.start_point.row)
1628
1629 assert len(comments) == 2, f"Expected 2 comments, found {len(comments)}"
1630
1631 # Fixed behavior: inline comment about key1 now correctly associates with key1
1632 first_structure = utils.find_yaml_associated_structure(comments[0])
1633 assert first_structure, "No structure found for first comment"
1634 first_text = first_structure.text.decode("utf-8")
1635 assert "key1:" in first_text, f"Expected 'key1:' in '{first_text}'"
1636
1637 # Fixed behavior: inline comment about key3 now correctly associates with key3
1638 second_structure = utils.find_yaml_associated_structure(comments[1])
1639 assert second_structure, "No structure found for second comment"
1640 second_text = second_structure.text.decode("utf-8")
1641 assert "key3:" in second_text, f"Expected 'key3:' in '{second_text}'"
1642
1643
1644@pytest.mark.parametrize(
1645 ("code", "expected_associations"),
1646 [
1647 # Basic inline comment case
1648 (
1649 b"""key1: value1 # comment about key1
1650key2: value2
1651 """,
1652 ["key1:"], # Now correctly associates with key1
1653 ),
1654 # Multiple inline comments
1655 (
1656 b"""database:
1657 host: localhost # production server
1658 port: 5432 # default postgres port
1659 user: admin
1660 """,
1661 [
1662 "host: localhost",
1663 "port: 5432",
1664 ], # Now correctly associates with the right structures
1665 ),
1666 ],
1667)
1668def test_yaml_inline_comments_fixed_behavior(
1669 code, expected_associations, init_yaml_tree_sitter
1670):
1671 """Test that inline comments now correctly associate with the structure they comment on."""
1672 parser, query = init_yaml_tree_sitter
1673 comments = utils.extract_comments(code, parser, query)
1674 comments.sort(key=lambda x: x.start_point.row)
1675
1676 assert len(comments) == len(expected_associations), (
1677 f"Expected {len(expected_associations)} comments, found {len(comments)}"
1678 )
1679
1680 for i, comment in enumerate(comments):
1681 structure = utils.find_yaml_associated_structure(comment)
1682 assert structure, f"No structure found for comment {i}"
1683 structure_text = structure.text.decode("utf-8")
1684 assert expected_associations[i] in structure_text, (
1685 f"Expected '{expected_associations[i]}' in structure text: '{structure_text}'"
1686 )
1687
1688
1689@pytest.mark.parametrize(
1690 ("code", "expected_associations"),
1691 [
1692 # Inline comments with list items
1693 (
1694 b"""items:
1695 - name: item1 # first item
1696 - name: item2 # second item
1697 """,
1698 [
1699 "name: item1",
1700 "name: item2",
1701 ], # The inline comment finds the key-value pair within the list item
1702 ),
1703 # Mixed inline and block comments
1704 (
1705 b"""# Block comment for database
1706database:
1707 host: localhost # inline comment for host
1708 port: 5432
1709 # Block comment for user
1710 user: admin
1711 """,
1712 ["database:", "host: localhost", "user: admin"],
1713 ),
1714 # Inline comments in nested structures
1715 (
1716 b"""services:
1717 web:
1718 image: nginx # web server image
1719 ports:
1720 - "80:80" # http port
1721 """,
1722 ["image: nginx", '- "80:80"'],
1723 ),
1724 ],
1725)
1726def test_yaml_inline_comments_comprehensive(
1727 code, expected_associations, init_yaml_tree_sitter
1728):
1729 """Comprehensive test for inline comment behavior in various YAML structures."""
1730 parser, query = init_yaml_tree_sitter
1731 comments = utils.extract_comments(code, parser, query)
1732 comments.sort(key=lambda x: x.start_point.row)
1733
1734 assert len(comments) == len(expected_associations), (
1735 f"Expected {len(expected_associations)} comments, found {len(comments)}"
1736 )
1737
1738 for i, comment in enumerate(comments):
1739 structure = utils.find_yaml_associated_structure(comment)
1740 assert structure, (
1741 f"No structure found for comment {i}: '{comment.text.decode('utf-8')}'"
1742 )
1743 structure_text = structure.text.decode("utf-8")
1744 assert expected_associations[i] in structure_text, (
1745 f"Comment {i} '{comment.text.decode('utf-8')}' -> Expected '{expected_associations[i]}' in '{structure_text}'"
1746 )