claude-dev-env 1.49.1 → 1.50.1
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.
- package/audit-rubrics/category_rubrics/category-a-api-contracts.md +17 -3
- package/audit-rubrics/prompts/category-a-api-contracts.md +17 -2
- package/docs/CODE_RULES.md +6 -1
- package/hooks/blocking/_gh_body_arg_utils.py +67 -11
- package/hooks/blocking/_md_to_html_blocker_test_support.py +65 -0
- package/hooks/blocking/code_rules_enforcer.py +386 -32
- package/hooks/blocking/conftest.py +30 -0
- package/hooks/blocking/md_to_html_blocker.py +2 -2
- package/hooks/blocking/pr_description_body_audit.py +148 -0
- package/hooks/blocking/pr_description_command_parser.py +233 -0
- package/hooks/blocking/pr_description_enforcer.py +36 -825
- package/hooks/blocking/pr_description_pr_number.py +153 -0
- package/hooks/blocking/pr_description_readability.py +366 -0
- package/hooks/blocking/test_code_rules_enforcer.py +65 -0
- package/hooks/blocking/test_code_rules_enforcer_docstring_args_signature.py +256 -0
- package/hooks/blocking/test_code_rules_enforcer_function_length.py +136 -5
- package/hooks/blocking/test_code_rules_enforcer_ignored_must_check_return.py +256 -0
- package/hooks/blocking/test_code_rules_enforcer_naming_pattern.py +137 -1
- package/hooks/blocking/test_md_to_html_blocker_exemptions.py +368 -0
- package/hooks/blocking/test_md_to_html_blocker_extensions.py +157 -0
- package/hooks/blocking/test_md_to_html_blocker_path_resolution.py +336 -0
- package/hooks/blocking/test_pr_description_enforcer.py +13 -1499
- package/hooks/blocking/test_pr_description_enforcer_body_audit.py +247 -0
- package/hooks/blocking/test_pr_description_enforcer_body_rules.py +493 -0
- package/hooks/blocking/test_pr_description_enforcer_command_parser.py +366 -0
- package/hooks/blocking/test_pr_description_enforcer_pr_number.py +159 -0
- package/hooks/blocking/test_pr_description_enforcer_readability.py +443 -0
- package/hooks/hooks_constants/blocking_check_limits.py +2 -0
- package/hooks/hooks_constants/code_rules_enforcer_constants.py +15 -1
- package/hooks/hooks_constants/md_to_html_blocker_constants.py +1 -1
- package/hooks/hooks_constants/pr_description_enforcer_constants.py +7 -0
- package/hooks/hooks_constants/test_md_to_html_blocker_constants.py +11 -4
- package/package.json +1 -1
- package/hooks/blocking/test_md_to_html_blocker.py +0 -772
|
@@ -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
|