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