claude-dev-env 1.50.0 → 1.50.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (82) hide show
  1. package/hooks/blocking/_gh_body_arg_utils.py +67 -11
  2. package/hooks/blocking/_md_to_html_blocker_test_support.py +65 -0
  3. package/hooks/blocking/code_rules_annotations_length.py +167 -0
  4. package/hooks/blocking/code_rules_banned_identifiers.py +385 -0
  5. package/hooks/blocking/code_rules_boolean_mustcheck.py +350 -0
  6. package/hooks/blocking/code_rules_comments.py +337 -0
  7. package/hooks/blocking/code_rules_constants_config.py +252 -0
  8. package/hooks/blocking/code_rules_docstrings.py +308 -0
  9. package/hooks/blocking/code_rules_enforcer.py +98 -5765
  10. package/hooks/blocking/code_rules_imports_logging.py +276 -0
  11. package/hooks/blocking/code_rules_magic_values.py +180 -0
  12. package/hooks/blocking/code_rules_mock_completeness.py +295 -0
  13. package/hooks/blocking/code_rules_naming_collection.py +264 -0
  14. package/hooks/blocking/code_rules_optional_params.py +288 -0
  15. package/hooks/blocking/code_rules_paths_syspath.py +186 -0
  16. package/hooks/blocking/code_rules_probe_chains.py +305 -0
  17. package/hooks/blocking/code_rules_probe_detection.py +257 -0
  18. package/hooks/blocking/code_rules_probe_recording.py +225 -0
  19. package/hooks/blocking/code_rules_scope_binding.py +151 -0
  20. package/hooks/blocking/code_rules_shared.py +301 -0
  21. package/hooks/blocking/code_rules_string_magic.py +207 -0
  22. package/hooks/blocking/code_rules_test_assertions.py +226 -0
  23. package/hooks/blocking/code_rules_test_branching_except.py +181 -0
  24. package/hooks/blocking/code_rules_test_isolation.py +341 -0
  25. package/hooks/blocking/code_rules_type_escape.py +341 -0
  26. package/hooks/blocking/code_rules_typeddict_stub.py +305 -0
  27. package/hooks/blocking/code_rules_unused_imports.py +256 -0
  28. package/hooks/blocking/conftest.py +30 -0
  29. package/hooks/blocking/pr_description_body_audit.py +148 -0
  30. package/hooks/blocking/pr_description_command_parser.py +233 -0
  31. package/hooks/blocking/pr_description_enforcer.py +36 -825
  32. package/hooks/blocking/pr_description_pr_number.py +153 -0
  33. package/hooks/blocking/pr_description_readability.py +366 -0
  34. package/hooks/blocking/tdd_enforcer.py +31 -0
  35. package/hooks/blocking/test_code_rules_constants_config.py +26 -0
  36. package/hooks/blocking/test_code_rules_enforcer_banned_noun_word.py +5 -2
  37. package/hooks/blocking/test_code_rules_enforcer_cap_meta.py +0 -5
  38. package/hooks/blocking/test_code_rules_enforcer_comment_string_awareness.py +21 -15
  39. package/hooks/blocking/test_code_rules_enforcer_config_path.py +20 -16
  40. package/hooks/blocking/test_code_rules_enforcer_exempt_marker_chained.py +4 -2
  41. package/hooks/blocking/test_code_rules_enforcer_function_length.py +154 -18
  42. package/hooks/blocking/test_code_rules_enforcer_hardcoded_user_path.py +1 -2
  43. package/hooks/blocking/test_code_rules_enforcer_ignored_must_check_return.py +22 -12
  44. package/hooks/blocking/test_code_rules_enforcer_split_annotations_length.py +55 -0
  45. package/hooks/blocking/test_code_rules_enforcer_split_banned.py +170 -0
  46. package/hooks/blocking/test_code_rules_enforcer_split_comments.py +60 -0
  47. package/hooks/blocking/test_code_rules_enforcer_split_config_path.py +52 -0
  48. package/hooks/blocking/test_code_rules_enforcer_split_constants_config.py +236 -0
  49. package/hooks/blocking/test_code_rules_enforcer_split_entry_1.py +296 -0
  50. package/hooks/blocking/test_code_rules_enforcer_split_entry_2.py +238 -0
  51. package/hooks/blocking/test_code_rules_enforcer_split_isolation_1.py +271 -0
  52. package/hooks/blocking/test_code_rules_enforcer_split_isolation_2.py +283 -0
  53. package/hooks/blocking/test_code_rules_enforcer_split_isolation_3.py +268 -0
  54. package/hooks/blocking/test_code_rules_enforcer_split_isolation_4.py +85 -0
  55. package/hooks/blocking/test_code_rules_enforcer_split_mocks_1.py +303 -0
  56. package/hooks/blocking/test_code_rules_enforcer_split_mocks_2.py +111 -0
  57. package/hooks/blocking/test_code_rules_enforcer_split_mustcheck.py +87 -0
  58. package/hooks/blocking/test_code_rules_enforcer_split_naming.py +107 -0
  59. package/hooks/blocking/test_code_rules_enforcer_split_optional_params.py +325 -0
  60. package/hooks/blocking/test_code_rules_enforcer_split_paths_syspath.py +110 -0
  61. package/hooks/blocking/test_code_rules_enforcer_split_shared.py +44 -0
  62. package/hooks/blocking/test_code_rules_enforcer_split_string_magic.py +55 -0
  63. package/hooks/blocking/test_code_rules_enforcer_split_test_assertions.py +56 -0
  64. package/hooks/blocking/test_code_rules_enforcer_todo_markers.py +21 -15
  65. package/hooks/blocking/test_code_rules_paths_syspath.py +26 -0
  66. package/hooks/blocking/test_md_to_html_blocker_exemptions.py +368 -0
  67. package/hooks/blocking/test_md_to_html_blocker_extensions.py +157 -0
  68. package/hooks/blocking/test_md_to_html_blocker_path_resolution.py +336 -0
  69. package/hooks/blocking/test_pr_description_enforcer.py +13 -1499
  70. package/hooks/blocking/test_pr_description_enforcer_body_audit.py +247 -0
  71. package/hooks/blocking/test_pr_description_enforcer_body_rules.py +493 -0
  72. package/hooks/blocking/test_pr_description_enforcer_command_parser.py +366 -0
  73. package/hooks/blocking/test_pr_description_enforcer_pr_number.py +159 -0
  74. package/hooks/blocking/test_pr_description_enforcer_readability.py +443 -0
  75. package/hooks/blocking/test_tdd_enforcer.py +116 -0
  76. package/hooks/hooks_constants/blocking_check_limits.py +3 -0
  77. package/hooks/hooks_constants/code_rules_enforcer_constants.py +8 -0
  78. package/hooks/hooks_constants/pr_description_enforcer_constants.py +7 -0
  79. package/hooks/hooks_constants/sys_path_insert_constants.py +1 -0
  80. package/package.json +1 -1
  81. package/hooks/blocking/test_code_rules_enforcer.py +0 -2669
  82. package/hooks/blocking/test_md_to_html_blocker.py +0 -810
