claude-dev-env 2.35.0 → 2.36.0
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/.agents/skills/autoconverge/SKILL.md +19 -15
- package/.agents/skills/autoconverge/reference/gotchas.md +3 -3
- package/.agents/skills/autoconverge/reference/multi-pr.md +6 -2
- package/.agents/skills/autoconverge/workflow/converge.contract.test.mjs +11 -0
- package/.agents/skills/autoconverge/workflow/converge.mjs +26 -33
- package/.agents/skills/autoconverge/workflow/converge.run-input.test.mjs +26 -2
- package/.agents/skills/autoconverge/workflow/converge_multi.mjs +12 -28
- package/.agents/skills/autoconverge/workflow/converge_multi.run-input.test.mjs +16 -0
- package/bin/cde.mjs +175 -0
- package/bin/cde.test.mjs +222 -0
- package/bin/install-constants.mjs +4 -0
- package/bin/install.cursor-rules.test.mjs +233 -2
- package/bin/install.mjs +49 -3
- package/bin/pstack-model-preferences.codex.json +23 -0
- package/bin/select-pstack-models.test.mjs +378 -0
- package/hooks/hooks_constants/tool_names.py +20 -0
- package/hooks/validators/fast_save_validators.py +63 -89
- package/hooks/validators/run_all_validators.py +16 -8
- package/hooks/validators/test_fast_save_validators_policy_lint.py +25 -0
- package/hooks/validators/test_run_all_validators_policy_lint.py +52 -0
- package/package.json +3 -2
- package/rules/pstack-models.md +119 -0
- package/scripts/cde_lint.py +258 -0
- package/scripts/policy_lint/__init__.py +41 -0
- package/scripts/policy_lint/adapter_configuration.py +91 -0
- package/scripts/policy_lint/adapter_detectors.py +328 -0
- package/scripts/policy_lint/adapter_pairing.py +145 -0
- package/scripts/policy_lint/adapter_support.py +97 -0
- package/scripts/policy_lint/adapters.py +352 -0
- package/scripts/policy_lint/config/__init__.py +1 -0
- package/scripts/policy_lint/config/constants.py +66 -0
- package/scripts/policy_lint/engine.py +224 -0
- package/scripts/policy_lint/model.py +387 -0
- package/scripts/policy_lint/registry.py +162 -0
- package/scripts/policy_lint/render.py +91 -0
- package/scripts/policy_lint/selection.py +372 -0
- package/scripts/policy_lint/selection_git.py +147 -0
- package/scripts/select_pstack_models.mjs +304 -0
- package/scripts/tests/test_cde_lint.py +374 -0
- package/scripts/tests/test_policy_lint_rules_engine.py +381 -0
- package/scripts/tests/test_policy_lint_rules_registry.py +252 -0
- package/scripts/tests/test_policy_lint_selection.py +373 -0
|
@@ -0,0 +1,352 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import importlib
|
|
4
|
+
import sys
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
from types import ModuleType
|
|
7
|
+
|
|
8
|
+
from . import adapter_configuration, adapter_detectors, adapter_pairing, adapter_support
|
|
9
|
+
from .config import constants
|
|
10
|
+
from .model import Diagnostic, Document, DocumentSet
|
|
11
|
+
|
|
12
|
+
_document_path = adapter_support._document_path
|
|
13
|
+
_line_number = adapter_support._line_number
|
|
14
|
+
_diagnostics_for_messages = adapter_support._diagnostics_for_messages
|
|
15
|
+
_source_line_for_phrase = adapter_support._source_line_for_phrase
|
|
16
|
+
_diagnostics_for_state_messages = adapter_support._diagnostics_for_state_messages
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def _hooks_module(module_name: str) -> ModuleType:
|
|
20
|
+
hooks_directory = str(Path(__file__).resolve().parents[1].parent / "hooks")
|
|
21
|
+
if hooks_directory not in sys.path:
|
|
22
|
+
sys.path.insert(0, hooks_directory)
|
|
23
|
+
return importlib.import_module(module_name)
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def validator_diagnostics(
|
|
27
|
+
document: Document, repository_root: Path
|
|
28
|
+
) -> tuple[Diagnostic, ...]:
|
|
29
|
+
"""Run the fast validator set on one document.
|
|
30
|
+
|
|
31
|
+
Args:
|
|
32
|
+
document: Current source text and path.
|
|
33
|
+
repository_root: Request repository root for configuration resolution.
|
|
34
|
+
|
|
35
|
+
Returns:
|
|
36
|
+
Path-normalized diagnostics for failed validators.
|
|
37
|
+
"""
|
|
38
|
+
return adapter_detectors.validator_diagnostics(
|
|
39
|
+
document, repository_root, _hooks_module
|
|
40
|
+
)
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def code_rule_diagnostics(
|
|
44
|
+
document: Document, repository_root: Path
|
|
45
|
+
) -> tuple[Diagnostic, ...]:
|
|
46
|
+
"""Run the code-rule engine on one document.
|
|
47
|
+
|
|
48
|
+
Args:
|
|
49
|
+
document: Current text and optional prior text.
|
|
50
|
+
repository_root: Request repository root for sibling-file resolution.
|
|
51
|
+
|
|
52
|
+
Returns:
|
|
53
|
+
Diagnostics from the code-rule engine.
|
|
54
|
+
"""
|
|
55
|
+
return adapter_detectors.code_rule_diagnostics(
|
|
56
|
+
document, repository_root, _hooks_module
|
|
57
|
+
)
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def state_description_diagnostics(
|
|
61
|
+
document: Document, repository_root: Path
|
|
62
|
+
) -> tuple[Diagnostic, ...]:
|
|
63
|
+
"""Run the state-description detector on one document.
|
|
64
|
+
|
|
65
|
+
Args:
|
|
66
|
+
document: Current document text.
|
|
67
|
+
repository_root: Request repository root for document-path resolution.
|
|
68
|
+
|
|
69
|
+
Returns:
|
|
70
|
+
State-description diagnostics.
|
|
71
|
+
"""
|
|
72
|
+
return adapter_detectors.state_description_diagnostics(
|
|
73
|
+
document, repository_root, _hooks_module
|
|
74
|
+
)
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
def plain_language_diagnostics(
|
|
78
|
+
document: Document, repository_root: Path
|
|
79
|
+
) -> tuple[Diagnostic, ...]:
|
|
80
|
+
"""Run the stored-prose detector on one document.
|
|
81
|
+
|
|
82
|
+
Args:
|
|
83
|
+
document: Current document text.
|
|
84
|
+
repository_root: Request repository root for document-path resolution.
|
|
85
|
+
|
|
86
|
+
Returns:
|
|
87
|
+
Plain-language diagnostics.
|
|
88
|
+
"""
|
|
89
|
+
return adapter_detectors.plain_language_diagnostics(
|
|
90
|
+
document, repository_root, _hooks_module
|
|
91
|
+
)
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
def docstring_gate_diagnostics(
|
|
95
|
+
document: Document, repository_root: Path
|
|
96
|
+
) -> tuple[Diagnostic, ...]:
|
|
97
|
+
"""Run the docstring gate-count detector on one document.
|
|
98
|
+
|
|
99
|
+
Args:
|
|
100
|
+
document: Current document text.
|
|
101
|
+
repository_root: Request repository root for document-path resolution.
|
|
102
|
+
|
|
103
|
+
Returns:
|
|
104
|
+
Docstring gate diagnostics.
|
|
105
|
+
"""
|
|
106
|
+
return adapter_detectors.docstring_gate_diagnostics(
|
|
107
|
+
document, repository_root, _hooks_module
|
|
108
|
+
)
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
def workflow_substitution_diagnostics(
|
|
112
|
+
document: Document, repository_root: Path
|
|
113
|
+
) -> tuple[Diagnostic, ...]:
|
|
114
|
+
"""Run the workflow substitution detector on one document.
|
|
115
|
+
|
|
116
|
+
Args:
|
|
117
|
+
document: Current document text.
|
|
118
|
+
repository_root: Request repository root for document-path resolution.
|
|
119
|
+
|
|
120
|
+
Returns:
|
|
121
|
+
Workflow substitution diagnostics.
|
|
122
|
+
"""
|
|
123
|
+
return adapter_detectors.workflow_substitution_diagnostics(
|
|
124
|
+
document, repository_root, _hooks_module
|
|
125
|
+
)
|
|
126
|
+
|
|
127
|
+
|
|
128
|
+
def rmtree_diagnostics(
|
|
129
|
+
document: Document, repository_root: Path
|
|
130
|
+
) -> tuple[Diagnostic, ...]:
|
|
131
|
+
"""Run the Windows cleanup detectors on one document.
|
|
132
|
+
|
|
133
|
+
Args:
|
|
134
|
+
document: Current text.
|
|
135
|
+
repository_root: Request repository root for document-path resolution.
|
|
136
|
+
|
|
137
|
+
Returns:
|
|
138
|
+
Cleanup safety diagnostics.
|
|
139
|
+
"""
|
|
140
|
+
return adapter_detectors.rmtree_diagnostics(
|
|
141
|
+
document, repository_root, _hooks_module
|
|
142
|
+
)
|
|
143
|
+
|
|
144
|
+
|
|
145
|
+
def subprocess_budget_diagnostics(
|
|
146
|
+
document: Document, repository_root: Path
|
|
147
|
+
) -> tuple[Diagnostic, ...]:
|
|
148
|
+
"""Run the subprocess budget detector on one document.
|
|
149
|
+
|
|
150
|
+
Args:
|
|
151
|
+
document: Current text.
|
|
152
|
+
repository_root: Request repository root for document-path resolution.
|
|
153
|
+
|
|
154
|
+
Returns:
|
|
155
|
+
Subprocess budget diagnostics.
|
|
156
|
+
"""
|
|
157
|
+
return adapter_detectors.subprocess_budget_diagnostics(
|
|
158
|
+
document, repository_root, _hooks_module
|
|
159
|
+
)
|
|
160
|
+
|
|
161
|
+
|
|
162
|
+
def hook_prose_diagnostics(
|
|
163
|
+
document: Document, repository_root: Path
|
|
164
|
+
) -> tuple[Diagnostic, ...]:
|
|
165
|
+
"""Run the hook-prose consistency detector on one document.
|
|
166
|
+
|
|
167
|
+
Args:
|
|
168
|
+
document: Current text.
|
|
169
|
+
repository_root: Request repository root for document-path resolution.
|
|
170
|
+
|
|
171
|
+
Returns:
|
|
172
|
+
Hook-prose diagnostics.
|
|
173
|
+
"""
|
|
174
|
+
return adapter_detectors.hook_prose_diagnostics(
|
|
175
|
+
document, repository_root, _hooks_module
|
|
176
|
+
)
|
|
177
|
+
|
|
178
|
+
|
|
179
|
+
def open_question_diagnostics(
|
|
180
|
+
document: Document, repository_root: Path
|
|
181
|
+
) -> tuple[Diagnostic, ...]:
|
|
182
|
+
"""Run the open-question plan detector on one document.
|
|
183
|
+
|
|
184
|
+
Args:
|
|
185
|
+
document: Current plan text.
|
|
186
|
+
repository_root: Request repository root for document-path resolution.
|
|
187
|
+
|
|
188
|
+
Returns:
|
|
189
|
+
Plan question diagnostics.
|
|
190
|
+
"""
|
|
191
|
+
return adapter_detectors.open_question_diagnostics(
|
|
192
|
+
document, repository_root, _hooks_module
|
|
193
|
+
)
|
|
194
|
+
|
|
195
|
+
|
|
196
|
+
def hook_configuration_diagnostics(
|
|
197
|
+
document: Document, repository_root: Path
|
|
198
|
+
) -> tuple[Diagnostic, ...]:
|
|
199
|
+
"""Check hook configuration for policy decision fields.
|
|
200
|
+
|
|
201
|
+
Args:
|
|
202
|
+
document: Current hook configuration text.
|
|
203
|
+
repository_root: Request repository root for document-path resolution.
|
|
204
|
+
|
|
205
|
+
Returns:
|
|
206
|
+
Hook configuration diagnostics.
|
|
207
|
+
"""
|
|
208
|
+
return adapter_configuration.hook_configuration_diagnostics(
|
|
209
|
+
document, repository_root
|
|
210
|
+
)
|
|
211
|
+
|
|
212
|
+
|
|
213
|
+
def test_pairing_diagnostics(document_set: DocumentSet) -> tuple[Diagnostic, ...]:
|
|
214
|
+
"""Report changed production files without a changed matching test.
|
|
215
|
+
|
|
216
|
+
Args:
|
|
217
|
+
document_set: Staged or base change selection.
|
|
218
|
+
|
|
219
|
+
Returns:
|
|
220
|
+
Test-pairing diagnostics for unmatched production files.
|
|
221
|
+
"""
|
|
222
|
+
return adapter_pairing.test_pairing_diagnostics(document_set, _hooks_module)
|
|
223
|
+
|
|
224
|
+
|
|
225
|
+
def hook_format_diagnostics(
|
|
226
|
+
document: Document, repository_root: Path
|
|
227
|
+
) -> tuple[Diagnostic, ...]:
|
|
228
|
+
"""Check one settings document for a nonportable hook command.
|
|
229
|
+
|
|
230
|
+
Args:
|
|
231
|
+
document: Current settings text and path.
|
|
232
|
+
repository_root: Request repository root for document-path resolution.
|
|
233
|
+
|
|
234
|
+
Returns:
|
|
235
|
+
A diagnostic when the command uses a home-relative Python script path.
|
|
236
|
+
"""
|
|
237
|
+
return adapter_detectors.hook_format_diagnostics(
|
|
238
|
+
document, repository_root, _hooks_module
|
|
239
|
+
)
|
|
240
|
+
|
|
241
|
+
|
|
242
|
+
def accepts_python(document: Document) -> bool:
|
|
243
|
+
return document.path.suffix.lower() == constants.PYTHON_SUFFIX
|
|
244
|
+
|
|
245
|
+
|
|
246
|
+
def accepts_code(document: Document) -> bool:
|
|
247
|
+
"""Return whether the document contains supported source code.
|
|
248
|
+
|
|
249
|
+
Args:
|
|
250
|
+
document: Candidate document.
|
|
251
|
+
|
|
252
|
+
Returns:
|
|
253
|
+
True for a supported source extension.
|
|
254
|
+
"""
|
|
255
|
+
return document.path.suffix.lower() in constants.ALL_CODE_SUFFIXES
|
|
256
|
+
|
|
257
|
+
|
|
258
|
+
def accepts_markdown(document: Document) -> bool:
|
|
259
|
+
return document.path.suffix.lower() in constants.ALL_MARKDOWN_SUFFIXES
|
|
260
|
+
|
|
261
|
+
|
|
262
|
+
def accepts_source_or_markdown(document: Document) -> bool:
|
|
263
|
+
"""Return whether a state-description rule can inspect the document.
|
|
264
|
+
|
|
265
|
+
Args:
|
|
266
|
+
document: Candidate document.
|
|
267
|
+
|
|
268
|
+
Returns:
|
|
269
|
+
True for supported source or Markdown.
|
|
270
|
+
"""
|
|
271
|
+
return accepts_code(document) or accepts_markdown(document)
|
|
272
|
+
|
|
273
|
+
|
|
274
|
+
def accepts_stored_prompt(document: Document) -> bool:
|
|
275
|
+
"""Return whether the document is stored prompt prose.
|
|
276
|
+
|
|
277
|
+
Args:
|
|
278
|
+
document: Candidate document.
|
|
279
|
+
|
|
280
|
+
Returns:
|
|
281
|
+
True for Markdown in an instruction directory.
|
|
282
|
+
"""
|
|
283
|
+
normalized_path = f"/{document.path.as_posix().lower()}"
|
|
284
|
+
return accepts_markdown(document) and any(
|
|
285
|
+
each_segment in normalized_path
|
|
286
|
+
for each_segment in constants.ALL_STORED_PROMPT_SEGMENTS
|
|
287
|
+
)
|
|
288
|
+
|
|
289
|
+
|
|
290
|
+
def accepts_hook_configuration(document: Document) -> bool:
|
|
291
|
+
return document.path.name == "hooks.json"
|
|
292
|
+
|
|
293
|
+
|
|
294
|
+
def accepts_hook_format(document: Document) -> bool:
|
|
295
|
+
"""Return whether the document is Claude settings JSON.
|
|
296
|
+
|
|
297
|
+
Args:
|
|
298
|
+
document: Candidate document.
|
|
299
|
+
|
|
300
|
+
Returns:
|
|
301
|
+
True for ``settings.json`` under a ``.claude`` directory.
|
|
302
|
+
"""
|
|
303
|
+
settings_file_name = "settings.json"
|
|
304
|
+
claude_directory_segment = "/.claude/"
|
|
305
|
+
normalized_path = f"/{document.path.as_posix().lower()}"
|
|
306
|
+
return (
|
|
307
|
+
document.path.name.lower() == settings_file_name
|
|
308
|
+
and claude_directory_segment in normalized_path
|
|
309
|
+
)
|
|
310
|
+
|
|
311
|
+
|
|
312
|
+
def accepts_workflow(document: Document) -> bool:
|
|
313
|
+
"""Return whether the document is a workflow substitution template.
|
|
314
|
+
|
|
315
|
+
Args:
|
|
316
|
+
document: Candidate document.
|
|
317
|
+
|
|
318
|
+
Returns:
|
|
319
|
+
True for a ``.workflow.js`` path.
|
|
320
|
+
"""
|
|
321
|
+
workflow_constants = _hooks_module(
|
|
322
|
+
"hooks_constants.workflow_substitution_slot_blocker_constants"
|
|
323
|
+
)
|
|
324
|
+
return document.path.as_posix().lower().endswith(
|
|
325
|
+
workflow_constants.WORKFLOW_FILE_SUFFIX
|
|
326
|
+
)
|
|
327
|
+
|
|
328
|
+
|
|
329
|
+
def accepts_plans(document: Document) -> bool:
|
|
330
|
+
"""Return whether the document lives in a plan directory.
|
|
331
|
+
|
|
332
|
+
Args:
|
|
333
|
+
document: Candidate document.
|
|
334
|
+
|
|
335
|
+
Returns:
|
|
336
|
+
True for repository ``docs/plans`` or ``.claude/plans`` paths.
|
|
337
|
+
"""
|
|
338
|
+
plans_constants = _hooks_module(
|
|
339
|
+
"hooks_constants.open_questions_in_plans_blocker_constants"
|
|
340
|
+
)
|
|
341
|
+
normalized_path = f"/{document.path.as_posix().lower()}"
|
|
342
|
+
relative_path = document.path.as_posix().lower()
|
|
343
|
+
return (
|
|
344
|
+
plans_constants.PLANS_PATH_SEGMENT in normalized_path
|
|
345
|
+
or plans_constants.DOCS_PLANS_PATH_SEGMENT in normalized_path
|
|
346
|
+
or relative_path.startswith(
|
|
347
|
+
(
|
|
348
|
+
plans_constants.PLANS_PATH_PREFIX,
|
|
349
|
+
plans_constants.DOCS_PLANS_PATH_PREFIX,
|
|
350
|
+
)
|
|
351
|
+
)
|
|
352
|
+
)
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""Configuration values for the policy linter."""
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
INCOMPLETE_EXIT_CODE = 3
|
|
2
|
+
INVALID_INPUT_EXIT_CODE = 2
|
|
3
|
+
GIT_EXECUTABLE = "git"
|
|
4
|
+
UTF8_ENCODING = "utf-8"
|
|
5
|
+
PATH_SEPARATOR = "/"
|
|
6
|
+
NUL_BYTE = b"\0"
|
|
7
|
+
ALL_GIT_ROOT_ARGUMENTS = ("rev-parse", "--show-toplevel")
|
|
8
|
+
ALL_GIT_HEAD_ARGUMENTS = ("rev-parse", "--verify", "HEAD")
|
|
9
|
+
ALL_UNBORN_HEAD_ERROR_FRAGMENTS = ("needed a single revision",)
|
|
10
|
+
GIT_MERGE_BASE_ARGUMENT = "merge-base"
|
|
11
|
+
GIT_HEAD_REFERENCE = "HEAD"
|
|
12
|
+
GIT_INDEX_REFERENCE_PREFIX = ""
|
|
13
|
+
GIT_ENVIRONMENT_VARIABLE_PREFIX = "GIT_"
|
|
14
|
+
ALL_GIT_BLOB_ARGUMENTS = ("cat-file", "blob")
|
|
15
|
+
ALL_MISSING_BLOB_ERROR_FRAGMENTS = (
|
|
16
|
+
"does not exist in",
|
|
17
|
+
"does not exist (at stage",
|
|
18
|
+
"exists on disk, but not in",
|
|
19
|
+
)
|
|
20
|
+
GIT_DIFF_ARGUMENT = "diff"
|
|
21
|
+
GIT_CACHED_FLAG = "--cached"
|
|
22
|
+
GIT_NAME_STATUS_FLAG = "--name-status"
|
|
23
|
+
GIT_FIND_RENAMES_FLAG = "--find-renames"
|
|
24
|
+
GIT_ZERO_TERMINATED_FLAG = "-z"
|
|
25
|
+
GIT_FILES_ARGUMENT = "ls-files"
|
|
26
|
+
GIT_SEPARATOR = "--"
|
|
27
|
+
ALL_DIFF_CHANGED_TAGS = frozenset({"replace", "insert"})
|
|
28
|
+
NEW_DOCUMENT_ORIGIN_LINE = 1
|
|
29
|
+
RENAME_PATH_COUNT = 2
|
|
30
|
+
PYTHON_SUFFIX = ".py"
|
|
31
|
+
ALL_CODE_SUFFIXES = frozenset({".py", ".js", ".jsx", ".ts", ".tsx", ".mjs"})
|
|
32
|
+
ALL_MARKDOWN_SUFFIXES = frozenset({".md", ".mdx"})
|
|
33
|
+
ALL_STORED_PROMPT_SEGMENTS = (
|
|
34
|
+
"/.agents/",
|
|
35
|
+
"/commands/",
|
|
36
|
+
"/rules/",
|
|
37
|
+
"/system-prompts/",
|
|
38
|
+
)
|
|
39
|
+
ALL_HOOK_REGISTRATION_KEYS = frozenset({"command", "path", "script", "entrypoint"})
|
|
40
|
+
ALL_DECISION_REGISTRATION_KEYS = frozenset(
|
|
41
|
+
{"permissionDecision", "permission_decision", "decision"}
|
|
42
|
+
)
|
|
43
|
+
ALL_ACTION_BOUNDARY_SEGMENTS = frozenset(
|
|
44
|
+
{
|
|
45
|
+
"deny",
|
|
46
|
+
"block",
|
|
47
|
+
"ask",
|
|
48
|
+
"blocking",
|
|
49
|
+
"blocker",
|
|
50
|
+
"permissiondecision",
|
|
51
|
+
"permission_decision",
|
|
52
|
+
"pre_tool_use_dispatcher",
|
|
53
|
+
}
|
|
54
|
+
)
|
|
55
|
+
ALL_ACTION_BOUNDARY_PREFIXES = ("deny_", "block_", "ask_")
|
|
56
|
+
ALL_TEST_DIRECTORY_NAMES = frozenset({"tests"})
|
|
57
|
+
ALL_TEST_FILE_PREFIXES = ("test_",)
|
|
58
|
+
ALL_TEST_FILE_SUFFIXES = ("_test.py",)
|
|
59
|
+
POLICY_LINT_DIRECTORY_NAME = "policy_lint"
|
|
60
|
+
POLICY_LINT_RULES_TEST_PREFIX = "test_policy_lint_rules"
|
|
61
|
+
POLICY_LINT_SELECTION_TEST_PREFIX = "test_policy_lint_selection"
|
|
62
|
+
RUN_ALL_VALIDATORS_STEM = "run_all_validators"
|
|
63
|
+
FAST_SAVE_VALIDATORS_STEM = "fast_save_validators"
|
|
64
|
+
ALL_OVERLAPPING_VALIDATOR_NAMES = frozenset(
|
|
65
|
+
{"Python Style", "Magic Values", "Type Safety", "Test Safety", "React"}
|
|
66
|
+
)
|
|
@@ -0,0 +1,224 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from collections.abc import Iterable, Sequence
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
|
|
6
|
+
from .model import (
|
|
7
|
+
ChangeSetRule,
|
|
8
|
+
Diagnostic,
|
|
9
|
+
Document,
|
|
10
|
+
DocumentRule,
|
|
11
|
+
DocumentSet,
|
|
12
|
+
LintReport,
|
|
13
|
+
LintRequest,
|
|
14
|
+
RepositoryRule,
|
|
15
|
+
Rule,
|
|
16
|
+
SelectionKind,
|
|
17
|
+
)
|
|
18
|
+
from .registry import default_registry, selected_rules
|
|
19
|
+
from .selection import select_documents
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def lint(
|
|
23
|
+
request: LintRequest,
|
|
24
|
+
all_registry: Sequence[Rule] | None = None,
|
|
25
|
+
) -> LintReport:
|
|
26
|
+
"""Run the selected rules on one source selection.
|
|
27
|
+
|
|
28
|
+
Args:
|
|
29
|
+
request: Repository root, source, and rule sets.
|
|
30
|
+
all_registry: Optional registry replacement.
|
|
31
|
+
|
|
32
|
+
Returns:
|
|
33
|
+
A report of diagnostics and rule execution state.
|
|
34
|
+
"""
|
|
35
|
+
document_set = select_documents(request)
|
|
36
|
+
candidate_rules = tuple(all_registry) if all_registry is not None else default_registry()
|
|
37
|
+
selected = selected_rules(candidate_rules, request.rule_sets)
|
|
38
|
+
all_diagnostics, executed, failed, skipped = _run_selected_rules(selected, document_set)
|
|
39
|
+
return LintReport(
|
|
40
|
+
1,
|
|
41
|
+
tuple(sorted(all_diagnostics, key=_diagnostic_sort_key)),
|
|
42
|
+
tuple(each_document.path for each_document in document_set.documents),
|
|
43
|
+
executed,
|
|
44
|
+
failed,
|
|
45
|
+
skipped,
|
|
46
|
+
)
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def _run_selected_rules(
|
|
50
|
+
all_rules: tuple[Rule, ...], document_set: DocumentSet
|
|
51
|
+
) -> tuple[list[Diagnostic], tuple[str, ...], tuple[str, ...], tuple[str, ...]]:
|
|
52
|
+
resolved_repository_root = document_set.repository_root.resolve()
|
|
53
|
+
all_diagnostics: list[Diagnostic] = []
|
|
54
|
+
all_executed_rules: list[str] = []
|
|
55
|
+
all_failed_rules: list[str] = []
|
|
56
|
+
all_skipped_rules: list[str] = []
|
|
57
|
+
for each_rule in all_rules:
|
|
58
|
+
_run_rule(
|
|
59
|
+
each_rule,
|
|
60
|
+
document_set,
|
|
61
|
+
resolved_repository_root,
|
|
62
|
+
all_diagnostics,
|
|
63
|
+
all_executed_rules,
|
|
64
|
+
all_failed_rules,
|
|
65
|
+
all_skipped_rules,
|
|
66
|
+
)
|
|
67
|
+
return (
|
|
68
|
+
all_diagnostics,
|
|
69
|
+
tuple(all_executed_rules),
|
|
70
|
+
tuple(all_failed_rules),
|
|
71
|
+
tuple(all_skipped_rules),
|
|
72
|
+
)
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
def _run_rule(
|
|
76
|
+
rule: Rule,
|
|
77
|
+
document_set: DocumentSet,
|
|
78
|
+
resolved_repository_root: Path,
|
|
79
|
+
all_diagnostics: list[Diagnostic],
|
|
80
|
+
all_executed_rules: list[str],
|
|
81
|
+
all_failed_rules: list[str],
|
|
82
|
+
all_skipped_rules: list[str],
|
|
83
|
+
) -> None:
|
|
84
|
+
pending_diagnostics: list[Diagnostic] = []
|
|
85
|
+
all_skipped_before = len(all_skipped_rules)
|
|
86
|
+
if _rule_execution_failed(
|
|
87
|
+
rule,
|
|
88
|
+
document_set,
|
|
89
|
+
resolved_repository_root,
|
|
90
|
+
pending_diagnostics,
|
|
91
|
+
all_skipped_rules,
|
|
92
|
+
):
|
|
93
|
+
all_failed_rules.append(rule.rule_id)
|
|
94
|
+
return
|
|
95
|
+
if len(all_skipped_rules) != all_skipped_before:
|
|
96
|
+
return
|
|
97
|
+
all_diagnostics.extend(pending_diagnostics)
|
|
98
|
+
all_executed_rules.append(rule.rule_id)
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
def _rule_execution_failed(
|
|
102
|
+
rule: Rule,
|
|
103
|
+
document_set: DocumentSet,
|
|
104
|
+
resolved_repository_root: Path,
|
|
105
|
+
all_diagnostics: list[Diagnostic],
|
|
106
|
+
all_skipped_rules: list[str],
|
|
107
|
+
) -> bool:
|
|
108
|
+
try:
|
|
109
|
+
_execute_rule(
|
|
110
|
+
rule,
|
|
111
|
+
document_set,
|
|
112
|
+
resolved_repository_root,
|
|
113
|
+
all_diagnostics,
|
|
114
|
+
all_skipped_rules,
|
|
115
|
+
)
|
|
116
|
+
except (
|
|
117
|
+
AttributeError,
|
|
118
|
+
ImportError,
|
|
119
|
+
KeyError,
|
|
120
|
+
OSError,
|
|
121
|
+
RuntimeError,
|
|
122
|
+
SyntaxError,
|
|
123
|
+
TypeError,
|
|
124
|
+
UnicodeError,
|
|
125
|
+
ValueError,
|
|
126
|
+
):
|
|
127
|
+
return True
|
|
128
|
+
return False
|
|
129
|
+
|
|
130
|
+
|
|
131
|
+
def _execute_rule(
|
|
132
|
+
rule: Rule,
|
|
133
|
+
document_set: DocumentSet,
|
|
134
|
+
resolved_repository_root: Path,
|
|
135
|
+
all_diagnostics: list[Diagnostic],
|
|
136
|
+
all_skipped_rules: list[str],
|
|
137
|
+
) -> None:
|
|
138
|
+
if isinstance(rule, DocumentRule):
|
|
139
|
+
_run_document_rule(
|
|
140
|
+
rule,
|
|
141
|
+
document_set,
|
|
142
|
+
resolved_repository_root,
|
|
143
|
+
all_diagnostics,
|
|
144
|
+
all_skipped_rules,
|
|
145
|
+
)
|
|
146
|
+
return
|
|
147
|
+
if isinstance(rule, ChangeSetRule):
|
|
148
|
+
_run_changeset_rule(rule, document_set, all_diagnostics, all_skipped_rules)
|
|
149
|
+
return
|
|
150
|
+
_run_repository_rule(rule, document_set, all_diagnostics, all_skipped_rules)
|
|
151
|
+
|
|
152
|
+
|
|
153
|
+
def _run_document_rule(
|
|
154
|
+
rule: DocumentRule,
|
|
155
|
+
document_set: DocumentSet,
|
|
156
|
+
resolved_repository_root: Path,
|
|
157
|
+
all_diagnostics: list[Diagnostic],
|
|
158
|
+
all_skipped_rules: list[str],
|
|
159
|
+
) -> None:
|
|
160
|
+
all_documents = tuple(
|
|
161
|
+
each_document
|
|
162
|
+
for each_document in document_set.documents
|
|
163
|
+
if rule.accepts(each_document)
|
|
164
|
+
)
|
|
165
|
+
if not all_documents:
|
|
166
|
+
all_skipped_rules.append(rule.rule_id)
|
|
167
|
+
return
|
|
168
|
+
for each_document in all_documents:
|
|
169
|
+
all_diagnostics.extend(
|
|
170
|
+
_scoped_diagnostics(
|
|
171
|
+
rule.check(each_document, resolved_repository_root),
|
|
172
|
+
each_document,
|
|
173
|
+
)
|
|
174
|
+
)
|
|
175
|
+
|
|
176
|
+
|
|
177
|
+
def _run_changeset_rule(
|
|
178
|
+
rule: ChangeSetRule,
|
|
179
|
+
document_set: DocumentSet,
|
|
180
|
+
all_diagnostics: list[Diagnostic],
|
|
181
|
+
all_skipped_rules: list[str],
|
|
182
|
+
) -> None:
|
|
183
|
+
if document_set.selection not in {SelectionKind.STAGED, SelectionKind.BASE}:
|
|
184
|
+
all_skipped_rules.append(rule.rule_id)
|
|
185
|
+
return
|
|
186
|
+
all_diagnostics.extend(rule.check(document_set))
|
|
187
|
+
|
|
188
|
+
|
|
189
|
+
def _run_repository_rule(
|
|
190
|
+
rule: RepositoryRule,
|
|
191
|
+
document_set: DocumentSet,
|
|
192
|
+
all_diagnostics: list[Diagnostic],
|
|
193
|
+
all_skipped_rules: list[str],
|
|
194
|
+
) -> None:
|
|
195
|
+
if document_set.selection is not SelectionKind.REPOSITORY:
|
|
196
|
+
all_skipped_rules.append(rule.rule_id)
|
|
197
|
+
return
|
|
198
|
+
all_diagnostics.extend(rule.check(document_set))
|
|
199
|
+
|
|
200
|
+
|
|
201
|
+
def _scoped_diagnostics(
|
|
202
|
+
all_diagnostics: Iterable[Diagnostic],
|
|
203
|
+
document: Document,
|
|
204
|
+
) -> tuple[Diagnostic, ...]:
|
|
205
|
+
if document.changed_lines is None or not document.changed_lines:
|
|
206
|
+
return tuple(all_diagnostics)
|
|
207
|
+
return tuple(
|
|
208
|
+
each_diagnostic
|
|
209
|
+
for each_diagnostic in all_diagnostics
|
|
210
|
+
if each_diagnostic.location is None
|
|
211
|
+
or each_diagnostic.location.start_line in document.changed_lines
|
|
212
|
+
)
|
|
213
|
+
|
|
214
|
+
|
|
215
|
+
def _diagnostic_sort_key(diagnostic: Diagnostic) -> tuple[str, int, int, str, str]:
|
|
216
|
+
if diagnostic.location is None:
|
|
217
|
+
return ("", 0, 0, diagnostic.rule_id, diagnostic.message)
|
|
218
|
+
return (
|
|
219
|
+
diagnostic.location.path.as_posix(),
|
|
220
|
+
diagnostic.location.start_line,
|
|
221
|
+
diagnostic.location.start_column,
|
|
222
|
+
diagnostic.rule_id,
|
|
223
|
+
diagnostic.message,
|
|
224
|
+
)
|