claude-dev-env 1.0.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.
Files changed (215) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +219 -0
  3. package/agents/agent-writer.md +157 -0
  4. package/agents/clasp-deployment-orchestrator.md +609 -0
  5. package/agents/clean-coder.md +295 -0
  6. package/agents/code-quality-agent.md +40 -0
  7. package/agents/code-standards-agent.md +93 -0
  8. package/agents/config-centralizer.md +686 -0
  9. package/agents/config-extraction-agent.md +225 -0
  10. package/agents/doc-orchestrator.md +47 -0
  11. package/agents/docs-agent.md +112 -0
  12. package/agents/docx-agent.md +211 -0
  13. package/agents/git-commit-crafter.md +100 -0
  14. package/agents/magic-value-eliminator-agent.md +72 -0
  15. package/agents/mandatory-agent-workflow-agent.md +88 -0
  16. package/agents/parallel-workflow-coordinator.md +779 -0
  17. package/agents/pdf-agent.md +302 -0
  18. package/agents/plan-executor.md +226 -0
  19. package/agents/pr-description-writer.md +87 -0
  20. package/agents/project-context-loader.md +238 -0
  21. package/agents/project-docs-analyzer.md +54 -0
  22. package/agents/project-structure-organizer-agent.md +72 -0
  23. package/agents/readability-review-agent.md +76 -0
  24. package/agents/refactoring-specialist.md +69 -0
  25. package/agents/right-sized-engineer.md +129 -0
  26. package/agents/session-continuity-manager.md +53 -0
  27. package/agents/skill-to-agent-converter.md +371 -0
  28. package/agents/skill-writer-agent.md +470 -0
  29. package/agents/stub-detector-agent.md +140 -0
  30. package/agents/tdd-test-writer.md +62 -0
  31. package/agents/test-data-builder.md +68 -0
  32. package/agents/tooling-builder.md +78 -0
  33. package/agents/user-docs-writer.md +67 -0
  34. package/agents/validation-expert.md +71 -0
  35. package/agents/workflow-visual-documenter.md +82 -0
  36. package/agents/xlsx-agent.md +169 -0
  37. package/bin/install.mjs +256 -0
  38. package/commands/commit.md +28 -0
  39. package/commands/docupdate.md +322 -0
  40. package/commands/implement.md +102 -0
  41. package/commands/initialize.md +91 -0
  42. package/commands/plan.md +63 -0
  43. package/commands/pr-comments.md +47 -0
  44. package/commands/readability-review.md +20 -0
  45. package/commands/review-plan.md +7 -0
  46. package/commands/right-size.md +15 -0
  47. package/commands/stubcheck.md +89 -0
  48. package/commands/sum.md +30 -0
  49. package/docs/CODE_RULES.md +186 -0
  50. package/docs/DJANGO_PATTERNS.md +80 -0
  51. package/docs/REACT_PATTERNS.md +185 -0
  52. package/docs/TEST_QUALITY.md +104 -0
  53. package/hooks/advisory/migration-safety-advisor.py +49 -0
  54. package/hooks/advisory/refactor-guard.py +205 -0
  55. package/hooks/blocking/block-main-commit.py +168 -0
  56. package/hooks/blocking/code-rules-enforcer.py +549 -0
  57. package/hooks/blocking/destructive-command-blocker.py +107 -0
  58. package/hooks/blocking/docker-settings-guard.py +44 -0
  59. package/hooks/blocking/hedging-language-blocker.py +130 -0
  60. package/hooks/blocking/parallel-task-blocker.py +69 -0
  61. package/hooks/blocking/pr-description-enforcer.py +87 -0
  62. package/hooks/blocking/pyautogui-scroll-blocker.py +74 -0
  63. package/hooks/blocking/sensitive-file-protector.py +70 -0
  64. package/hooks/blocking/tdd-enforcer.py +62 -0
  65. package/hooks/blocking/test-preflight-check.py +343 -0
  66. package/hooks/blocking/write-existing-file-blocker.py +63 -0
  67. package/hooks/git-hooks/post-commit.py +103 -0
  68. package/hooks/github-action/test_workflow.py +33 -0
  69. package/hooks/hooks.json +246 -0
  70. package/hooks/lifecycle/config-change-guard.py +84 -0
  71. package/hooks/lifecycle/session-end-cleanup.py +59 -0
  72. package/hooks/notification/attention-needed-notify.py +63 -0
  73. package/hooks/notification/claude-notification-handler.py +59 -0
  74. package/hooks/notification/notification_utils.py +206 -0
  75. package/hooks/rewrite-plugin-paths.py +116 -0
  76. package/hooks/session/bulk-edit-reminder.py +30 -0
  77. package/hooks/session/code-rules-reminder.py +97 -0
  78. package/hooks/session/compact-context-reinject.py +39 -0
  79. package/hooks/session/hook-structure-context.py +140 -0
  80. package/hooks/session/plugin-data-dir-cleanup.py +39 -0
  81. package/hooks/validation/code-style-validator.py +145 -0
  82. package/hooks/validation/e2e-test-validator.py +142 -0
  83. package/hooks/validation/hook-format-validator.py +66 -0
  84. package/hooks/validation/mypy_validator.py +180 -0
  85. package/hooks/validators/README.md +125 -0
  86. package/hooks/validators/VALIDATION_REPORT.md +287 -0
  87. package/hooks/validators/__init__.py +19 -0
  88. package/hooks/validators/abbreviation_checks.py +82 -0
  89. package/hooks/validators/code_quality_checks.py +133 -0
  90. package/hooks/validators/comment_checks.py +188 -0
  91. package/hooks/validators/file_structure_checks.py +182 -0
  92. package/hooks/validators/git_checks.py +107 -0
  93. package/hooks/validators/health_check.py +214 -0
  94. package/hooks/validators/magic_value_checks.py +81 -0
  95. package/hooks/validators/mypy_integration.py +52 -0
  96. package/hooks/validators/output_formatter.py +266 -0
  97. package/hooks/validators/pr_reference_checks.py +72 -0
  98. package/hooks/validators/python_antipattern_checks.py +110 -0
  99. package/hooks/validators/python_style_checks.py +364 -0
  100. package/hooks/validators/react_checks.py +90 -0
  101. package/hooks/validators/ruff_integration.py +80 -0
  102. package/hooks/validators/run_all_validators.py +772 -0
  103. package/hooks/validators/security_checks.py +135 -0
  104. package/hooks/validators/test_abbreviation_checks.py +76 -0
  105. package/hooks/validators/test_bad.tsx +7 -0
  106. package/hooks/validators/test_code_quality_checks.py +129 -0
  107. package/hooks/validators/test_file_structure_checks.py +307 -0
  108. package/hooks/validators/test_files/01_basic_component.tsx +10 -0
  109. package/hooks/validators/test_files/02_component_without_react.tsx +10 -0
  110. package/hooks/validators/test_files/03_pure_component.tsx +10 -0
  111. package/hooks/validators/test_files/04_pure_component_import.tsx +10 -0
  112. package/hooks/validators/test_files/05_typescript_generics.tsx +14 -0
  113. package/hooks/validators/test_files/06_typescript_two_generics.tsx +18 -0
  114. package/hooks/validators/test_files/07_multiline_declaration.tsx +11 -0
  115. package/hooks/validators/test_files/08_error_boundary_valid.tsx +14 -0
  116. package/hooks/validators/test_files/09_error_boundary_with_other_class.tsx +20 -0
  117. package/hooks/validators/test_files/10_inheritance_chain.tsx +16 -0
  118. package/hooks/validators/test_files/11_ts_file.ts +10 -0
  119. package/hooks/validators/test_files/12_non_react_class.tsx +14 -0
  120. package/hooks/validators/test_files/13_functional_component.tsx +8 -0
  121. package/hooks/validators/test_files/14_indented_class.tsx +13 -0
  122. package/hooks/validators/test_files/15_getDerivedStateFromError.tsx +14 -0
  123. package/hooks/validators/test_files/16_mixed_components.tsx +20 -0
  124. package/hooks/validators/test_files/EXECUTIVE_SUMMARY.md +175 -0
  125. package/hooks/validators/test_files/TEST_RESULTS_TABLE.txt +60 -0
  126. package/hooks/validators/test_files/VALIDATION_REPORT.md +201 -0
  127. package/hooks/validators/test_files/async_views.py +23 -0
  128. package/hooks/validators/test_files/async_with_imports.py +14 -0
  129. package/hooks/validators/test_files/bad_inline_imports.py +37 -0
  130. package/hooks/validators/test_files/management/commands/cmd_01_no_debug_check.py +10 -0
  131. package/hooks/validators/test_files/management/commands/cmd_02_proper_debug_check.py +14 -0
  132. package/hooks/validators/test_files/management/commands/cmd_03_debug_check_with_return.py +14 -0
  133. package/hooks/validators/test_files/management/commands/cmd_04_imported_DEBUG.py +14 -0
  134. package/hooks/validators/test_files/management/commands/cmd_05_debug_check_in_helper.py +16 -0
  135. package/hooks/validators/test_files/management/commands/cmd_06_debug_check_late.py +22 -0
  136. package/hooks/validators/test_files/management/commands/cmd_07_positive_debug_check.py +15 -0
  137. package/hooks/validators/test_files/management/commands/cmd_08_debug_with_and.py +14 -0
  138. package/hooks/validators/test_files/not_management_command.py +10 -0
  139. package/hooks/validators/test_files/skip_decorators/test_01_simple_skip.py +8 -0
  140. package/hooks/validators/test_files/skip_decorators/test_02_pytest_skipif.py +8 -0
  141. package/hooks/validators/test_files/skip_decorators/test_03_unittest_skipIf.py +8 -0
  142. package/hooks/validators/test_files/skip_decorators/test_04_skip_with_parens.py +8 -0
  143. package/hooks/validators/test_files/skip_decorators/test_05_xfail.py +7 -0
  144. package/hooks/validators/test_files/skip_decorators/test_06_custom_skip.py +11 -0
  145. package/hooks/validators/test_files/skip_decorators/test_07_capital_Skip.py +8 -0
  146. package/hooks/validators/test_files/skip_decorators/test_08_skipUnless.py +7 -0
  147. package/hooks/validators/test_files/skip_decorators/test_09_pytest_mark_skip_simple.py +7 -0
  148. package/hooks/validators/test_files/test_async_functions.py +45 -0
  149. package/hooks/validators/test_files/test_purecomponent/PureComponentExample.tsx +7 -0
  150. package/hooks/validators/test_files/test_purecomponent/ReactPureComponentExample.tsx +7 -0
  151. package/hooks/validators/test_git_checks.py +295 -0
  152. package/hooks/validators/test_good.tsx +5 -0
  153. package/hooks/validators/test_health_check.py +57 -0
  154. package/hooks/validators/test_magic_value_checks.py +63 -0
  155. package/hooks/validators/test_mypy_integration.py +27 -0
  156. package/hooks/validators/test_output_formatter.py +150 -0
  157. package/hooks/validators/test_pr_reference_checks.py +41 -0
  158. package/hooks/validators/test_python_antipattern_checks.py +113 -0
  159. package/hooks/validators/test_python_style_checks.py +439 -0
  160. package/hooks/validators/test_react_checks.py +213 -0
  161. package/hooks/validators/test_results.txt +25 -0
  162. package/hooks/validators/test_ruff_integration.py +27 -0
  163. package/hooks/validators/test_run_all_validators.py +228 -0
  164. package/hooks/validators/test_run_all_validators_integration.py +48 -0
  165. package/hooks/validators/test_safety_checks.py +243 -0
  166. package/hooks/validators/test_security_checks.py +105 -0
  167. package/hooks/validators/test_test_safety_checks.py +321 -0
  168. package/hooks/validators/test_todo_checks.py +39 -0
  169. package/hooks/validators/test_type_safety_checks.py +85 -0
  170. package/hooks/validators/test_useless_test_checks.py +55 -0
  171. package/hooks/validators/test_validator_base.py +26 -0
  172. package/hooks/validators/test_verify_paths.py +34 -0
  173. package/hooks/validators/todo_checks.py +59 -0
  174. package/hooks/validators/type_safety_checks.py +101 -0
  175. package/hooks/validators/useless_test_checks.py +92 -0
  176. package/hooks/validators/validator_base.py +19 -0
  177. package/hooks/validators/verify_paths.py +57 -0
  178. package/hooks/workflow/auto-formatter.py +114 -0
  179. package/hooks/workflow/investigation-tracker-reset.py +46 -0
  180. package/package.json +30 -0
  181. package/rules/agent-spawn-protocol.md +47 -0
  182. package/rules/cleanup-temp-files.md +27 -0
  183. package/rules/code-reviews.md +11 -0
  184. package/rules/code-standards.md +43 -0
  185. package/rules/conservative-action.md +20 -0
  186. package/rules/context7.md +12 -0
  187. package/rules/explore-thoroughly.md +27 -0
  188. package/rules/git-workflow.md +42 -0
  189. package/rules/parallel-tools.md +23 -0
  190. package/rules/research-mode.md +23 -0
  191. package/rules/right-sized-engineering.md +28 -0
  192. package/rules/tdd.md +7 -0
  193. package/rules/testing.md +12 -0
  194. package/skills/agent-prompt/SKILL.md +102 -0
  195. package/skills/anthropic-plan/SKILL.md +107 -0
  196. package/skills/everything-search/SKILL.md +144 -0
  197. package/skills/ingest/SKILL.md +40 -0
  198. package/skills/npm-creator/SKILL.md +183 -0
  199. package/skills/pr-review-responder/EXAMPLES.md +590 -0
  200. package/skills/pr-review-responder/PRINCIPLES.md +539 -0
  201. package/skills/pr-review-responder/README.md +209 -0
  202. package/skills/pr-review-responder/SKILL.md +202 -0
  203. package/skills/pr-review-responder/TESTING.md +407 -0
  204. package/skills/pr-review-responder/scripts/respond_to_reviews.py +376 -0
  205. package/skills/pr-review-responder/update_skill.py +297 -0
  206. package/skills/prompt-generator/REFERENCE.md +150 -0
  207. package/skills/prompt-generator/SKILL.md +154 -0
  208. package/skills/readability-review/SKILL.md +127 -0
  209. package/skills/recall/SKILL.md +27 -0
  210. package/skills/remember/SKILL.md +63 -0
  211. package/skills/rule-audit/SKILL.md +307 -0
  212. package/skills/rule-creator/SKILL.md +150 -0
  213. package/skills/skill-writer/REFERENCE.md +246 -0
  214. package/skills/skill-writer/SKILL.md +270 -0
  215. package/skills/tdd-team/SKILL.md +128 -0