@@ -0,0 +1,148 @@
1
+ """Audit PR body markdown for prose substance, shape, and structural rules.
2
+
3
+ Strips Markdown ceremony to measure substantive prose, classifies the body as
4
+ trivial, standard, or heavy, enumerates section headers, prepares the prose
5
+ scanned for vague language, and flags self-closing references to the PR's own
6
+ number and the discouraged "This PR ..." opening. Vague-language enforcement
7
+ runs in validate_pr_body in pr_description_enforcer.py.
8
+ """
9
+
10
+ import re
11
+ import sys
12
+ from pathlib import Path
13
+
14
+ _hooks_dir = str(Path(__file__).resolve().parent.parent)
15
+ if _hooks_dir not in sys.path:
16
+ sys.path.insert(0, _hooks_dir)
17
+
18
+ from hooks_constants.pr_description_enforcer_constants import ( # noqa: E402
19
+ BLOCKQUOTE_LINE_PATTERN,
20
+ BLOCKQUOTE_MARKER_PATTERN,
21
+ BOLD_PAIR_PATTERN,
22
+ BULLET_MARKER_PATTERN,
23
+ FENCED_CODE_BLOCK_PATTERN,
24
+ HEADING_LINE_PATTERN,
25
+ HEAVY_MIN_BODY_CHARS_FOR_CLASSIFICATION,
26
+ HEAVY_SHAPE,
27
+ INLINE_CODE_PATTERN,
28
+ LINK_TEXT_PATTERN,
29
+ SELF_REFERENCE_PATTERN_TEMPLATE,
30
+ STANDARD_SHAPE,
31
+ TABLE_ROW_LINE_PATTERN,
32
+ THIS_PR_OPENING_PATTERN,
33
+ TRIVIAL_BODY_CHAR_THRESHOLD,
34
+ TRIVIAL_SHAPE,
35
+ WHITESPACE_RUN_PATTERN,
36
+ )
37
+
38
+
39
+ def strip_markdown_ceremony(body: str) -> str:
40
+ """Return the body with Markdown ceremony stripped to leave underlying prose.
41
+
42
+ Removes fenced code, inline code, heading lines, blockquote markers,
43
+ bullet list markers, bold/emphasis markers, and Markdown link targets.
44
+ Whitespace is preserved so callers can collapse or measure it as needed.
45
+ """
46
+ body_without_fences = FENCED_CODE_BLOCK_PATTERN.sub("", body)
47
+ body_without_inline_code = INLINE_CODE_PATTERN.sub("", body_without_fences)
48
+ body_without_blockquotes = BLOCKQUOTE_MARKER_PATTERN.sub("", body_without_inline_code)
49
+ body_without_headings = HEADING_LINE_PATTERN.sub("", body_without_blockquotes)
50
+ body_without_bullets = BULLET_MARKER_PATTERN.sub("", body_without_headings)
51
+ body_without_bold = BOLD_PAIR_PATTERN.sub(r"\1", body_without_bullets)
52
+ body_without_emphasis = body_without_bold.replace("*", "")
53
+ body_without_links = LINK_TEXT_PATTERN.sub(r"\1", body_without_emphasis)
54
+ return body_without_links
55
+
56
+
57
+ def _count_substantive_prose_chars(body: str) -> int:
58
+ """Return the count of prose characters after stripping Markdown ceremony.
59
+
60
+ Collapses internal whitespace so a body of only headers and bullets --
61
+ no real WHY paragraph -- registers as effectively empty.
62
+ """
63
+ stripped_body = strip_markdown_ceremony(body)
64
+ body_collapsed = WHITESPACE_RUN_PATTERN.sub(" ", stripped_body).strip()
65
+ return len(body_collapsed)
66
+
67
+
68
+ def _extract_vague_scan_text(body: str) -> str:
69
+ """Return the prose to scan for vague language, with non-prose regions removed.
70
+
71
+ Drops whole blockquote lines and whole pipe-delimited table rows, then strips
72
+ the same Markdown ceremony as the prose-count path -- which removes fenced
73
+ code, inline code, and whole heading lines. This exempts vague phrases that
74
+ appear only inside code fences, inline code, Markdown headings, quoted
75
+ reviewer text, or pipe-delimited example tables -- those are not the author's
76
+ own prose. A pipe-delimited row carries at least two pipes; a line with a
77
+ single leading pipe, or a borderless table row with no leading pipe, stays in
78
+ scope.
79
+ """
80
+ without_blockquote_lines = BLOCKQUOTE_LINE_PATTERN.sub("", body)
81
+ without_table_rows = TABLE_ROW_LINE_PATTERN.sub("", without_blockquote_lines)
82
+ return strip_markdown_ceremony(without_table_rows)
83
+
84
+
85
+ def _iter_section_headers(body: str) -> list[str]:
86
+ """Return every ATX heading line in the body, preserving canonical form.
87
+
88
+ HEADING_LINE_PATTERN matches the leading hash run (one or more hash
89
+ characters at line start), so the result spans every ATX level.
90
+ Downstream callers in this module only test specific two-hash header
91
+ strings, so matching every heading level keeps the parser permissive
92
+ without changing behaviour for the canonical two-hash header shape.
93
+
94
+ Fenced code blocks are stripped first so example markdown nested inside ``` fences
95
+ (a PR body that demonstrates the Heavy shape, for instance) is not counted as a
96
+ structural header. This keeps the shape classifier and Heavy required-header check
97
+ aligned with `strip_markdown_ceremony`, which already strips fences before measuring.
98
+ """
99
+ body_without_fences = FENCED_CODE_BLOCK_PATTERN.sub("", body)
100
+ all_headers: list[str] = []
101
+ for each_match in HEADING_LINE_PATTERN.finditer(body_without_fences):
102
+ header_text = each_match.group(0).strip()
103
+ all_headers.append(header_text)
104
+ return all_headers
105
+
106
+
107
+ def _compute_pr_body_shape(body: str) -> str:
108
+ """Classify a PR body as `trivial`, `standard`, or `heavy` from content alone.
109
+
110
+ Uses substantive prose chars (post-Markdown-strip) rather than raw length so the
111
+ classifier and the ceremony-on-Trivial check both measure the same metric against
112
+ TRIVIAL_BODY_CHAR_THRESHOLD; otherwise a body can be classified Standard by shape
113
+ while simultaneously being flagged as Trivial-sized by the ceremony check.
114
+ """
115
+ substantive_length = _count_substantive_prose_chars(body)
116
+ header_count = len(_iter_section_headers(body))
117
+
118
+ if substantive_length < TRIVIAL_BODY_CHAR_THRESHOLD and header_count == 0:
119
+ return TRIVIAL_SHAPE
120
+
121
+ if substantive_length >= HEAVY_MIN_BODY_CHARS_FOR_CLASSIFICATION:
122
+ return HEAVY_SHAPE
123
+
124
+ return STANDARD_SHAPE
125
+
126
+
127
+ def _body_contains_any_header(body: str, all_candidate_headers: frozenset[str]) -> bool:
128
+ body_headers_lower = {each_header.lower() for each_header in _iter_section_headers(body)}
129
+ for each_candidate in all_candidate_headers:
130
+ candidate_lower = each_candidate.lower()
131
+ for each_present in body_headers_lower:
132
+ if each_present == candidate_lower:
133
+ return True
134
+ if each_present.startswith(candidate_lower):
135
+ character_after_candidate = each_present[len(candidate_lower)]
136
+ if not (character_after_candidate.isalnum() or character_after_candidate == "_"):
137
+ return True
138
+ return False
139
+
140
+
141
+ def _matches_self_closing_reference(body: str, pr_number: int) -> bool:
142
+ pattern_source = SELF_REFERENCE_PATTERN_TEMPLATE.format(pr_number=pr_number)
143
+ compiled_pattern = re.compile(pattern_source, re.IGNORECASE)
144
+ return compiled_pattern.search(body) is not None
145
+
146
+
147
+ def _opens_with_this_pr_phrase(body: str) -> bool:
148
+ return THIS_PR_OPENING_PATTERN.search(body) is not None
@@ -0,0 +1,233 @@
1
+ """Parse gh pr create/edit/comment commands into auditable body content.
2
+
3
+ Tokenizes the captured shell command and extracts the PR body that should be
4
+ audited, resolving body-file paths and rejecting unauditable shell variables,
5
+ stdin sentinels, and path-traversal targets. Positional PR-number extraction
6
+ lives in pr_description_pr_number.py.
7
+ """
8
+
9
+ import shlex
10
+ import sys
11
+ from pathlib import Path
12
+
13
+ _hooks_dir = str(Path(__file__).resolve().parent.parent)
14
+ if _hooks_dir not in sys.path:
15
+ sys.path.insert(0, _hooks_dir)
16
+
17
+ from blocking._gh_body_arg_utils import ( # noqa: E402
18
+ all_body_flags,
19
+ body_file_flag,
20
+ body_file_short_flag,
21
+ count_extra_tokens_to_skip_for_split_quoted_value,
22
+ get_logical_first_line,
23
+ is_flag_shaped_token,
24
+ is_unresolvable_shell_value,
25
+ iter_significant_tokens,
26
+ match_body_file_equals_prefix,
27
+ match_body_flag_equals_prefix,
28
+ match_non_body_value_flag_equals_prefix,
29
+ non_body_value_flags,
30
+ strip_surrounding_quotes,
31
+ )
32
+ from hooks_constants.pr_description_enforcer_constants import ( # noqa: E402
33
+ BODY_FILE_STDIN_SENTINEL,
34
+ )
35
+ from hooks_constants.setup_project_paths_constants import UTF8_ENCODING # noqa: E402
36
+
37
+
38
+ class PathTraversalError(Exception):
39
+ pass
40
+
41
+
42
+ def _read_body_file_contents(file_path: str) -> str | None:
43
+ given_path = Path(file_path)
44
+ allowed_root = Path.cwd().resolve()
45
+ if given_path.is_symlink():
46
+ resolved_target = given_path.resolve()
47
+ try:
48
+ resolved_target.relative_to(allowed_root)
49
+ except ValueError:
50
+ raise PathTraversalError("symlink target resolves outside allowed root")
51
+ resolved_path = given_path.resolve()
52
+ if not given_path.is_absolute():
53
+ try:
54
+ resolved_path.relative_to(allowed_root)
55
+ except ValueError:
56
+ raise PathTraversalError("relative path resolves outside allowed root")
57
+ try:
58
+ with open(resolved_path, "r", encoding=UTF8_ENCODING, errors="replace") as body_file:
59
+ return body_file.read()
60
+ except (FileNotFoundError, IsADirectoryError, PermissionError, OSError):
61
+ return None
62
+
63
+
64
+ def _resolve_body_file_value(raw_value_token: str) -> str | None:
65
+ """Return file contents, or None when the body cannot be audited.
66
+
67
+ None means body is present but unauditable -- skip enforcement.
68
+ This covers: stdin sentinel, unresolvable shell variables, and path-traversal-rejected paths.
69
+ """
70
+ stripped_value = strip_surrounding_quotes(raw_value_token)
71
+ if not stripped_value:
72
+ return None
73
+ if stripped_value == BODY_FILE_STDIN_SENTINEL:
74
+ return None
75
+ if is_unresolvable_shell_value(stripped_value):
76
+ return None
77
+ try:
78
+ return _read_body_file_contents(stripped_value)
79
+ except PathTraversalError:
80
+ return None
81
+
82
+
83
+ def _resolve_body_string_value(raw_value_token: str) -> str | None:
84
+ """Return the literal body string, or None when the value is an
85
+ unresolvable shell variable.
86
+
87
+ Distinguishing the two cases lets `pr_description_enforcer.main()` skip enforcement only for
88
+ unauditable bodies; a literal `--body ""` still returns `""` and flows
89
+ into `validate_pr_body` so the substantive-prose check blocks it.
90
+ """
91
+ stripped_value = strip_surrounding_quotes(raw_value_token)
92
+ if is_unresolvable_shell_value(stripped_value):
93
+ return None
94
+ return stripped_value
95
+
96
+
97
+ def _reassemble_split_quoted_value(
98
+ first_value_token: str, all_remaining_tokens: list[str]
99
+ ) -> str | None:
100
+ extra_tokens_consumed = count_extra_tokens_to_skip_for_split_quoted_value(
101
+ all_remaining_tokens,
102
+ first_value_token,
103
+ )
104
+ if extra_tokens_consumed is None:
105
+ return None
106
+ if extra_tokens_consumed == 0:
107
+ return first_value_token
108
+ continuation_tokens = all_remaining_tokens[:extra_tokens_consumed]
109
+ return " ".join([first_value_token, *continuation_tokens])
110
+
111
+
112
+ def _scan_raw_tokens_for_body(all_raw_tokens: list[str]) -> str | None | bool:
113
+ """Return the body value from a raw token list, or False if no body flag found.
114
+
115
+ Returns False when no body/body-file flag is present (caller should continue).
116
+ Returns None when a body-file flag is present but malformed (no value
117
+ follows), OR when the body value is an unresolvable shell variable (e.g.
118
+ `--body "$VAR"`) — in either case the body is unauditable and the caller
119
+ skips enforcement.
120
+ Returns str for resolved body string values. An empty string `""` is a
121
+ literal-empty body (e.g. `--body ""`) and must still flow into
122
+ `validate_pr_body` so the substantive-prose check blocks it.
123
+ """
124
+ token_index = 0
125
+ while token_index < len(all_raw_tokens):
126
+ current_token = all_raw_tokens[token_index]
127
+ remaining_raw = all_raw_tokens[token_index + 1 :]
128
+ non_body_equals_prefix = match_non_body_value_flag_equals_prefix(current_token)
129
+ if non_body_equals_prefix is not None:
130
+ first_value_token = current_token[len(non_body_equals_prefix) :]
131
+ extra_skip = count_extra_tokens_to_skip_for_split_quoted_value(
132
+ remaining_raw, first_value_token
133
+ )
134
+ token_index += 1 + (extra_skip or 0)
135
+ continue
136
+ if current_token in non_body_value_flags:
137
+ if remaining_raw and not is_flag_shaped_token(remaining_raw[0]):
138
+ first_value_token = remaining_raw[0]
139
+ extra_skip = count_extra_tokens_to_skip_for_split_quoted_value(
140
+ remaining_raw[1:], first_value_token
141
+ )
142
+ token_index += 1 + 1 + (extra_skip or 0)
143
+ continue
144
+ token_index += 1
145
+ continue
146
+ body_equals_prefix = match_body_flag_equals_prefix(current_token)
147
+ if body_equals_prefix is not None:
148
+ first_value_token = current_token[len(body_equals_prefix) :]
149
+ full_value_token = _reassemble_split_quoted_value(first_value_token, remaining_raw)
150
+ if full_value_token is None:
151
+ return None
152
+ return _resolve_body_string_value(full_value_token)
153
+ body_file_equals_prefix = match_body_file_equals_prefix(current_token)
154
+ if body_file_equals_prefix is not None:
155
+ first_value_token = current_token[len(body_file_equals_prefix) :]
156
+ full_value_token = _reassemble_split_quoted_value(first_value_token, remaining_raw)
157
+ if full_value_token is None:
158
+ return None
159
+ return _resolve_body_file_value(full_value_token)
160
+ if current_token in all_body_flags:
161
+ if not remaining_raw or is_flag_shaped_token(remaining_raw[0]):
162
+ return None
163
+ first_value_token = remaining_raw[0]
164
+ full_value_token = _reassemble_split_quoted_value(first_value_token, remaining_raw[1:])
165
+ if full_value_token is None:
166
+ return None
167
+ return _resolve_body_string_value(full_value_token)
168
+ if current_token in {body_file_flag, body_file_short_flag}:
169
+ if not remaining_raw or is_flag_shaped_token(remaining_raw[0]):
170
+ return None
171
+ first_value_token = remaining_raw[0]
172
+ full_value_token = _reassemble_split_quoted_value(first_value_token, remaining_raw[1:])
173
+ if full_value_token is None:
174
+ return None
175
+ return _resolve_body_file_value(full_value_token)
176
+ token_index += 1
177
+ return False
178
+
179
+
180
+ def extract_body_from_command(
181
+ command: str,
182
+ all_pre_tokenized: tuple[str, list[str]] | None = None,
183
+ ) -> str | None:
184
+ """Return the PR body content for validation, or None if unextractable.
185
+
186
+ Uses iter_significant_tokens to skip values of non-body value-taking flags
187
+ so that --body/--body-file embedded in a quoted --title value never false-matches.
188
+ For space-form body-file flags, scans the raw token list directly because
189
+ iter_significant_tokens consumes the value token (yielding remaining-after-value).
190
+
191
+ If all_pre_tokenized is provided as (logical_line, raw_tokens), reuses those instead
192
+ of recomputing the logical line and shlex split a second time.
193
+ """
194
+ if all_pre_tokenized is not None:
195
+ logical_line, all_raw_tokens = all_pre_tokenized
196
+ else:
197
+ logical_line = get_logical_first_line(command)
198
+ if not logical_line:
199
+ return None
200
+ try:
201
+ all_raw_tokens = shlex.split(logical_line, posix=False)
202
+ except ValueError:
203
+ return None
204
+ try:
205
+ all_significant_tokens = list(
206
+ iter_significant_tokens(command, pre_tokenized=(logical_line, all_raw_tokens))
207
+ )
208
+ except ValueError:
209
+ return None
210
+
211
+ significant_token_set = {each_token for each_token, _ in all_significant_tokens}
212
+ body_flag_found_in_significant = (
213
+ any(each_token in all_body_flags for each_token in significant_token_set)
214
+ or any(
215
+ match_body_flag_equals_prefix(each_token) is not None
216
+ for each_token in significant_token_set
217
+ )
218
+ or any(
219
+ match_body_file_equals_prefix(each_token) is not None
220
+ for each_token in significant_token_set
221
+ )
222
+ or any(
223
+ each_token in {body_file_flag, body_file_short_flag}
224
+ for each_token in significant_token_set
225
+ )
226
+ )
227
+ if not body_flag_found_in_significant:
228
+ return None
229
+
230
+ scan_outcome = _scan_raw_tokens_for_body(all_raw_tokens)
231
+ if isinstance(scan_outcome, bool):
232
+ return None
233
+ return scan_outcome