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.
- 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_annotations_length.py +167 -0
- package/hooks/blocking/code_rules_banned_identifiers.py +385 -0
- package/hooks/blocking/code_rules_boolean_mustcheck.py +350 -0
- package/hooks/blocking/code_rules_comments.py +337 -0
- package/hooks/blocking/code_rules_constants_config.py +252 -0
- package/hooks/blocking/code_rules_docstrings.py +308 -0
- package/hooks/blocking/code_rules_enforcer.py +98 -5765
- package/hooks/blocking/code_rules_imports_logging.py +276 -0
- package/hooks/blocking/code_rules_magic_values.py +180 -0
- package/hooks/blocking/code_rules_mock_completeness.py +295 -0
- package/hooks/blocking/code_rules_naming_collection.py +264 -0
- package/hooks/blocking/code_rules_optional_params.py +288 -0
- package/hooks/blocking/code_rules_paths_syspath.py +186 -0
- package/hooks/blocking/code_rules_probe_chains.py +305 -0
- package/hooks/blocking/code_rules_probe_detection.py +257 -0
- package/hooks/blocking/code_rules_probe_recording.py +225 -0
- package/hooks/blocking/code_rules_scope_binding.py +151 -0
- package/hooks/blocking/code_rules_shared.py +301 -0
- package/hooks/blocking/code_rules_string_magic.py +207 -0
- package/hooks/blocking/code_rules_test_assertions.py +226 -0
- package/hooks/blocking/code_rules_test_branching_except.py +181 -0
- package/hooks/blocking/code_rules_test_isolation.py +341 -0
- package/hooks/blocking/code_rules_type_escape.py +341 -0
- package/hooks/blocking/code_rules_typeddict_stub.py +305 -0
- package/hooks/blocking/code_rules_unused_imports.py +256 -0
- package/hooks/blocking/conftest.py +30 -0
- 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/tdd_enforcer.py +31 -0
- package/hooks/blocking/test_code_rules_constants_config.py +26 -0
- package/hooks/blocking/test_code_rules_enforcer_banned_noun_word.py +5 -2
- package/hooks/blocking/test_code_rules_enforcer_cap_meta.py +0 -5
- package/hooks/blocking/test_code_rules_enforcer_comment_string_awareness.py +21 -15
- package/hooks/blocking/test_code_rules_enforcer_config_path.py +20 -16
- package/hooks/blocking/test_code_rules_enforcer_exempt_marker_chained.py +4 -2
- package/hooks/blocking/test_code_rules_enforcer_function_length.py +154 -18
- package/hooks/blocking/test_code_rules_enforcer_hardcoded_user_path.py +1 -2
- package/hooks/blocking/test_code_rules_enforcer_ignored_must_check_return.py +22 -12
- package/hooks/blocking/test_code_rules_enforcer_split_annotations_length.py +55 -0
- package/hooks/blocking/test_code_rules_enforcer_split_banned.py +170 -0
- package/hooks/blocking/test_code_rules_enforcer_split_comments.py +60 -0
- package/hooks/blocking/test_code_rules_enforcer_split_config_path.py +52 -0
- package/hooks/blocking/test_code_rules_enforcer_split_constants_config.py +236 -0
- package/hooks/blocking/test_code_rules_enforcer_split_entry_1.py +296 -0
- package/hooks/blocking/test_code_rules_enforcer_split_entry_2.py +238 -0
- package/hooks/blocking/test_code_rules_enforcer_split_isolation_1.py +271 -0
- package/hooks/blocking/test_code_rules_enforcer_split_isolation_2.py +283 -0
- package/hooks/blocking/test_code_rules_enforcer_split_isolation_3.py +268 -0
- package/hooks/blocking/test_code_rules_enforcer_split_isolation_4.py +85 -0
- package/hooks/blocking/test_code_rules_enforcer_split_mocks_1.py +303 -0
- package/hooks/blocking/test_code_rules_enforcer_split_mocks_2.py +111 -0
- package/hooks/blocking/test_code_rules_enforcer_split_mustcheck.py +87 -0
- package/hooks/blocking/test_code_rules_enforcer_split_naming.py +107 -0
- package/hooks/blocking/test_code_rules_enforcer_split_optional_params.py +325 -0
- package/hooks/blocking/test_code_rules_enforcer_split_paths_syspath.py +110 -0
- package/hooks/blocking/test_code_rules_enforcer_split_shared.py +44 -0
- package/hooks/blocking/test_code_rules_enforcer_split_string_magic.py +55 -0
- package/hooks/blocking/test_code_rules_enforcer_split_test_assertions.py +56 -0
- package/hooks/blocking/test_code_rules_enforcer_todo_markers.py +21 -15
- package/hooks/blocking/test_code_rules_paths_syspath.py +26 -0
- 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/blocking/test_tdd_enforcer.py +116 -0
- package/hooks/hooks_constants/blocking_check_limits.py +3 -0
- package/hooks/hooks_constants/code_rules_enforcer_constants.py +8 -0
- package/hooks/hooks_constants/pr_description_enforcer_constants.py +7 -0
- package/hooks/hooks_constants/sys_path_insert_constants.py +1 -0
- package/package.json +1 -1
- package/hooks/blocking/test_code_rules_enforcer.py +0 -2669
- package/hooks/blocking/test_md_to_html_blocker.py +0 -810
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
"""Shared gh body-arg parsing utilities for blocking hooks."""
|
|
1
|
+
"""Shared shell-token and gh body-arg parsing utilities for blocking hooks."""
|
|
2
2
|
|
|
3
3
|
from __future__ import annotations
|
|
4
4
|
|
|
@@ -55,6 +55,63 @@ _all_equals_prefixes_for_skip: tuple[str, ...] = tuple(
|
|
|
55
55
|
bash_continuation_marker: str = "\\"
|
|
56
56
|
powershell_continuation_marker: str = "`"
|
|
57
57
|
|
|
58
|
+
shell_variable_sigil: str = "$"
|
|
59
|
+
all_quote_characters: frozenset[str] = frozenset({'"', "'"})
|
|
60
|
+
minimum_meaningful_token_length: int = 2
|
|
61
|
+
|
|
62
|
+
non_body_value_flags: frozenset[str] = all_value_flags - {body_file_flag, body_file_short_flag}
|
|
63
|
+
|
|
64
|
+
_non_body_value_flag_equals_prefixes: tuple[str, ...] = tuple(
|
|
65
|
+
sorted((f"{each_flag}=" for each_flag in non_body_value_flags), key=len, reverse=True)
|
|
66
|
+
)
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
def is_flag_shaped_token(token: str) -> bool:
|
|
70
|
+
"""Report whether a token is flag-shaped for body/PR-number extraction.
|
|
71
|
+
|
|
72
|
+
Treats any token whose second character is "-" as flag-shaped, so bare
|
|
73
|
+
"--" and "--<digit>" tokens both count as flags. `_is_flag_shaped` applies
|
|
74
|
+
a stricter rule for token-stream scanning.
|
|
75
|
+
"""
|
|
76
|
+
if len(token) < minimum_meaningful_token_length:
|
|
77
|
+
return False
|
|
78
|
+
if not token.startswith("-"):
|
|
79
|
+
return False
|
|
80
|
+
return token[1] == "-" or token[1].isalpha()
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
def strip_surrounding_quotes(token: str) -> str:
|
|
84
|
+
if len(token) < minimum_meaningful_token_length:
|
|
85
|
+
return token
|
|
86
|
+
first_character = token[0]
|
|
87
|
+
last_character = token[-1]
|
|
88
|
+
if first_character in all_quote_characters and first_character == last_character:
|
|
89
|
+
return token[1:-1]
|
|
90
|
+
return token
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
def is_unresolvable_shell_value(token: str) -> bool:
|
|
94
|
+
return token.startswith(shell_variable_sigil)
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
def _match_prefix(token: str, all_prefixes: tuple[str, ...]) -> str | None:
|
|
98
|
+
for each_prefix in all_prefixes:
|
|
99
|
+
if token.startswith(each_prefix):
|
|
100
|
+
return each_prefix
|
|
101
|
+
return None
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
def match_body_flag_equals_prefix(token: str) -> str | None:
|
|
105
|
+
return _match_prefix(token, all_body_flag_prefixes)
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
def match_body_file_equals_prefix(token: str) -> str | None:
|
|
109
|
+
return _match_prefix(token, (body_file_flag_prefix, body_file_short_flag_prefix))
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
def match_non_body_value_flag_equals_prefix(token: str) -> str | None:
|
|
113
|
+
return _match_prefix(token, _non_body_value_flag_equals_prefixes)
|
|
114
|
+
|
|
58
115
|
|
|
59
116
|
def _count_trailing_run(text: str, marker_character: str) -> int:
|
|
60
117
|
trailing_run_length = 0
|
|
@@ -91,7 +148,13 @@ def get_logical_first_line(command: str) -> str:
|
|
|
91
148
|
|
|
92
149
|
|
|
93
150
|
def _is_flag_shaped(token: str) -> bool:
|
|
94
|
-
|
|
151
|
+
"""Report whether a token is flag-shaped for token-stream scanning.
|
|
152
|
+
|
|
153
|
+
Requires an alphabetic character after "--", so bare "--" and "--<digit>"
|
|
154
|
+
tokens are not flag-shaped. `is_flag_shaped_token` applies a looser rule
|
|
155
|
+
for body/PR-number extraction.
|
|
156
|
+
"""
|
|
157
|
+
if len(token) < minimum_meaningful_token_length:
|
|
95
158
|
return False
|
|
96
159
|
if not token.startswith("-"):
|
|
97
160
|
return False
|
|
@@ -102,7 +165,7 @@ def _is_flag_shaped(token: str) -> bool:
|
|
|
102
165
|
|
|
103
166
|
|
|
104
167
|
def _quoted_value_starts_split(value_token: str) -> bool:
|
|
105
|
-
if len(value_token) <
|
|
168
|
+
if len(value_token) < minimum_meaningful_token_length:
|
|
106
169
|
return False
|
|
107
170
|
first_character = value_token[0]
|
|
108
171
|
if first_character not in {'"', "'"}:
|
|
@@ -129,13 +192,6 @@ def count_extra_tokens_to_skip_for_split_quoted_value(
|
|
|
129
192
|
return None
|
|
130
193
|
|
|
131
194
|
|
|
132
|
-
def _match_equals_prefix_for_skip(token: str) -> str | None:
|
|
133
|
-
for each_prefix in _all_equals_prefixes_for_skip:
|
|
134
|
-
if token.startswith(each_prefix):
|
|
135
|
-
return each_prefix
|
|
136
|
-
return None
|
|
137
|
-
|
|
138
|
-
|
|
139
195
|
def iter_significant_tokens(
|
|
140
196
|
command: str,
|
|
141
197
|
pre_tokenized: tuple[str, list[str]] | None = None,
|
|
@@ -175,7 +231,7 @@ def iter_significant_tokens(
|
|
|
175
231
|
while token_index < len(all_tokens):
|
|
176
232
|
current_token = all_tokens[token_index]
|
|
177
233
|
remaining_tokens = all_tokens[token_index + 1:]
|
|
178
|
-
matched_equals_prefix =
|
|
234
|
+
matched_equals_prefix = _match_prefix(current_token, _all_equals_prefixes_for_skip)
|
|
179
235
|
if matched_equals_prefix is not None:
|
|
180
236
|
value_token = current_token[len(matched_equals_prefix):]
|
|
181
237
|
split_value_extra_tokens = count_extra_tokens_to_skip_for_split_quoted_value(
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
"""Shared subprocess-invocation helpers for the md_to_html_blocker test suites.
|
|
2
|
+
|
|
3
|
+
Subprocess CWD is rooted in a per-session sandbox created lazily so that
|
|
4
|
+
relative-path test cases canonicalize outside any `.claude-plugin/` ancestor,
|
|
5
|
+
outside the OS temp directory, and outside the exempt home-relative
|
|
6
|
+
subdirectories. The sandbox is a real repo root (it carries a `.git` marker) so
|
|
7
|
+
relative `README.md` / `CHANGELOG.md` writes exercise the repo-root exemption
|
|
8
|
+
path. This keeps the suites independent of where pytest itself is run.
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
import functools
|
|
12
|
+
import json
|
|
13
|
+
import os
|
|
14
|
+
import shutil
|
|
15
|
+
import stat
|
|
16
|
+
import subprocess
|
|
17
|
+
import sys
|
|
18
|
+
import tempfile
|
|
19
|
+
from pathlib import Path
|
|
20
|
+
|
|
21
|
+
HOOK_SCRIPT_PATH = os.path.join(os.path.dirname(__file__), "md_to_html_blocker.py")
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def _strip_read_only_and_retry(removal_function, target_path, *_exc_info):
|
|
25
|
+
try:
|
|
26
|
+
os.chmod(target_path, stat.S_IWRITE)
|
|
27
|
+
removal_function(target_path)
|
|
28
|
+
except OSError:
|
|
29
|
+
pass
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def _force_rmtree(target_path: str) -> None:
|
|
33
|
+
handler_kw = (
|
|
34
|
+
{"onexc": _strip_read_only_and_retry}
|
|
35
|
+
if sys.version_info >= (3, 12)
|
|
36
|
+
else {"onerror": _strip_read_only_and_retry}
|
|
37
|
+
)
|
|
38
|
+
try:
|
|
39
|
+
shutil.rmtree(target_path, **handler_kw)
|
|
40
|
+
except OSError:
|
|
41
|
+
pass
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
@functools.lru_cache(maxsize=1)
|
|
45
|
+
def _get_sandbox_parent_directory() -> str:
|
|
46
|
+
sandbox_parent = tempfile.mkdtemp(prefix="pytest_md_blocker_", dir=str(Path.home()))
|
|
47
|
+
git_marker_path = os.path.join(sandbox_parent, ".git")
|
|
48
|
+
Path(git_marker_path).touch()
|
|
49
|
+
return sandbox_parent
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
class _RunHook:
|
|
53
|
+
def __call__(self, tool_name: str, tool_input: dict) -> subprocess.CompletedProcess:
|
|
54
|
+
payload = json.dumps({"tool_name": tool_name, "tool_input": tool_input})
|
|
55
|
+
return subprocess.run(
|
|
56
|
+
[sys.executable, HOOK_SCRIPT_PATH],
|
|
57
|
+
input=payload,
|
|
58
|
+
capture_output=True,
|
|
59
|
+
text=True,
|
|
60
|
+
check=False,
|
|
61
|
+
cwd=_get_sandbox_parent_directory(),
|
|
62
|
+
)
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
_run_hook = _RunHook()
|
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
"""Parameter-annotation, return-annotation, and function-length checks."""
|
|
2
|
+
|
|
3
|
+
import ast
|
|
4
|
+
import sys
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
|
|
7
|
+
_blocking_directory = str(Path(__file__).resolve().parent)
|
|
8
|
+
_hooks_directory = str(Path(__file__).resolve().parent.parent)
|
|
9
|
+
if _blocking_directory not in sys.path:
|
|
10
|
+
sys.path.insert(0, _blocking_directory)
|
|
11
|
+
if _hooks_directory not in sys.path:
|
|
12
|
+
sys.path.insert(0, _hooks_directory)
|
|
13
|
+
|
|
14
|
+
from code_rules_shared import ( # noqa: E402
|
|
15
|
+
_collect_annotated_arguments,
|
|
16
|
+
_definition_docstring_line_span,
|
|
17
|
+
_function_definition_line_span,
|
|
18
|
+
_scope_violations_to_changed_lines,
|
|
19
|
+
is_hook_infrastructure,
|
|
20
|
+
is_migration_file,
|
|
21
|
+
is_test_file,
|
|
22
|
+
is_workflow_registry_file,
|
|
23
|
+
)
|
|
24
|
+
|
|
25
|
+
from hooks_constants.code_rules_enforcer_constants import ( # noqa: E402
|
|
26
|
+
ALL_SELF_AND_CLS_PARAMETER_NAMES,
|
|
27
|
+
FUNCTION_LENGTH_BLOCKING_MESSAGE_SUFFIX,
|
|
28
|
+
FUNCTION_LENGTH_BLOCKING_THRESHOLD,
|
|
29
|
+
)
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def check_parameter_annotations(content: str, file_path: str) -> list[str]:
|
|
33
|
+
if is_test_file(file_path):
|
|
34
|
+
return []
|
|
35
|
+
if is_workflow_registry_file(file_path) or is_migration_file(file_path):
|
|
36
|
+
return []
|
|
37
|
+
try:
|
|
38
|
+
tree = ast.parse(content)
|
|
39
|
+
except SyntaxError:
|
|
40
|
+
return []
|
|
41
|
+
issues: list[str] = []
|
|
42
|
+
for each_node in ast.walk(tree):
|
|
43
|
+
if not isinstance(each_node, (ast.FunctionDef, ast.AsyncFunctionDef)):
|
|
44
|
+
continue
|
|
45
|
+
for each_arg in _collect_annotated_arguments(each_node):
|
|
46
|
+
if each_arg.arg in ALL_SELF_AND_CLS_PARAMETER_NAMES:
|
|
47
|
+
continue
|
|
48
|
+
if each_arg.annotation is None:
|
|
49
|
+
issues.append(
|
|
50
|
+
f"Line {each_arg.lineno}: parameter {each_arg.arg!r} on {each_node.name!r} missing type annotation (CODE_RULES §6)"
|
|
51
|
+
)
|
|
52
|
+
return issues
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def check_return_annotations(content: str, file_path: str) -> list[str]:
|
|
56
|
+
if is_test_file(file_path):
|
|
57
|
+
return []
|
|
58
|
+
if is_workflow_registry_file(file_path) or is_migration_file(file_path):
|
|
59
|
+
return []
|
|
60
|
+
try:
|
|
61
|
+
tree = ast.parse(content)
|
|
62
|
+
except SyntaxError:
|
|
63
|
+
return []
|
|
64
|
+
issues: list[str] = []
|
|
65
|
+
for each_node in ast.walk(tree):
|
|
66
|
+
if not isinstance(each_node, (ast.FunctionDef, ast.AsyncFunctionDef)):
|
|
67
|
+
continue
|
|
68
|
+
if each_node.returns is None:
|
|
69
|
+
issues.append(
|
|
70
|
+
f"Line {each_node.lineno}: function {each_node.name!r} missing return type annotation (CODE_RULES §6)"
|
|
71
|
+
)
|
|
72
|
+
return issues
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
def check_function_length(
|
|
76
|
+
content: str,
|
|
77
|
+
file_path: str,
|
|
78
|
+
all_changed_lines: set[int] | None = None,
|
|
79
|
+
defer_scope_to_caller: bool = False,
|
|
80
|
+
) -> list[str]:
|
|
81
|
+
"""Flag functions whose executable span exceeds cognitive-load thresholds.
|
|
82
|
+
|
|
83
|
+
Function executable spans — the definition span (signature line through
|
|
84
|
+
last body statement, inclusive) minus the leading docstring lines of the
|
|
85
|
+
function and of every function or class nested within it, per
|
|
86
|
+
``_definition_docstring_line_span`` summed over the nested definitions —
|
|
87
|
+
at or above ``FUNCTION_LENGTH_BLOCKING_THRESHOLD`` appear in
|
|
88
|
+
the returned issues list and block the write at the
|
|
89
|
+
gate. The threshold rests on the small-function guidance in Robert C.
|
|
90
|
+
Martin, *Clean Code* Chapter Three ("Functions") and the Google Python Style
|
|
91
|
+
Guide's ~forty-line function review hint
|
|
92
|
+
(https://google.github.io/styleguide/pyguide.html) — a measure of
|
|
93
|
+
executable complexity, paired with the Guide's complete-docstring mandate
|
|
94
|
+
for public APIs, so documentation lines never count against the gate; this
|
|
95
|
+
gate blocks on body growth that pushes a function past that span. It does
|
|
96
|
+
not derive from CODE_RULES file-length guidance, which governs advisory
|
|
97
|
+
file-length signals and argues against hard numeric blocks.
|
|
98
|
+
|
|
99
|
+
The issue message carries ``Function NAME (defined at line X) is Y lines``
|
|
100
|
+
precisely so the gate's ``function_length_span_range`` can recover the
|
|
101
|
+
function's full declared span (lines ``X`` through ``X + Y - 1``). The
|
|
102
|
+
gate classifies the violation blocking when that span intersects the
|
|
103
|
+
diff's added lines — the body grew this diff — and advisory otherwise — a
|
|
104
|
+
pre-existing, untouched long function in a file the diff happened to
|
|
105
|
+
touch. Anchoring to the span rather than a single ``Line N:`` definition
|
|
106
|
+
line lets body growth on any interior line block correctly even when the
|
|
107
|
+
``def`` line itself is untouched.
|
|
108
|
+
|
|
109
|
+
Exempt: test files (test bodies are sometimes long by necessity), Django
|
|
110
|
+
migrations (auto-generated), workflow registries (registry entries), and
|
|
111
|
+
hook infrastructure.
|
|
112
|
+
|
|
113
|
+
Args:
|
|
114
|
+
content: The Python source to analyze.
|
|
115
|
+
file_path: The path of the file being checked.
|
|
116
|
+
all_changed_lines: Post-edit line numbers the current edit touched, or
|
|
117
|
+
None to treat the whole file as in scope. When provided, a violation
|
|
118
|
+
blocks only when the function's declared span intersects the changed
|
|
119
|
+
lines.
|
|
120
|
+
defer_scope_to_caller: When True, return every violation so the
|
|
121
|
+
commit/push gate's ``split_violations_by_scope`` can scope by added
|
|
122
|
+
line and report the in-scope set.
|
|
123
|
+
|
|
124
|
+
Returns:
|
|
125
|
+
Blocking issues. When *defer_scope_to_caller* is True every violation is
|
|
126
|
+
returned for the gate to scope; otherwise every violation in scope is
|
|
127
|
+
returned.
|
|
128
|
+
"""
|
|
129
|
+
if is_test_file(file_path):
|
|
130
|
+
return []
|
|
131
|
+
if is_hook_infrastructure(file_path):
|
|
132
|
+
return []
|
|
133
|
+
if is_workflow_registry_file(file_path) or is_migration_file(file_path):
|
|
134
|
+
return []
|
|
135
|
+
|
|
136
|
+
try:
|
|
137
|
+
parsed_tree = ast.parse(content)
|
|
138
|
+
except SyntaxError:
|
|
139
|
+
return []
|
|
140
|
+
|
|
141
|
+
all_violations_in_walk_order: list[tuple[range, str]] = []
|
|
142
|
+
for each_node in ast.walk(parsed_tree):
|
|
143
|
+
if not isinstance(each_node, (ast.FunctionDef, ast.AsyncFunctionDef)):
|
|
144
|
+
continue
|
|
145
|
+
line_span = _function_definition_line_span(each_node)
|
|
146
|
+
if line_span < FUNCTION_LENGTH_BLOCKING_THRESHOLD:
|
|
147
|
+
continue
|
|
148
|
+
docstring_line_total = sum(
|
|
149
|
+
_definition_docstring_line_span(each_definition)
|
|
150
|
+
for each_definition in ast.walk(each_node)
|
|
151
|
+
if isinstance(
|
|
152
|
+
each_definition, (ast.FunctionDef, ast.AsyncFunctionDef, ast.ClassDef)
|
|
153
|
+
)
|
|
154
|
+
)
|
|
155
|
+
executable_line_span = line_span - docstring_line_total
|
|
156
|
+
if executable_line_span >= FUNCTION_LENGTH_BLOCKING_THRESHOLD:
|
|
157
|
+
span_range = range(each_node.lineno, each_node.lineno + line_span)
|
|
158
|
+
message = (
|
|
159
|
+
f"Function {each_node.name!r} (defined at line {each_node.lineno}) "
|
|
160
|
+
f"is {line_span} lines - {FUNCTION_LENGTH_BLOCKING_MESSAGE_SUFFIX}"
|
|
161
|
+
)
|
|
162
|
+
all_violations_in_walk_order.append((span_range, message))
|
|
163
|
+
return _scope_violations_to_changed_lines(
|
|
164
|
+
all_violations_in_walk_order,
|
|
165
|
+
all_changed_lines,
|
|
166
|
+
defer_scope_to_caller,
|
|
167
|
+
)
|