@@ -0,0 +1,439 @@
1
+ """Tests for Python style checks."""
2
+
3
+ import ast
4
+ import tempfile
5
+ from pathlib import Path
6
+ from typing import List
7
+
8
+ import pytest
9
+
10
+ from python_style_checks import (
11
+ Violation,
12
+ check_imports_at_top,
13
+ check_no_empty_line_after_decorators,
14
+ check_single_empty_line_between_functions,
15
+ check_view_function_naming,
16
+ validate_file,
17
+ )
18
+
19
+
20
+ # Test data: Code samples
21
+ GOOD_IMPORTS = '''import os
22
+ import sys
23
+ from typing import List
24
+
25
+ def foo() -> None:
26
+ pass
27
+ '''
28
+
29
+ BAD_IMPORTS_AFTER_CODE = '''def foo() -> None:
30
+ pass
31
+
32
+ import os
33
+ '''
34
+
35
+ BAD_IMPORTS_AFTER_CONSTANT = '''MY_CONSTANT = 42
36
+
37
+ import os
38
+ '''
39
+
40
+ GOOD_DECORATOR_NO_BLANK = '''@decorator
41
+ def foo() -> None:
42
+ pass
43
+ '''
44
+
45
+ BAD_DECORATOR_WITH_BLANK = '''@decorator
46
+
47
+ def foo() -> None:
48
+ pass
49
+ '''
50
+
51
+ GOOD_SINGLE_LINE_BETWEEN_FUNCTIONS = '''def foo() -> None:
52
+ pass
53
+
54
+ def bar() -> None:
55
+ pass
56
+ '''
57
+
58
+ BAD_NO_LINE_BETWEEN_FUNCTIONS = '''def foo() -> None:
59
+ pass
60
+ def bar() -> None:
61
+ pass
62
+ '''
63
+
64
+ BAD_MULTIPLE_LINES_BETWEEN_FUNCTIONS = '''def foo() -> None:
65
+ pass
66
+
67
+
68
+ def bar() -> None:
69
+ pass
70
+ '''
71
+
72
+ GOOD_VIEW_NAMING = '''def user_profile_view(request):
73
+ pass
74
+
75
+ def get_tasks_view(request, user_id):
76
+ pass
77
+ '''
78
+
79
+ BAD_VIEW_NAMING = '''def user_profile(request):
80
+ pass
81
+
82
+ def getTasks(request, user_id):
83
+ pass
84
+ '''
85
+
86
+ GOOD_NON_VIEW_FUNCTION = '''def helper_function(data):
87
+ pass
88
+
89
+ def process_data(items):
90
+ pass
91
+ '''
92
+
93
+ # Async function test samples
94
+ ASYNC_GOOD_DECORATOR_NO_BLANK = '''import asyncio
95
+
96
+ @asyncio.coroutine
97
+ async def foo() -> None:
98
+ await asyncio.sleep(0)
99
+ '''
100
+
101
+ ASYNC_BAD_DECORATOR_WITH_BLANK = '''import asyncio
102
+
103
+ @asyncio.coroutine
104
+
105
+ async def foo() -> None:
106
+ await asyncio.sleep(0)
107
+ '''
108
+
109
+ ASYNC_GOOD_SINGLE_LINE_BETWEEN = '''import asyncio
110
+
111
+ async def foo() -> None:
112
+ await asyncio.sleep(0)
113
+
114
+ async def bar() -> None:
115
+ await asyncio.sleep(0)
116
+ '''
117
+
118
+ ASYNC_BAD_MULTIPLE_LINES_BETWEEN = '''import asyncio
119
+
120
+ async def foo() -> None:
121
+ await asyncio.sleep(0)
122
+
123
+
124
+ async def bar() -> None:
125
+ await asyncio.sleep(0)
126
+ '''
127
+
128
+ ASYNC_GOOD_VIEW_NAMING = '''from django.http import HttpRequest, HttpResponse
129
+
130
+ async def user_profile_view(request: HttpRequest) -> HttpResponse:
131
+ return HttpResponse("profile")
132
+
133
+ async def get_tasks_view(request: HttpRequest, user_id: int) -> HttpResponse:
134
+ return HttpResponse("tasks")
135
+ '''
136
+
137
+ ASYNC_BAD_VIEW_NAMING = '''from django.http import HttpRequest, HttpResponse
138
+
139
+ async def user_profile(request: HttpRequest) -> HttpResponse:
140
+ return HttpResponse("profile")
141
+
142
+ async def getTasks(request: HttpRequest, user_id: int) -> HttpResponse:
143
+ return HttpResponse("tasks")
144
+ '''
145
+
146
+ ASYNC_BAD_INLINE_IMPORT = '''import asyncio
147
+
148
+ async def foo() -> None:
149
+ import json
150
+ await asyncio.sleep(0)
151
+ '''
152
+
153
+
154
+ class TestImportsAtTop:
155
+ """Test import positioning validation."""
156
+
157
+ def test_imports_at_top_valid(self) -> None:
158
+ """All imports at top should pass."""
159
+ tree = ast.parse(GOOD_IMPORTS)
160
+ violations = check_imports_at_top(tree, "test.py")
161
+ assert violations == []
162
+
163
+ def test_import_after_function(self) -> None:
164
+ """Import after function should fail."""
165
+ tree = ast.parse(BAD_IMPORTS_AFTER_CODE)
166
+ violations = check_imports_at_top(tree, "test.py")
167
+ assert len(violations) == 1
168
+ assert violations[0].line == 4
169
+ assert "import" in violations[0].message.lower()
170
+
171
+ def test_import_after_constant(self) -> None:
172
+ """Import after constant should fail."""
173
+ tree = ast.parse(BAD_IMPORTS_AFTER_CONSTANT)
174
+ violations = check_imports_at_top(tree, "test.py")
175
+ assert len(violations) == 1
176
+ assert violations[0].line == 3
177
+
178
+ def test_async_inline_import_fails(self) -> None:
179
+ """Import inside async function should fail."""
180
+ tree = ast.parse(ASYNC_BAD_INLINE_IMPORT)
181
+ violations = check_imports_at_top(tree, "test.py")
182
+ assert len(violations) == 1
183
+ assert "inside function" in violations[0].message.lower()
184
+
185
+
186
+ class TestNoEmptyLineAfterDecorators:
187
+ """Test decorator spacing validation."""
188
+
189
+ def test_no_blank_line_valid(self) -> None:
190
+ """Decorator directly above function should pass."""
191
+ violations = check_no_empty_line_after_decorators(
192
+ GOOD_DECORATOR_NO_BLANK, "test.py"
193
+ )
194
+ assert violations == []
195
+
196
+ def test_blank_line_after_decorator_fails(self) -> None:
197
+ """Blank line after decorator should fail."""
198
+ violations = check_no_empty_line_after_decorators(
199
+ BAD_DECORATOR_WITH_BLANK, "test.py"
200
+ )
201
+ assert len(violations) == 1
202
+ assert violations[0].line == 1
203
+ assert "decorator" in violations[0].message.lower()
204
+
205
+ def test_async_no_blank_line_valid(self) -> None:
206
+ """Decorator directly above async function should pass."""
207
+ violations = check_no_empty_line_after_decorators(
208
+ ASYNC_GOOD_DECORATOR_NO_BLANK, "test.py"
209
+ )
210
+ assert violations == []
211
+
212
+ def test_async_blank_line_after_decorator_fails(self) -> None:
213
+ """Blank line after decorator on async function should fail."""
214
+ violations = check_no_empty_line_after_decorators(
215
+ ASYNC_BAD_DECORATOR_WITH_BLANK, "test.py"
216
+ )
217
+ assert len(violations) == 1
218
+ assert violations[0].line == 3
219
+ assert "decorator" in violations[0].message.lower()
220
+
221
+
222
+ class TestSingleEmptyLineBetweenFunctions:
223
+ """Test function spacing validation."""
224
+
225
+ def test_single_line_valid(self) -> None:
226
+ """Exactly one blank line should pass."""
227
+ violations = check_single_empty_line_between_functions(
228
+ GOOD_SINGLE_LINE_BETWEEN_FUNCTIONS, "test.py"
229
+ )
230
+ assert violations == []
231
+
232
+ def test_no_line_between_functions_fails(self) -> None:
233
+ """No blank line should fail."""
234
+ violations = check_single_empty_line_between_functions(
235
+ BAD_NO_LINE_BETWEEN_FUNCTIONS, "test.py"
236
+ )
237
+ assert len(violations) == 1
238
+ assert "empty line" in violations[0].message.lower()
239
+
240
+ def test_multiple_lines_between_functions_fails(self) -> None:
241
+ """Multiple blank lines should fail."""
242
+ violations = check_single_empty_line_between_functions(
243
+ BAD_MULTIPLE_LINES_BETWEEN_FUNCTIONS, "test.py"
244
+ )
245
+ assert len(violations) == 1
246
+ assert "empty line" in violations[0].message.lower()
247
+
248
+ def test_async_single_line_valid(self) -> None:
249
+ """Exactly one blank line between async functions should pass."""
250
+ violations = check_single_empty_line_between_functions(
251
+ ASYNC_GOOD_SINGLE_LINE_BETWEEN, "test.py"
252
+ )
253
+ assert violations == []
254
+
255
+ def test_async_multiple_lines_fails(self) -> None:
256
+ """Multiple blank lines between async functions should fail."""
257
+ violations = check_single_empty_line_between_functions(
258
+ ASYNC_BAD_MULTIPLE_LINES_BETWEEN, "test.py"
259
+ )
260
+ assert len(violations) == 1
261
+ assert "empty line" in violations[0].message.lower()
262
+
263
+
264
+ class TestViewFunctionNaming:
265
+ """Test view function naming validation."""
266
+
267
+ def test_view_functions_named_correctly(self) -> None:
268
+ """View functions ending with _view should pass."""
269
+ tree = ast.parse(GOOD_VIEW_NAMING)
270
+ violations = check_view_function_naming(tree, "views.py")
271
+ assert violations == []
272
+
273
+ def test_view_functions_without_suffix_fail(self) -> None:
274
+ """View functions not ending with _view should fail."""
275
+ tree = ast.parse(BAD_VIEW_NAMING)
276
+ violations = check_view_function_naming(tree, "views.py")
277
+ assert len(violations) == 2
278
+ assert all("_view" in v.message for v in violations)
279
+
280
+ def test_non_view_file_ignored(self) -> None:
281
+ """Non-views.py files should be ignored."""
282
+ tree = ast.parse(BAD_VIEW_NAMING)
283
+ violations = check_view_function_naming(tree, "utils.py")
284
+ assert violations == []
285
+
286
+ def test_non_request_function_ignored(self) -> None:
287
+ """Functions without request parameter should be ignored."""
288
+ tree = ast.parse(GOOD_NON_VIEW_FUNCTION)
289
+ violations = check_view_function_naming(tree, "views.py")
290
+ assert violations == []
291
+
292
+ def test_async_view_functions_named_correctly(self) -> None:
293
+ """Async view functions ending with _view should pass."""
294
+ tree = ast.parse(ASYNC_GOOD_VIEW_NAMING)
295
+ violations = check_view_function_naming(tree, "views.py")
296
+ assert violations == []
297
+
298
+ def test_async_view_functions_without_suffix_fail(self) -> None:
299
+ """Async view functions not ending with _view should fail."""
300
+ tree = ast.parse(ASYNC_BAD_VIEW_NAMING)
301
+ violations = check_view_function_naming(tree, "views.py")
302
+ assert len(violations) == 2
303
+ assert all("_view" in v.message for v in violations)
304
+
305
+
306
+ class TestValidateFile:
307
+ """Test file validation integration."""
308
+
309
+ def test_valid_file_passes(self) -> None:
310
+ """File with no violations should pass."""
311
+ with tempfile.NamedTemporaryFile(mode="w", suffix=".py", delete=False) as f:
312
+ f.write(GOOD_IMPORTS)
313
+ f.flush()
314
+ temp_path = Path(f.name)
315
+
316
+ try:
317
+ violations = validate_file(temp_path)
318
+ assert violations == []
319
+ finally:
320
+ temp_path.unlink()
321
+
322
+ def test_invalid_file_returns_violations(self) -> None:
323
+ """File with violations should return them."""
324
+ with tempfile.NamedTemporaryFile(mode="w", suffix=".py", delete=False) as f:
325
+ f.write(BAD_IMPORTS_AFTER_CODE)
326
+ f.flush()
327
+ temp_path = Path(f.name)
328
+
329
+ try:
330
+ violations = validate_file(temp_path)
331
+ assert len(violations) > 0
332
+ assert all(isinstance(v, Violation) for v in violations)
333
+ finally:
334
+ temp_path.unlink()
335
+
336
+ def test_syntax_error_returns_violation(self) -> None:
337
+ """File with syntax error should return violation."""
338
+ with tempfile.NamedTemporaryFile(mode="w", suffix=".py", delete=False) as f:
339
+ f.write("def foo(\n") # Invalid syntax
340
+ f.flush()
341
+ temp_path = Path(f.name)
342
+
343
+ try:
344
+ violations = validate_file(temp_path)
345
+ assert len(violations) == 1
346
+ assert "syntax error" in violations[0].message.lower()
347
+ finally:
348
+ temp_path.unlink()
349
+
350
+
351
+ class TestViolationClass:
352
+ """Test Violation dataclass."""
353
+
354
+ def test_violation_creation(self) -> None:
355
+ """Violation should store file, line, message."""
356
+ v = Violation("test.py", 42, "Test message")
357
+ assert v.file == "test.py"
358
+ assert v.line == 42
359
+ assert v.message == "Test message"
360
+
361
+ def test_violation_str_format(self) -> None:
362
+ """Violation should format as file:line: message."""
363
+ v = Violation("test.py", 42, "Test message")
364
+ assert str(v) == "test.py:42: Test message"
365
+
366
+
367
+ class TestAutoFix:
368
+ """Test auto-fix capabilities."""
369
+
370
+ def test_fix_empty_line_after_decorator(self) -> None:
371
+ """Auto-fix should remove blank line between decorator and function."""
372
+ code = '''@decorator
373
+
374
+ def foo():
375
+ pass
376
+ '''
377
+ expected = '''@decorator
378
+ def foo():
379
+ pass
380
+ '''
381
+ with tempfile.NamedTemporaryFile(mode="w", suffix=".py", delete=False) as temp_file:
382
+ temp_file.write(code)
383
+ temp_path = Path(temp_file.name)
384
+
385
+ try:
386
+ from python_style_checks import fix_file
387
+ fixed = fix_file(temp_path)
388
+ assert fixed is True
389
+ result = temp_path.read_text()
390
+ assert result.strip() == expected.strip()
391
+ finally:
392
+ temp_path.unlink()
393
+
394
+ def test_fix_multiple_blank_lines_between_functions(self) -> None:
395
+ """Auto-fix should collapse multiple blank lines to single."""
396
+ code = '''def foo():
397
+ pass
398
+
399
+
400
+ def bar():
401
+ pass
402
+ '''
403
+ expected = '''def foo():
404
+ pass
405
+
406
+ def bar():
407
+ pass
408
+ '''
409
+ with tempfile.NamedTemporaryFile(mode="w", suffix=".py", delete=False) as temp_file:
410
+ temp_file.write(code)
411
+ temp_path = Path(temp_file.name)
412
+
413
+ try:
414
+ from python_style_checks import fix_file
415
+ fixed = fix_file(temp_path)
416
+ assert fixed is True
417
+ result = temp_path.read_text()
418
+ assert result.strip() == expected.strip()
419
+ finally:
420
+ temp_path.unlink()
421
+
422
+ def test_no_fix_needed_returns_false(self) -> None:
423
+ """Auto-fix should return False when no fixes needed."""
424
+ code = '''def foo():
425
+ pass
426
+
427
+ def bar():
428
+ pass
429
+ '''
430
+ with tempfile.NamedTemporaryFile(mode="w", suffix=".py", delete=False) as temp_file:
431
+ temp_file.write(code)
432
+ temp_path = Path(temp_file.name)
433
+
434
+ try:
435
+ from python_style_checks import fix_file
436
+ fixed = fix_file(temp_path)
437
+ assert fixed is False
438
+ finally:
439
+ temp_path.unlink()
@@ -0,0 +1,213 @@
1
+ """Tests for React class component validator."""
2
+
3
+ import pytest
4
+ from pathlib import Path
5
+ from react_checks import check_no_class_components, Violation
6
+
7
+
8
+ def test_class_component_extends_component_should_fail(tmp_path: Path) -> None:
9
+ """Class component using Component should be detected."""
10
+ test_file = tmp_path / "BadComponent.tsx"
11
+ test_file.write_text("""import { Component } from 'react';
12
+
13
+ class UserProfile extends Component {
14
+ render() {
15
+ return <div>Profile</div>;
16
+ }
17
+ }
18
+ """)
19
+
20
+ violations = check_no_class_components([str(test_file)])
21
+
22
+ assert len(violations) == 1
23
+ assert violations[0].file == str(test_file)
24
+ assert violations[0].line == 3
25
+ assert "class component" in violations[0].message.lower()
26
+
27
+
28
+ def test_class_component_extends_react_component_should_fail(tmp_path: Path) -> None:
29
+ """Class component using React.Component should be detected."""
30
+ test_file = tmp_path / "BadComponent.tsx"
31
+ test_file.write_text("""import React from 'react';
32
+
33
+ class TaskList extends React.Component {
34
+ render() {
35
+ return <ul>Tasks</ul>;
36
+ }
37
+ }
38
+ """)
39
+
40
+ violations = check_no_class_components([str(test_file)])
41
+
42
+ assert len(violations) == 1
43
+ assert violations[0].file == str(test_file)
44
+ assert violations[0].line == 3
45
+
46
+
47
+ def test_class_component_extends_purecomponent_should_fail(tmp_path: Path) -> None:
48
+ """Class component using PureComponent should be detected."""
49
+ test_file = tmp_path / "BadPureComponent.tsx"
50
+ test_file.write_text("""import { PureComponent } from 'react';
51
+
52
+ class OptimizedList extends PureComponent {
53
+ render() {
54
+ return <ul>Items</ul>;
55
+ }
56
+ }
57
+ """)
58
+
59
+ violations = check_no_class_components([str(test_file)])
60
+
61
+ assert len(violations) == 1
62
+ assert violations[0].file == str(test_file)
63
+ assert violations[0].line == 3
64
+ assert "class component" in violations[0].message.lower()
65
+
66
+
67
+ def test_class_component_extends_react_purecomponent_should_fail(tmp_path: Path) -> None:
68
+ """Class component using React.PureComponent should be detected."""
69
+ test_file = tmp_path / "BadReactPureComponent.tsx"
70
+ test_file.write_text("""import React from 'react';
71
+
72
+ class Dashboard extends React.PureComponent {
73
+ render() {
74
+ return <div>Dashboard</div>;
75
+ }
76
+ }
77
+ """)
78
+
79
+ violations = check_no_class_components([str(test_file)])
80
+
81
+ assert len(violations) == 1
82
+ assert violations[0].file == str(test_file)
83
+ assert violations[0].line == 3
84
+ assert "class component" in violations[0].message.lower()
85
+
86
+
87
+ def test_functional_component_should_pass(tmp_path: Path) -> None:
88
+ """Functional components should not trigger violations."""
89
+ test_file = tmp_path / "GoodComponent.tsx"
90
+ test_file.write_text("""import React from 'react';
91
+
92
+ function UserProfile() {
93
+ return <div>Profile</div>;
94
+ }
95
+
96
+ export const TaskList: React.FC = () => {
97
+ return <ul>Tasks</ul>;
98
+ };
99
+ """)
100
+
101
+ violations = check_no_class_components([str(test_file)])
102
+
103
+ assert len(violations) == 0
104
+
105
+
106
+ def test_error_boundary_class_should_pass(tmp_path: Path) -> None:
107
+ """Error boundary classes should be allowed (documented exception)."""
108
+ test_file = tmp_path / "ErrorBoundary.tsx"
109
+ test_file.write_text("""import { Component, ErrorInfo, ReactNode } from 'react';
110
+
111
+ interface Props {
112
+ children: ReactNode;
113
+ }
114
+
115
+ interface State {
116
+ hasError: boolean;
117
+ }
118
+
119
+ class ErrorBoundary extends Component<Props, State> {
120
+ state = { hasError: false };
121
+
122
+ static getDerivedStateFromError(): State {
123
+ return { hasError: true };
124
+ }
125
+
126
+ componentDidCatch(error: Error, errorInfo: ErrorInfo): void {
127
+ console.error('Error caught:', error, errorInfo);
128
+ }
129
+
130
+ render() {
131
+ if (this.state.hasError) {
132
+ return <h1>Something went wrong.</h1>;
133
+ }
134
+ return this.props.children;
135
+ }
136
+ }
137
+ """)
138
+
139
+ violations = check_no_class_components([str(test_file)])
140
+
141
+ assert len(violations) == 0
142
+
143
+
144
+ def test_multiple_files_with_mixed_violations(tmp_path: Path) -> None:
145
+ """Should detect violations across multiple files."""
146
+ good_file = tmp_path / "GoodComponent.tsx"
147
+ good_file.write_text("""function MyComponent() {
148
+ return <div>Good</div>;
149
+ }
150
+ """)
151
+
152
+ bad_file1 = tmp_path / "BadComponent1.tsx"
153
+ bad_file1.write_text("""import { Component } from 'react';
154
+
155
+ class Bad1 extends Component {
156
+ render() { return <div>Bad</div>; }
157
+ }
158
+ """)
159
+
160
+ bad_file2 = tmp_path / "BadComponent2.tsx"
161
+ bad_file2.write_text("""import React from 'react';
162
+
163
+ class Bad2 extends React.Component {
164
+ render() { return <div>Bad</div>; }
165
+ }
166
+ """)
167
+
168
+ violations = check_no_class_components([
169
+ str(good_file),
170
+ str(bad_file1),
171
+ str(bad_file2)
172
+ ])
173
+
174
+ assert len(violations) == 2
175
+ assert any(str(bad_file1) in v.file for v in violations)
176
+ assert any(str(bad_file2) in v.file for v in violations)
177
+
178
+
179
+ def test_non_react_class_should_pass(tmp_path: Path) -> None:
180
+ """Regular TypeScript classes should not trigger violations."""
181
+ test_file = tmp_path / "RegularClass.ts"
182
+ test_file.write_text("""class UserService {
183
+ fetchUser(id: string): Promise<User> {
184
+ return fetch(`/api/users/${id}`).then(r => r.json());
185
+ }
186
+ }
187
+
188
+ class TaskManager extends EventEmitter {
189
+ private tasks: Task[] = [];
190
+ }
191
+ """)
192
+
193
+ violations = check_no_class_components([str(test_file)])
194
+
195
+ assert len(violations) == 0
196
+
197
+
198
+ def test_jsx_file_with_class_component_should_fail(tmp_path: Path) -> None:
199
+ """Should detect class components in .jsx files too."""
200
+ test_file = tmp_path / "OldComponent.jsx"
201
+ test_file.write_text("""const React = require('react');
202
+
203
+ class OldComponent extends React.Component {
204
+ render() {
205
+ return <div>Old style</div>;
206
+ }
207
+ }
208
+ """)
209
+
210
+ violations = check_no_class_components([str(test_file)])
211
+
212
+ assert len(violations) == 1
213
+ assert violations[0].file == str(test_file)
@@ -0,0 +1,25 @@
1
+ ============================= test session starts =============================
2
+ platform -- Python X.Y.Z, pytest-X.Y.Z -- python
3
+ cachedir: .pytest_cache
4
+ benchmark: 5.1.0 (defaults: timer=time.perf_counter disable_gc=False min_rounds=5 min_time=0.000005 max_time=1.0 calibration_precision=10 warmup=False warmup_iterations=100000)
5
+ rootdir: hooks/validators
6
+ plugins: anyio-4.9.0, asyncio-1.1.0, benchmark-5.1.0, cov-6.2.1, mock-3.14.1
7
+ asyncio: mode=Mode.STRICT, asyncio_default_fixture_loop_scope=None, asyncio_default_test_loop_scope=function
8
+ collecting ... collected 88 items / 2 errors
9
+
10
+ =================================== ERRORS ====================================
11
+ ___ ERROR collecting test_files/skip_decorators/test_04_skip_with_parens.py ___
12
+ test_files\skip_decorators\test_04_skip_with_parens.py:6: in <module>
13
+ @skip()
14
+ ^^^^^^
15
+ E TypeError: skip() missing 1 required positional argument: 'reason'
16
+ _____________ ERROR collecting test_files/test_async_functions.py _____________
17
+ test_files\test_async_functions.py:11: in <module>
18
+ @asyncio.coroutine
19
+ ^^^^^^^^^^^^^^^^^
20
+ E AttributeError: module 'asyncio' has no attribute 'coroutine'. Did you mean: 'coroutines'?
21
+ =========================== short test summary info ===========================
22
+ ERROR test_files/skip_decorators/test_04_skip_with_parens.py - TypeError: ski...
23
+ ERROR test_files/test_async_functions.py - AttributeError: module 'asyncio' h...
24
+ !!!!!!!!!!!!!!!!!!! Interrupted: 2 errors during collection !!!!!!!!!!!!!!!!!!!
25
+ ============================== 2 errors in 0.14s ==============================
@@ -0,0 +1,27 @@
1
+ """Tests for ruff integration module."""
2
+
3
+ from pathlib import Path
4
+ from unittest.mock import patch
5
+
6
+ from ruff_integration import RuffResult, check_ruff_available, run_ruff_check
7
+
8
+
9
+ def test_ruff_result_dataclass() -> None:
10
+ """Test RuffResult dataclass creation."""
11
+ result = RuffResult(passed=True, output="test", fixed_count=0)
12
+ assert result.passed is True
13
+ assert result.output == "test"
14
+ assert result.fixed_count == 0
15
+
16
+
17
+ def test_check_ruff_available_returns_false_when_not_installed() -> None:
18
+ """Test that check_ruff_available returns False when ruff not found."""
19
+ with patch("subprocess.run", side_effect=FileNotFoundError):
20
+ assert check_ruff_available() is False
21
+
22
+
23
+ def test_run_ruff_check_returns_passed_for_empty_files() -> None:
24
+ """Test that run_ruff_check passes with no files."""
25
+ result = run_ruff_check([])
26
+ assert result.passed is True
27
+ assert "No files" in result.output