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