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