[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        )