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