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