[docs] 1# @Test suite for one-line comment parser functionality, TEST_OLP_1, test, [IMPL_OLP_1]
2import pytest
3
4from sphinx_codelinks.analyse.oneline_parser import (
5 OnelineParserInvalidWarning,
6 WarningSubTypeEnum,
7 docstring_tag,
8 jsdoc_tag,
9 oneline_parser,
10)
11from sphinx_codelinks.config import ESCAPE, UNIX_NEWLINE, OneLineCommentStyle
12
13from .conftest import ONELINE_COMMENT_STYLE, ONELINE_COMMENT_STYLE_DEFAULT
14
15
16@pytest.mark.parametrize(
17 "oneline, result",
18 [
19 (
20 f"@title 1, IMPL_1 {UNIX_NEWLINE}",
21 {
22 "title": "title 1",
23 "id": "IMPL_1",
24 "type": "impl",
25 "links": [],
26 "start_column": 1,
27 "end_column": 17,
28 },
29 ),
30 # Test case for leading space after start sequence (bug fix)
31 (
32 f"@ title 1, IMPL_1 {UNIX_NEWLINE}",
33 {
34 "title": "title 1",
35 "id": "IMPL_1",
36 "type": "impl",
37 "links": [],
38 "start_column": 1,
39 "end_column": 18,
40 },
41 ),
42 # Test case for multiple leading spaces after start sequence
43 (
44 f"@ title 1, IMPL_1 {UNIX_NEWLINE}",
45 {
46 "title": "title 1",
47 "id": "IMPL_1",
48 "type": "impl",
49 "links": [],
50 "start_column": 1,
51 "end_column": 20,
52 },
53 ),
54 # Test case for trailing space before end sequence
55 (
56 f"@title 1, IMPL_1 {UNIX_NEWLINE}",
57 {
58 "title": "title 1",
59 "id": "IMPL_1",
60 "type": "impl",
61 "links": [],
62 "start_column": 1,
63 "end_column": 18,
64 },
65 ),
66 # Test case for both leading and trailing spaces
67 (
68 f"@ title 1, IMPL_1 {UNIX_NEWLINE}",
69 {
70 "title": "title 1",
71 "id": "IMPL_1",
72 "type": "impl",
73 "links": [],
74 "start_column": 1,
75 "end_column": 21,
76 },
77 ),
78 ],
79)
80def test_oneline_parser_default_config_positive(
81 oneline: str, result: dict[str, str | list[str]]
82) -> None:
83 oneline_need = oneline_parser(oneline, ONELINE_COMMENT_STYLE_DEFAULT)
84 assert oneline_need == result
85
86
87# Test case for space as field separator (as mentioned by Kilian)
88# Example: @Implementation <field1> <field2>
89ONELINE_COMMENT_STYLE_SPACE_SEPARATOR = OneLineCommentStyle(
90 start_sequence="@",
91 end_sequence=UNIX_NEWLINE,
92 field_split_char=" ",
93 needs_fields=[
94 {"name": "title"},
95 {"name": "id"},
96 {"name": "type", "default": "impl"},
97 ],
98)
99
100
101@pytest.mark.parametrize(
102 "oneline, result",
103 [
104 # Basic space-separated fields
105 (
106 f"@Implementation IMPL_1{UNIX_NEWLINE}",
107 {
108 "title": "Implementation",
109 "id": "IMPL_1",
110 "type": "impl",
111 "start_column": 1,
112 "end_column": 22,
113 },
114 ),
115 # Space separator with explicit type
116 (
117 f"@MyFeature FEAT_001 feature{UNIX_NEWLINE}",
118 {
119 "title": "MyFeature",
120 "id": "FEAT_001",
121 "type": "feature",
122 "start_column": 1,
123 "end_column": 27,
124 },
125 ),
126 # Leading space after @ (the bug Kilian reported)
127 (
128 f"@ Implementation IMPL_2{UNIX_NEWLINE}",
129 {
130 "title": "Implementation",
131 "id": "IMPL_2",
132 "type": "impl",
133 "start_column": 1,
134 "end_column": 23,
135 },
136 ),
137 # Trailing space before newline
138 (
139 f"@Implementation IMPL_3 {UNIX_NEWLINE}",
140 {
141 "title": "Implementation",
142 "id": "IMPL_3",
143 "type": "impl",
144 "start_column": 1,
145 "end_column": 23,
146 },
147 ),
148 # Multiple leading spaces after @
149 (
150 f"@ Title ID_456{UNIX_NEWLINE}",
151 {
152 "title": "Title",
153 "id": "ID_456",
154 "type": "impl",
155 "start_column": 1,
156 "end_column": 15,
157 },
158 ),
159 # Title contain spaces
160 (
161 f"@ Title\ escape\ space ID_456{UNIX_NEWLINE}",
162 {
163 "title": "Title escape space",
164 "id": "ID_456",
165 "type": "impl",
166 "start_column": 1,
167 "end_column": 30,
168 },
169 ),
170 ],
171)
172def test_oneline_parser_space_separator(
173 oneline: str, result: dict[str, str | list[str]]
174) -> None:
175 """Test oneline parser with space as field separator."""
176 oneline_need = oneline_parser(oneline, ONELINE_COMMENT_STYLE_SPACE_SEPARATOR)
177 assert oneline_need == result
178
179
180@pytest.mark.parametrize(
181 "oneline, result",
182 [
183 (
184 "[[IMPL_1, title 1]]",
185 {
186 "id": "IMPL_1",
187 "title": "title 1",
188 "type": "impl",
189 "links": [],
190 "status": "open",
191 "priority": "low",
192 "start_column": 2,
193 "end_column": 17,
194 },
195 ),
196 (
197 "[[IMPL_2, title 2, impl, [], closed]]",
198 {
199 "id": "IMPL_2",
200 "title": "title 2",
201 "type": "impl",
202 "links": [],
203 "status": "closed",
204 "priority": "low",
205 "start_column": 2,
206 "end_column": 35,
207 },
208 ),
209 (
210 "[[IMPL_3, title\, 3, impl, [], closed]]",
211 {
212 "id": "IMPL_3",
213 "title": "title, 3",
214 "type": "impl",
215 "links": [],
216 "status": "closed",
217 "priority": "low",
218 "start_column": 2,
219 "end_column": 37,
220 },
221 ),
222 (
223 "[[IMPL_5, title 5, impl, [SPEC_1, SPEC_2], open]]",
224 {
225 "id": "IMPL_5",
226 "title": "title 5",
227 "type": "impl",
228 "links": ["SPEC_1", "SPEC_2"],
229 "status": "open",
230 "priority": "low",
231 "start_column": 2,
232 "end_column": 47,
233 },
234 ),
235 (
236 "[[IMPL_7, Function has a, in the title]]",
237 {
238 "id": "IMPL_7",
239 "title": "Function has a",
240 "type": "in the title",
241 "links": [],
242 "status": "open",
243 "priority": "low",
244 "start_column": 2,
245 "end_column": 38,
246 },
247 ),
248 (
249 "[[IMPL_8, [Title starts with a bracket], impl]]",
250 {
251 "id": "IMPL_8",
252 "title": "[Title starts with a bracket]",
253 "type": "impl",
254 "links": [],
255 "status": "open",
256 "priority": "low",
257 "start_column": 2,
258 "end_column": 45,
259 },
260 ),
261 (
262 "[[IMPL_9, Function Baz, impl, [SPEC_1, SPEC_2[text], SPEC_3], open]]",
263 {
264 "id": "IMPL_9",
265 "title": "Function Baz",
266 "type": "impl",
267 "links": ["SPEC_1", "SPEC_2[text"],
268 "status": "SPEC_3]",
269 "priority": "open",
270 "start_column": 2,
271 "end_column": 66,
272 },
273 ),
274 (
275 "[[IMPL_10, title 10, impl, [SPEC_1], open]]",
276 {
277 "id": "IMPL_10",
278 "title": "title 10",
279 "type": "impl",
280 "links": ["SPEC_1"],
281 "status": "open",
282 "priority": "low",
283 "start_column": 2,
284 "end_column": 41,
285 },
286 ),
287 (
288 "[[IMPL_11, title 11, impl, [SPEC\,_1], open]]",
289 {
290 "id": "IMPL_11",
291 "title": "title 11",
292 "type": "impl",
293 "links": ["SPEC,_1"],
294 "status": "open",
295 "priority": "low",
296 "start_column": 2,
297 "end_column": 43,
298 },
299 ),
300 (
301 "[[IMPL_12, title 12, impl, [\[SPEC\,_1\]], open]]",
302 {
303 "id": "IMPL_12",
304 "title": "title 12",
305 "type": "impl",
306 "links": ["[SPEC,_1]"],
307 "status": "open",
308 "priority": "low",
309 "start_column": 2,
310 "end_column": 47,
311 },
312 ),
313 (
314 "[[IMPL_13, title\\ 13, impl, [\[SPEC\,_1\]], open]]",
315 {
316 "id": "IMPL_13",
317 "title": "title\ 13",
318 "type": "impl",
319 "links": ["[SPEC,_1]"],
320 "status": "open",
321 "priority": "low",
322 "start_column": 2,
323 "end_column": 48,
324 },
325 ),
326 ],
327)
328def test_oneline_parser_custom_config_positive(
329 oneline: str, result: dict[str, str | list[str]]
330) -> None:
331 oneline_need = oneline_parser(oneline, ONELINE_COMMENT_STYLE)
332 assert oneline_need == result
333
334
335@pytest.mark.parametrize(
336 "oneline, result",
337 [
338 (
339 f"[[IMPL_4, title{ESCAPE}{ESCAPE}, 4, impl, [], closed]]",
340 OnelineParserInvalidWarning(
341 sub_type=WarningSubTypeEnum.missing_square_brackets,
342 msg="Field 'links' with 'type': 'list[str]' must be given with '[]' brackets",
343 ),
344 ),
345 (
346 "[[IMPL_2, Function Bar, impl, [SPEC_1, SPEC_2, open]]",
347 OnelineParserInvalidWarning(
348 sub_type=WarningSubTypeEnum.missing_square_brackets,
349 msg="Field 'links' with 'type': 'list[str]' must be given with '[]' brackets",
350 ),
351 ),
352 (
353 "[[IMPL_13, title 13, impl, 13[\[SPEC\,_1\]], open]]",
354 OnelineParserInvalidWarning(
355 sub_type=WarningSubTypeEnum.not_start_or_end_with_square_brackets,
356 msg="Field 'links' with 'type': 'list[str]' must start with '[' and end with ']'",
357 ),
358 ),
359 (
360 "[[IMPL_14, title 13, impl, 13[\[SPEC\,_1\]], open, low, high]]",
361 OnelineParserInvalidWarning(
362 sub_type=WarningSubTypeEnum.too_many_fields,
363 msg="7 given fields, maximum is 6",
364 ),
365 ),
366 (
367 "[[IMPL_15]]",
368 OnelineParserInvalidWarning(
369 sub_type=WarningSubTypeEnum.too_few_fields,
370 msg="1 given fields, minimum is 2",
371 ),
372 ),
373 (
374 f"[[IMPL_16]]{UNIX_NEWLINE}, title 16]]",
375 OnelineParserInvalidWarning(
376 sub_type=WarningSubTypeEnum.newline_in_field,
377 msg="Field 'id' contains a newline character",
378 ),
379 ),
380 ],
381)
382def test_oneline_parser_custom_config_negative(
383 oneline: str, result: OnelineParserInvalidWarning
384) -> None:
385 res = oneline_parser(oneline, ONELINE_COMMENT_STYLE)
386 assert res == result
387
388
389@pytest.mark.parametrize(
390 "oneline, result",
391 [
392 (
393 f"@title 17]]{UNIX_NEWLINE}, IMPL_17 {UNIX_NEWLINE}",
394 OnelineParserInvalidWarning(
395 sub_type=WarningSubTypeEnum.newline_in_field,
396 msg="Field 'title' contains a newline character",
397 ),
398 ),
399 (
400 f"@title 17]], IMPL_17, impl, [SPEC_3, SPEC_4{UNIX_NEWLINE} ] {UNIX_NEWLINE}",
401 OnelineParserInvalidWarning(
402 sub_type=WarningSubTypeEnum.newline_in_field,
403 msg="Field 'links' contains a newline character",
404 ),
405 ),
406 ],
407)
408def test_oneline_parser_default_config_negative(
409 oneline: str, result: OnelineParserInvalidWarning
410) -> None:
411 assert oneline_parser(oneline, ONELINE_COMMENT_STYLE_DEFAULT) == result
412
413
414@pytest.mark.parametrize(
415 "oneline_config, result",
416 [
417 (
418 OneLineCommentStyle(
419 start_sequence="[[",
420 end_sequence="]]",
421 field_split_char=",",
422 needs_fields=[
423 {"name": "title"},
424 {"name": "id"},
425 {"name": "type", "default": "impl"},
426 {"name": "links", "type": "list[]", "default": []}, # wrong type
427 ],
428 ),
429 [
430 "Schema validation error in need_fields 'links': 'list[]' is not one of ['str', 'list[str]']"
431 ],
432 ),
433 (
434 OneLineCommentStyle(
435 start_sequence="[[",
436 end_sequence="]]",
437 field_split_char=",",
438 needs_fields=[
439 {"name": "title"},
440 {"name": "id"},
441 {"name": "type", "default": 123}, # int is invalid
442 {"name": "links", "type": "list[str]", "default": []},
443 ],
444 ),
445 [
446 "Schema validation error in need_fields 'type': 123 is not of type 'string'"
447 ],
448 ),
449 (
450 OneLineCommentStyle(
451 start_sequence="[[",
452 end_sequence="]]",
453 field_split_char=",",
454 needs_fields=[
455 {"name": "title", "qwe": "qwe"}, # invalid qwe filed
456 {"name": "id"},
457 {"name": "type", "default": "impl"},
458 {"name": "links", "type": "list[str]", "default": []},
459 ],
460 ),
461 [
462 "Schema validation error in need_fields 'title': Additional properties are not allowed ('qwe' was unexpected)"
463 ],
464 ),
465 (
466 OneLineCommentStyle(
467 start_sequence="[[",
468 end_sequence="]]",
469 field_split_char=",",
470 needs_fields=[
471 {"name": "title"},
472 {"name": "id"},
473 {
474 "name": "type",
475 "type: ": "list[str]",
476 "default": "impl",
477 }, # wring combination of type and default
478 {"name": "links", "type": "list[str]", "default": []},
479 ],
480 ),
481 [
482 "Schema validation error in need_fields 'type': Additional properties are not allowed ('type: ' was unexpected)"
483 ],
484 ),
485 (
486 OneLineCommentStyle(
487 start_sequence="[[",
488 end_sequence="]]",
489 field_split_char=",",
490 needs_fields=[
491 {"name": "id"} # "title" and "type" are not given
492 ],
493 ),
494 ["Missing required fields: ['title', 'type']"],
495 ),
496 (
497 OneLineCommentStyle(
498 start_sequence="[[",
499 end_sequence="]]",
500 field_split_char=",",
501 needs_fields=[
502 {"name": "id"},
503 {"name": "id"}, # duplicate
504 ],
505 ),
506 [
507 "Missing required fields: ['title', 'type']",
508 "Field 'id' is defined multiple times.",
509 ],
510 ),
511 (
512 OneLineCommentStyle(
513 start_sequence=1234, # wrong type
514 end_sequence=5678,
515 field_split_char=2222,
516 needs_fields=[
517 {"name": "id"},
518 ],
519 ),
520 [
521 "Schema validation error in field 'field_split_char': 2222 is not of type 'string'",
522 "Schema validation error in field 'end_sequence': 5678 is not of type 'string'",
523 "Schema validation error in field 'start_sequence': 1234 is not of type 'string'",
524 "Missing required fields: ['title', 'type']",
525 ],
526 ),
527 ],
528)
529def test_oneline_schema_validator_negative(oneline_config, result):
530 errors = oneline_config.check_fields_configuration()
531 assert sorted(errors) == sorted(result)
532
533
534@pytest.mark.parametrize(
535 "oneline",
536 [
537 # A start sequence embedded in free-form prose (word chars before it)
538 # must not be treated as a one-line marker (issue #88).
539 f"// Some prose that mentions @@another-tag(item_1, item_2): more text{UNIX_NEWLINE}",
540 f"// See @author, check the example{UNIX_NEWLINE}",
541 f"// We parse things matching @pattern, then act{UNIX_NEWLINE}",
542 ],
543)
544def test_oneline_parser_ignores_start_sequence_in_prose(oneline: str) -> None:
545 """Issue #88: only anchor a marker at the start of the comment content."""
546 assert oneline_parser(oneline, ONELINE_COMMENT_STYLE_DEFAULT) is None
547
548
549@pytest.mark.parametrize(
550 "oneline, expected_id",
551 [
552 (f"// @My Title, IMPL_1{UNIX_NEWLINE}", "IMPL_1"), # line-comment leader
553 (f" // @My Title, IMPL_2{UNIX_NEWLINE}", "IMPL_2"), # indented
554 (f"* @My Title, IMPL_3{UNIX_NEWLINE}", "IMPL_3"), # block-comment continuation
555 (f"/// @My Title, IMPL_4{UNIX_NEWLINE}", "IMPL_4"), # doc comment
556 (f"//! @My Title, IMPL_5{UNIX_NEWLINE}", "IMPL_5"), # inner doc comment
557 ],
558)
559def test_oneline_parser_marker_preceded_only_by_comment_decoration(
560 oneline: str, expected_id: str
561) -> None:
562 """A marker preceded only by comment syntax/whitespace is still recognized."""
563 res = oneline_parser(oneline, ONELINE_COMMENT_STYLE_DEFAULT)
564 assert isinstance(res, dict)
565 assert res["id"] == expected_id
566
567
568def test_oneline_parser_bounded_marker_allowed_after_prose() -> None:
569 """Explicitly-bounded markers ([[ ... ]]) are self-delimiting, so they may
570 be embedded after prose; only newline-terminated markers are anchored."""
571 oneline = "// one-line comment style: [[IMPL_1, title 1]]"
572 res = oneline_parser(oneline, ONELINE_COMMENT_STYLE)
573 assert isinstance(res, dict)
574 assert res["id"] == "IMPL_1"
575
576
577#: one required field (``title``): a line without the separator is a whole marker
578ONE_REQUIRED_FIELD = OneLineCommentStyle(
579 needs_fields=[{"name": "title"}, {"name": "type", "default": "impl"}]
580)
581
582
583@pytest.mark.parametrize(
584 "oneline",
585 [
586 f"// @param x the value{UNIX_NEWLINE}",
587 f"/// @return nothing{UNIX_NEWLINE}",
588 f" * @only{UNIX_NEWLINE}",
589 ],
590)
591def test_one_character_start_without_separator_is_not_a_marker(oneline: str) -> None:
592 """A one-character start sequence, no field separator in the content and more than
593 one required field: documentation tags such as ``@param`` are not markers, and not
594 warnings either."""
595 assert oneline_parser(oneline, ONELINE_COMMENT_STYLE_DEFAULT) is None
596
597
598@pytest.mark.parametrize(
599 "oneline, sub_type",
600 [
601 (
602 f"// @see A, B, C, D, E{UNIX_NEWLINE}",
603 WarningSubTypeEnum.too_many_fields,
604 ),
605 # the fourth field is ``links``, a ``list[str]``
606 (
607 f"// @brief a, b, c, d{UNIX_NEWLINE}",
608 WarningSubTypeEnum.missing_square_brackets,
609 ),
610 ],
611)
612def test_one_character_start_with_separator_still_warns(
613 oneline: str, sub_type: WarningSubTypeEnum
614) -> None:
615 res = oneline_parser(oneline, ONELINE_COMMENT_STYLE_DEFAULT)
616 assert isinstance(res, OnelineParserInvalidWarning)
617 assert res.sub_type == sub_type
618
619
620def test_one_character_start_with_two_fields_is_still_a_need() -> None:
621 """A line with the separator, ``@brief Does a, b``, is the need it always was."""
622 assert oneline_parser(
623 f"// @brief Does a, b{UNIX_NEWLINE}", ONELINE_COMMENT_STYLE_DEFAULT
624 ) == {
625 "title": "brief Does a",
626 "id": "b",
627 "type": "impl",
628 "links": [],
629 "start_column": 4,
630 "end_column": 19,
631 }
632
633
634def test_multi_character_start_without_separator_still_warns() -> None:
635 """``[[`` is specific enough: a marker with too few fields is a warning."""
636 assert oneline_parser(
637 "// [[ only-title ]]", ONELINE_COMMENT_STYLE
638 ) == OnelineParserInvalidWarning(
639 sub_type=WarningSubTypeEnum.too_few_fields,
640 msg="1 given fields, minimum is 2",
641 )
642
643
644def test_one_character_start_with_one_required_field_is_a_need() -> None:
645 """With one required field, a line without the separator is a whole marker."""
646 assert oneline_parser(f"// @x{UNIX_NEWLINE}", ONE_REQUIRED_FIELD) == {
647 "title": "x",
648 "type": "impl",
649 "start_column": 4,
650 "end_column": 5,
651 }
652
653
654@pytest.mark.parametrize(
655 "oneline, style",
656 [
657 ("// [[ ]]", ONELINE_COMMENT_STYLE),
658 (f"// @{UNIX_NEWLINE}", ONE_REQUIRED_FIELD),
659 (f"// @{UNIX_NEWLINE}", ONELINE_COMMENT_STYLE_DEFAULT),
660 (f"// @ {UNIX_NEWLINE}", ONELINE_COMMENT_STYLE_DEFAULT),
661 ],
662)
663def test_empty_content_is_not_a_marker(
664 oneline: str, style: OneLineCommentStyle
665) -> None:
666 """Nothing between the start and end sequences: not a marker, whatever the start
667 sequence and however many fields are required."""
668 assert oneline_parser(oneline, style) is None
669
670
671def test_an_escaped_separator_counts_as_present() -> None:
672 """``\\,`` is a separator in the content, so the line is a marker, and one field
673 is too few."""
674 res = oneline_parser(f"// @a\\,b{UNIX_NEWLINE}", ONELINE_COMMENT_STYLE_DEFAULT)
675 assert isinstance(res, OnelineParserInvalidWarning)
676 assert res.sub_type == WarningSubTypeEnum.too_few_fields
677
678
679def test_a_non_ascii_start_sequence_of_one_character_is_one_character() -> None:
680 """One character is one code point: ``§`` is a one-character start sequence."""
681 style = OneLineCommentStyle(start_sequence="§")
682 assert oneline_parser(f"// §only{UNIX_NEWLINE}", style) is None
683 res = oneline_parser(f"// §a title, IMPL_SECT{UNIX_NEWLINE}", style)
684 assert isinstance(res, dict)
685 assert res["id"] == "IMPL_SECT"
686
687
688@pytest.mark.parametrize(
689 "line, expected",
690 [
691 pytest.param("@param a: the first, thing", "param", id="param_field"),
692 pytest.param("@return: nothing", "return", id="return_field"),
693 pytest.param("@raise ValueError: on bad input", "raise", id="raise_field"),
694 pytest.param("@type x_1: int", "type", id="type_field"),
695 pytest.param("@param a:", "param", id="field_empty_text"),
696 pytest.param('"""@return: nothing', "return", id="opening_quotes"),
697 pytest.param("@todo fix the parser, IMPL_DS, impl", None, id="todo_title"),
698 pytest.param(
699 '"""@todo fix the parser, IMPL_DS, impl"""',
700 None,
701 id="todo_title_opening_quotes",
702 ),
703 pytest.param("@param a the first, thing", None, id="doxygen_comma_form"),
704 pytest.param("@return", None, id="bare_tag"),
705 pytest.param("@param a b: text", None, id="two_words_before_colon"),
706 pytest.param("@param a : text", None, id="space_before_colon"),
707 pytest.param(
708 "@Todo fix the parser, IMPL_TODO2, impl", None, id="case_sensitive"
709 ),
710 pytest.param("@params a: text", None, id="not_a_tag"),
711 pytest.param("see @param a: text", None, id="text_before_start"),
712 # the field word is any run of characters other than space, tab and ``:``,
713 # after spaces or tabs only: ``:|[ \t]+[^ \t:]+:``, locale-free
714 pytest.param("@param नाम: text", "param", id="non_ascii_word_devanagari"),
715 pytest.param("@param ä: text", "param", id="non_ascii_word_latin"),
716 pytest.param("@param *args: text", "param", id="star_args_word"),
717 pytest.param("@type a.b: int", "type", id="dotted_word"),
718 pytest.param("@param\ta: text", "param", id="tab_separator"),
719 pytest.param("@param\x1ca: text", None, id="unit_separator_is_not_a_space"),
720 pytest.param(
721 "@param\u3000a: text", None, id="ideographic_space_is_not_a_space"
722 ),
723 pytest.param("@return : text", None, id="empty_word_before_colon"),
724 pytest.param("@param : text", None, id="empty_word_after_param"),
725 pytest.param("@param1: text", None, id="digit_glued_to_tag"),
726 pytest.param("@return_x: text", None, id="underscore_glued_to_tag"),
727 ],
728)
729def test_docstring_tag_field_shape(line: str, expected: str | None) -> None:
730 """A docstring line is a ``docstring_tag`` only when a listed tag right after the
731 start sequence has the Epydoc field shape: ``:`` directly, or spaces or tabs, one
732 word without space, tab or ``:``, and ``:``. The same table as ubCode's."""
733 assert docstring_tag(line, "@") == expected
734
735
736@pytest.mark.parametrize(
737 ("line", "expected"),
738 [
739 pytest.param(" * @param a the first, thing", "param", id="param"),
740 pytest.param(" * @param {number} a text", "param", id="param_type"),
741 pytest.param(" * @param{number} a text", "param", id="brace_glued_to_tag"),
742 pytest.param(" * @returns", "returns", id="bare_tag"),
743 pytest.param(" * @returns\n", "returns", id="bare_tag_terminated"),
744 pytest.param(" * @deprecated\tUse x, it is faster", "deprecated", id="tab"),
745 pytest.param(" * {@link Foo}, the original", "link", id="inline_tag"),
746 pytest.param(" * {@inheritDoc}", "inheritDoc", id="inline_tag_closed"),
747 pytest.param("@typeParam T the type, of it", "typeParam", id="camel_case_tag"),
748 pytest.param(" * @Param check, IMPL_1, impl", None, id="case_sensitive"),
749 pytest.param(" * @parameters Check, IMPL_1, impl", None, id="longer_word"),
750 pytest.param(" * @param1 text", None, id="digit_glued_to_tag"),
751 pytest.param(" * @param-a text", None, id="dash_glued_to_tag"),
752 pytest.param(" * @param: text", None, id="colon_glued_to_tag"),
753 pytest.param(" * see @param a text", None, id="text_before_start"),
754 pytest.param(" * @Real Need, IMPL_1, impl", None, id="not_a_tag"),
755 pytest.param(
756 " * @param\u3000a text", None, id="ideographic_space_ends_nothing"
757 ),
758 ],
759)
760def test_jsdoc_tag(line: str, expected: str | None) -> None:
761 """A JSDoc comment line is a ``jsdoc_tag`` only when a listed tag right after the
762 start sequence ends at a space, a tab, a ``{``, a ``}`` or the end of the line. The
763 same table as ubCode's."""
764 assert jsdoc_tag(line, "@") == expected