thailint 0.2.0__py3-none-any.whl → 0.15.3__py3-none-any.whl

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 (214) hide show
  1. src/__init__.py +1 -0
  2. src/analyzers/__init__.py +4 -3
  3. src/analyzers/ast_utils.py +54 -0
  4. src/analyzers/rust_base.py +155 -0
  5. src/analyzers/rust_context.py +141 -0
  6. src/analyzers/typescript_base.py +4 -0
  7. src/cli/__init__.py +30 -0
  8. src/cli/__main__.py +22 -0
  9. src/cli/config.py +480 -0
  10. src/cli/config_merge.py +241 -0
  11. src/cli/linters/__init__.py +67 -0
  12. src/cli/linters/code_patterns.py +270 -0
  13. src/cli/linters/code_smells.py +342 -0
  14. src/cli/linters/documentation.py +83 -0
  15. src/cli/linters/performance.py +287 -0
  16. src/cli/linters/shared.py +331 -0
  17. src/cli/linters/structure.py +327 -0
  18. src/cli/linters/structure_quality.py +328 -0
  19. src/cli/main.py +120 -0
  20. src/cli/utils.py +395 -0
  21. src/cli_main.py +37 -0
  22. src/config.py +44 -27
  23. src/core/base.py +95 -5
  24. src/core/cli_utils.py +19 -2
  25. src/core/config_parser.py +36 -6
  26. src/core/constants.py +54 -0
  27. src/core/linter_utils.py +95 -6
  28. src/core/python_lint_rule.py +101 -0
  29. src/core/registry.py +1 -1
  30. src/core/rule_discovery.py +147 -84
  31. src/core/types.py +13 -0
  32. src/core/violation_builder.py +78 -15
  33. src/core/violation_utils.py +69 -0
  34. src/formatters/__init__.py +22 -0
  35. src/formatters/sarif.py +202 -0
  36. src/linter_config/directive_markers.py +109 -0
  37. src/linter_config/ignore.py +254 -395
  38. src/linter_config/loader.py +45 -12
  39. src/linter_config/pattern_utils.py +65 -0
  40. src/linter_config/rule_matcher.py +89 -0
  41. src/linters/collection_pipeline/__init__.py +90 -0
  42. src/linters/collection_pipeline/any_all_analyzer.py +281 -0
  43. src/linters/collection_pipeline/ast_utils.py +40 -0
  44. src/linters/collection_pipeline/config.py +75 -0
  45. src/linters/collection_pipeline/continue_analyzer.py +94 -0
  46. src/linters/collection_pipeline/detector.py +360 -0
  47. src/linters/collection_pipeline/filter_map_analyzer.py +402 -0
  48. src/linters/collection_pipeline/linter.py +420 -0
  49. src/linters/collection_pipeline/suggestion_builder.py +130 -0
  50. src/linters/cqs/__init__.py +54 -0
  51. src/linters/cqs/config.py +55 -0
  52. src/linters/cqs/function_analyzer.py +201 -0
  53. src/linters/cqs/input_detector.py +139 -0
  54. src/linters/cqs/linter.py +159 -0
  55. src/linters/cqs/output_detector.py +84 -0
  56. src/linters/cqs/python_analyzer.py +54 -0
  57. src/linters/cqs/types.py +82 -0
  58. src/linters/cqs/typescript_cqs_analyzer.py +61 -0
  59. src/linters/cqs/typescript_function_analyzer.py +192 -0
  60. src/linters/cqs/typescript_input_detector.py +203 -0
  61. src/linters/cqs/typescript_output_detector.py +117 -0
  62. src/linters/cqs/violation_builder.py +94 -0
  63. src/linters/dry/base_token_analyzer.py +16 -9
  64. src/linters/dry/block_filter.py +125 -22
  65. src/linters/dry/block_grouper.py +4 -0
  66. src/linters/dry/cache.py +142 -94
  67. src/linters/dry/cache_query.py +4 -0
  68. src/linters/dry/config.py +68 -21
  69. src/linters/dry/constant.py +92 -0
  70. src/linters/dry/constant_matcher.py +223 -0
  71. src/linters/dry/constant_violation_builder.py +98 -0
  72. src/linters/dry/duplicate_storage.py +20 -82
  73. src/linters/dry/file_analyzer.py +15 -50
  74. src/linters/dry/inline_ignore.py +7 -16
  75. src/linters/dry/linter.py +182 -54
  76. src/linters/dry/python_analyzer.py +108 -336
  77. src/linters/dry/python_constant_extractor.py +100 -0
  78. src/linters/dry/single_statement_detector.py +417 -0
  79. src/linters/dry/storage_initializer.py +9 -18
  80. src/linters/dry/token_hasher.py +129 -71
  81. src/linters/dry/typescript_analyzer.py +68 -380
  82. src/linters/dry/typescript_constant_extractor.py +138 -0
  83. src/linters/dry/typescript_statement_detector.py +255 -0
  84. src/linters/dry/typescript_value_extractor.py +70 -0
  85. src/linters/dry/violation_builder.py +4 -0
  86. src/linters/dry/violation_filter.py +9 -5
  87. src/linters/dry/violation_generator.py +71 -14
  88. src/linters/file_header/__init__.py +24 -0
  89. src/linters/file_header/atemporal_detector.py +105 -0
  90. src/linters/file_header/base_parser.py +93 -0
  91. src/linters/file_header/bash_parser.py +66 -0
  92. src/linters/file_header/config.py +140 -0
  93. src/linters/file_header/css_parser.py +70 -0
  94. src/linters/file_header/field_validator.py +72 -0
  95. src/linters/file_header/linter.py +309 -0
  96. src/linters/file_header/markdown_parser.py +130 -0
  97. src/linters/file_header/python_parser.py +42 -0
  98. src/linters/file_header/typescript_parser.py +73 -0
  99. src/linters/file_header/violation_builder.py +79 -0
  100. src/linters/file_placement/config_loader.py +3 -1
  101. src/linters/file_placement/directory_matcher.py +4 -0
  102. src/linters/file_placement/linter.py +74 -31
  103. src/linters/file_placement/pattern_matcher.py +41 -6
  104. src/linters/file_placement/pattern_validator.py +31 -12
  105. src/linters/file_placement/rule_checker.py +12 -7
  106. src/linters/lazy_ignores/__init__.py +43 -0
  107. src/linters/lazy_ignores/config.py +74 -0
  108. src/linters/lazy_ignores/directive_utils.py +164 -0
  109. src/linters/lazy_ignores/header_parser.py +177 -0
  110. src/linters/lazy_ignores/linter.py +158 -0
  111. src/linters/lazy_ignores/matcher.py +168 -0
  112. src/linters/lazy_ignores/python_analyzer.py +209 -0
  113. src/linters/lazy_ignores/rule_id_utils.py +180 -0
  114. src/linters/lazy_ignores/skip_detector.py +298 -0
  115. src/linters/lazy_ignores/types.py +71 -0
  116. src/linters/lazy_ignores/typescript_analyzer.py +146 -0
  117. src/linters/lazy_ignores/violation_builder.py +135 -0
  118. src/linters/lbyl/__init__.py +31 -0
  119. src/linters/lbyl/config.py +63 -0
  120. src/linters/lbyl/linter.py +67 -0
  121. src/linters/lbyl/pattern_detectors/__init__.py +53 -0
  122. src/linters/lbyl/pattern_detectors/base.py +63 -0
  123. src/linters/lbyl/pattern_detectors/dict_key_detector.py +107 -0
  124. src/linters/lbyl/pattern_detectors/division_check_detector.py +232 -0
  125. src/linters/lbyl/pattern_detectors/file_exists_detector.py +220 -0
  126. src/linters/lbyl/pattern_detectors/hasattr_detector.py +119 -0
  127. src/linters/lbyl/pattern_detectors/isinstance_detector.py +119 -0
  128. src/linters/lbyl/pattern_detectors/len_check_detector.py +173 -0
  129. src/linters/lbyl/pattern_detectors/none_check_detector.py +146 -0
  130. src/linters/lbyl/pattern_detectors/string_validator_detector.py +145 -0
  131. src/linters/lbyl/python_analyzer.py +215 -0
  132. src/linters/lbyl/violation_builder.py +354 -0
  133. src/linters/magic_numbers/__init__.py +48 -0
  134. src/linters/magic_numbers/config.py +82 -0
  135. src/linters/magic_numbers/context_analyzer.py +249 -0
  136. src/linters/magic_numbers/linter.py +462 -0
  137. src/linters/magic_numbers/python_analyzer.py +64 -0
  138. src/linters/magic_numbers/typescript_analyzer.py +215 -0
  139. src/linters/magic_numbers/typescript_ignore_checker.py +81 -0
  140. src/linters/magic_numbers/violation_builder.py +98 -0
  141. src/linters/method_property/__init__.py +49 -0
  142. src/linters/method_property/config.py +138 -0
  143. src/linters/method_property/linter.py +414 -0
  144. src/linters/method_property/python_analyzer.py +473 -0
  145. src/linters/method_property/violation_builder.py +119 -0
  146. src/linters/nesting/__init__.py +6 -2
  147. src/linters/nesting/config.py +6 -3
  148. src/linters/nesting/linter.py +31 -34
  149. src/linters/nesting/python_analyzer.py +4 -0
  150. src/linters/nesting/typescript_analyzer.py +6 -11
  151. src/linters/nesting/violation_builder.py +1 -0
  152. src/linters/performance/__init__.py +91 -0
  153. src/linters/performance/config.py +43 -0
  154. src/linters/performance/constants.py +49 -0
  155. src/linters/performance/linter.py +149 -0
  156. src/linters/performance/python_analyzer.py +365 -0
  157. src/linters/performance/regex_analyzer.py +312 -0
  158. src/linters/performance/regex_linter.py +139 -0
  159. src/linters/performance/typescript_analyzer.py +236 -0
  160. src/linters/performance/violation_builder.py +160 -0
  161. src/linters/print_statements/__init__.py +53 -0
  162. src/linters/print_statements/config.py +78 -0
  163. src/linters/print_statements/linter.py +413 -0
  164. src/linters/print_statements/python_analyzer.py +153 -0
  165. src/linters/print_statements/typescript_analyzer.py +125 -0
  166. src/linters/print_statements/violation_builder.py +96 -0
  167. src/linters/srp/__init__.py +3 -3
  168. src/linters/srp/class_analyzer.py +11 -7
  169. src/linters/srp/config.py +12 -6
  170. src/linters/srp/heuristics.py +56 -22
  171. src/linters/srp/linter.py +47 -39
  172. src/linters/srp/python_analyzer.py +55 -20
  173. src/linters/srp/typescript_metrics_calculator.py +110 -50
  174. src/linters/stateless_class/__init__.py +25 -0
  175. src/linters/stateless_class/config.py +58 -0
  176. src/linters/stateless_class/linter.py +349 -0
  177. src/linters/stateless_class/python_analyzer.py +290 -0
  178. src/linters/stringly_typed/__init__.py +36 -0
  179. src/linters/stringly_typed/config.py +189 -0
  180. src/linters/stringly_typed/context_filter.py +451 -0
  181. src/linters/stringly_typed/function_call_violation_builder.py +135 -0
  182. src/linters/stringly_typed/ignore_checker.py +100 -0
  183. src/linters/stringly_typed/ignore_utils.py +51 -0
  184. src/linters/stringly_typed/linter.py +376 -0
  185. src/linters/stringly_typed/python/__init__.py +33 -0
  186. src/linters/stringly_typed/python/analyzer.py +348 -0
  187. src/linters/stringly_typed/python/call_tracker.py +175 -0
  188. src/linters/stringly_typed/python/comparison_tracker.py +257 -0
  189. src/linters/stringly_typed/python/condition_extractor.py +134 -0
  190. src/linters/stringly_typed/python/conditional_detector.py +179 -0
  191. src/linters/stringly_typed/python/constants.py +21 -0
  192. src/linters/stringly_typed/python/match_analyzer.py +94 -0
  193. src/linters/stringly_typed/python/validation_detector.py +189 -0
  194. src/linters/stringly_typed/python/variable_extractor.py +96 -0
  195. src/linters/stringly_typed/storage.py +620 -0
  196. src/linters/stringly_typed/storage_initializer.py +45 -0
  197. src/linters/stringly_typed/typescript/__init__.py +28 -0
  198. src/linters/stringly_typed/typescript/analyzer.py +157 -0
  199. src/linters/stringly_typed/typescript/call_tracker.py +335 -0
  200. src/linters/stringly_typed/typescript/comparison_tracker.py +378 -0
  201. src/linters/stringly_typed/violation_generator.py +419 -0
  202. src/orchestrator/core.py +264 -16
  203. src/orchestrator/language_detector.py +5 -3
  204. src/templates/thailint_config_template.yaml +354 -0
  205. src/utils/project_root.py +138 -16
  206. thailint-0.15.3.dist-info/METADATA +187 -0
  207. thailint-0.15.3.dist-info/RECORD +226 -0
  208. {thailint-0.2.0.dist-info → thailint-0.15.3.dist-info}/WHEEL +1 -1
  209. thailint-0.15.3.dist-info/entry_points.txt +4 -0
  210. src/cli.py +0 -1055
  211. thailint-0.2.0.dist-info/METADATA +0 -980
  212. thailint-0.2.0.dist-info/RECORD +0 -75
  213. thailint-0.2.0.dist-info/entry_points.txt +0 -4
  214. {thailint-0.2.0.dist-info → thailint-0.15.3.dist-info/licenses}/LICENSE +0 -0
@@ -0,0 +1,354 @@
1
+ # thai-lint Configuration File
2
+ # Generated by: thailint init-config
3
+ #
4
+ # For non-interactive mode (AI agents): thailint init-config --non-interactive
5
+ #
6
+ # Full documentation: https://github.com/your-org/thai-lint
7
+
8
+ # ============================================================================
9
+ # MAGIC NUMBERS LINTER
10
+ # ============================================================================
11
+ # Detects unnamed numeric literals that should be extracted as constants
12
+ #
13
+ # Preset: {{PRESET}}
14
+ #
15
+ magic-numbers:
16
+ enabled: true
17
+
18
+ # Numbers that are acceptable without being named constants
19
+ # Default: [-1, 0, 1, 2, 3, 4, 5, 10, 100, 1000]
20
+ allowed_numbers: {{ALLOWED_NUMBERS}}
21
+
22
+ # Maximum integer allowed in range() or enumerate() without flagging
23
+ # Default: 10
24
+ max_small_integer: {{MAX_SMALL_INTEGER}}
25
+
26
+ # -------------------------------------------------------------------------
27
+ # OPTIONAL: Uncomment to add time conversions (lenient mode)
28
+ # -------------------------------------------------------------------------
29
+ # allowed_numbers: [-1, 0, 1, 2, 3, 4, 5, 10, 60, 100, 1000, 3600]
30
+
31
+ # -------------------------------------------------------------------------
32
+ # OPTIONAL: Uncomment to add common HTTP status codes
33
+ # -------------------------------------------------------------------------
34
+ # allowed_numbers: [-1, 0, 1, 2, 3, 4, 5, 10, 100, 200, 201, 204, 400, 401, 403, 404, 500, 502, 503, 1000]
35
+
36
+ # -------------------------------------------------------------------------
37
+ # OPTIONAL: Uncomment to add decimal proportions (0.0-1.0)
38
+ # -------------------------------------------------------------------------
39
+ # allowed_numbers: [-1, 0, 1, 2, 3, 4, 5, 10, 100, 1000, 0.0, 0.1, 0.2, 0.25, 0.3, 0.4, 0.5, 0.6, 0.7, 0.75, 0.8, 0.9, 1.0]
40
+
41
+ # ============================================================================
42
+ # NESTING LINTER
43
+ # ============================================================================
44
+ # Checks for excessive nesting depth (if/for/while/try statements)
45
+ #
46
+ nesting:
47
+ enabled: true
48
+
49
+ # Maximum nesting depth allowed
50
+ # Default: 4
51
+ max_nesting_depth: 4
52
+
53
+ # ============================================================================
54
+ # SINGLE RESPONSIBILITY PRINCIPLE (SRP) LINTER
55
+ # ============================================================================
56
+ # Detects classes that may have too many responsibilities
57
+ #
58
+ srp:
59
+ enabled: true
60
+
61
+ # Maximum methods per class
62
+ # Default: 7
63
+ max_methods: 7
64
+
65
+ # Maximum lines of code per class
66
+ # Default: 200
67
+ max_loc: 200
68
+
69
+ # ============================================================================
70
+ # DRY (DON'T REPEAT YOURSELF) LINTER
71
+ # ============================================================================
72
+ # Detects duplicate code blocks
73
+ #
74
+ dry:
75
+ enabled: true
76
+
77
+ # Minimum lines for a block to be considered duplicate
78
+ # Default: 6
79
+ min_duplicate_lines: 6
80
+
81
+ # Enable SQLite caching for faster incremental scans
82
+ # Default: true
83
+ cache_enabled: true
84
+
85
+ # Cache file location (relative to project root)
86
+ # Default: .thailint-cache/dry.db
87
+ cache_path: .thailint-cache/dry.db
88
+
89
+ # ============================================================================
90
+ # FILE PLACEMENT LINTER
91
+ # ============================================================================
92
+ # Ensures files are in appropriate directories
93
+ #
94
+ file-placement:
95
+ enabled: true
96
+
97
+ # Rules for file placement
98
+ rules:
99
+ # Test files should be in tests/ directory
100
+ - pattern: "test_*.py"
101
+ required_dir: "tests/"
102
+ message: "Test files must be in tests/ directory"
103
+
104
+ # Config files should be in config/ or root
105
+ - pattern: "*config*.py"
106
+ required_dir: ["config/", "./"]
107
+ message: "Config files should be in config/ or project root"
108
+
109
+ # ============================================================================
110
+ # PRINT STATEMENTS LINTER
111
+ # ============================================================================
112
+ # Detects print()/console.* statements that should use proper logging
113
+ #
114
+ print-statements:
115
+ enabled: true
116
+
117
+ # Allow print() in if __name__ == "__main__": blocks (Python only)
118
+ # Default: true
119
+ allow_in_scripts: true
120
+
121
+ # Console methods to detect in TypeScript/JavaScript
122
+ # Default: [log, warn, error, debug, info]
123
+ console_methods:
124
+ - log
125
+ - warn
126
+ - error
127
+ - debug
128
+ - info
129
+
130
+ # File patterns to ignore (glob syntax)
131
+ # ignore:
132
+ # - "scripts/**"
133
+ # - "**/debug.py"
134
+
135
+ # ============================================================================
136
+ # STRINGLY-TYPED LINTER
137
+ # ============================================================================
138
+ # Detects "stringly typed" code patterns where strings are used instead of
139
+ # proper enums - e.g., if env == "production": ... repeated across files
140
+ #
141
+ stringly-typed:
142
+ enabled: true
143
+
144
+ # Minimum occurrences across files to flag a violation
145
+ # Default: 2
146
+ min_occurrences: 2
147
+
148
+ # Minimum unique string values to suggest creating an enum
149
+ # Default: 2
150
+ min_values_for_enum: 2
151
+
152
+ # Maximum unique string values to suggest an enum (above this, probably not enum-worthy)
153
+ # Default: 6
154
+ max_values_for_enum: 6
155
+
156
+ # Whether to require cross-file occurrences to flag violations
157
+ # Default: true
158
+ require_cross_file: true
159
+
160
+ # -------------------------------------------------------------------------
161
+ # OPTIONAL: String value sets that are acceptable (won't be flagged)
162
+ # -------------------------------------------------------------------------
163
+ # allowed_string_sets:
164
+ # - ["debug", "info", "warning", "error"] # Log levels
165
+ # - ["ASC", "DESC"] # Sort directions
166
+
167
+ # -------------------------------------------------------------------------
168
+ # OPTIONAL: Variable names to exclude from analysis
169
+ # -------------------------------------------------------------------------
170
+ # exclude_variables:
171
+ # - log_level
172
+ # - severity
173
+
174
+ # ============================================================================
175
+ # FILE HEADER LINTER
176
+ # ============================================================================
177
+ # Validates that files have proper documentation headers
178
+ #
179
+ file-header:
180
+ enabled: true
181
+
182
+ # Enforce atemporal language (no "currently", "now", dates)
183
+ # Default: true
184
+ enforce_atemporal: true
185
+
186
+ # -------------------------------------------------------------------------
187
+ # OPTIONAL: Override required fields by language
188
+ # -------------------------------------------------------------------------
189
+ # required_fields:
190
+ # python: [Purpose, Scope, Overview, Dependencies, Exports, Interfaces, Implementation]
191
+ # typescript: [Purpose, Scope, Overview, Dependencies, Exports, Props/Interfaces, State/Behavior]
192
+ # bash: [Purpose, Scope, Overview, Dependencies, Exports, Usage, Environment]
193
+ # markdown: [purpose, scope, overview, audience, status]
194
+ # css: [Purpose, Scope, Overview, Dependencies, Exports, Interfaces, Environment]
195
+
196
+ # -------------------------------------------------------------------------
197
+ # OPTIONAL: File patterns to ignore
198
+ # -------------------------------------------------------------------------
199
+ # ignore:
200
+ # - "test/**"
201
+ # - "**/migrations/**"
202
+ # - "**/__init__.py"
203
+
204
+ # ============================================================================
205
+ # METHOD PROPERTY LINTER
206
+ # ============================================================================
207
+ # Detects methods that should be @property (no args, simple return)
208
+ #
209
+ method-property:
210
+ enabled: true
211
+
212
+ # Maximum statements in method body to suggest @property
213
+ # Default: 3
214
+ max_body_statements: 3
215
+
216
+ # -------------------------------------------------------------------------
217
+ # OPTIONAL: Methods to ignore (exact names)
218
+ # -------------------------------------------------------------------------
219
+ # ignore_methods:
220
+ # - "__str__"
221
+ # - "__repr__"
222
+
223
+ # -------------------------------------------------------------------------
224
+ # OPTIONAL: Additional action verb prefixes to exclude
225
+ # -------------------------------------------------------------------------
226
+ # exclude_prefixes:
227
+ # - "fetch_"
228
+ # - "load_"
229
+
230
+ # -------------------------------------------------------------------------
231
+ # OPTIONAL: File patterns to ignore
232
+ # -------------------------------------------------------------------------
233
+ # ignore:
234
+ # - "tests/**"
235
+
236
+ # ============================================================================
237
+ # STATELESS CLASS LINTER
238
+ # ============================================================================
239
+ # Detects classes with no instance state (should be modules or functions)
240
+ #
241
+ stateless-class:
242
+ enabled: true
243
+
244
+ # Minimum methods to flag a stateless class
245
+ # Default: 2
246
+ min_methods: 2
247
+
248
+ # -------------------------------------------------------------------------
249
+ # OPTIONAL: File patterns to ignore
250
+ # -------------------------------------------------------------------------
251
+ # ignore:
252
+ # - "tests/**"
253
+
254
+ # ============================================================================
255
+ # COLLECTION PIPELINE LINTER
256
+ # ============================================================================
257
+ # Detects "embedded loop filtering" anti-pattern (if/continue in loops)
258
+ # Suggests using filter(), list comprehensions, or generator expressions
259
+ #
260
+ pipeline:
261
+ enabled: true
262
+
263
+ # Minimum if/continue patterns in a loop to flag
264
+ # Default: 1
265
+ min_continues: 1
266
+
267
+ # -------------------------------------------------------------------------
268
+ # OPTIONAL: File patterns to ignore
269
+ # -------------------------------------------------------------------------
270
+ # ignore:
271
+ # - "tests/**"
272
+
273
+ # ============================================================================
274
+ # LAZY IGNORES LINTER
275
+ # ============================================================================
276
+ # Detects unjustified linting suppressions (noqa, type: ignore, etc.)
277
+ # without proper documentation in file headers
278
+ #
279
+ lazy-ignores:
280
+ enabled: true
281
+
282
+ # Pattern-specific toggles
283
+ check_noqa: true
284
+ check_type_ignore: true
285
+ check_pylint_disable: true
286
+ check_nosec: true
287
+ check_ts_ignore: true
288
+ check_eslint_disable: true
289
+ check_thailint_ignore: true
290
+ check_test_skips: true
291
+
292
+ # Check for orphaned suppressions (documented but not used)
293
+ check_orphaned: true
294
+
295
+ # -------------------------------------------------------------------------
296
+ # OPTIONAL: File patterns to ignore
297
+ # -------------------------------------------------------------------------
298
+ # ignore_patterns:
299
+ # - "tests/**"
300
+
301
+ # ============================================================================
302
+ # PERFORMANCE LINTER
303
+ # ============================================================================
304
+ # Detects performance anti-patterns in loops that cause O(n²) behavior
305
+ #
306
+ performance:
307
+ enabled: true
308
+
309
+ # String concatenation in loops (O(n²) pattern)
310
+ # Detects: result += str in for/while loops
311
+ # Suggests: Use "".join() or list append + join
312
+ string-concat-loop:
313
+ enabled: true
314
+ # Report each += separately, or one violation per loop
315
+ # Default: false (one per loop)
316
+ report_each_concat: false
317
+
318
+ # Regex compilation in loops
319
+ # Detects: re.match(), re.search(), re.sub() etc. in loops
320
+ # Suggests: Use re.compile() outside loop
321
+ regex-in-loop:
322
+ enabled: true
323
+
324
+ # -------------------------------------------------------------------------
325
+ # OPTIONAL: File patterns to ignore
326
+ # -------------------------------------------------------------------------
327
+ # ignore:
328
+ # - "tests/**"
329
+ # - "scripts/**"
330
+
331
+ # ============================================================================
332
+ # GLOBAL SETTINGS
333
+ # ============================================================================
334
+ #
335
+ # Exclude patterns (files/directories to ignore)
336
+ exclude:
337
+ - ".git/"
338
+ - ".venv/"
339
+ - "venv/"
340
+ - "node_modules/"
341
+ - "__pycache__/"
342
+ - "*.pyc"
343
+ - ".pytest_cache/"
344
+ - "dist/"
345
+ - "build/"
346
+ - ".eggs/"
347
+
348
+ # Output format (text or json)
349
+ # Default: text
350
+ output_format: text
351
+
352
+ # Exit with error code if violations found
353
+ # Default: true
354
+ fail_on_violations: true
src/utils/project_root.py CHANGED
@@ -4,29 +4,55 @@ Purpose: Centralized project root detection for consistent file placement
4
4
  Scope: Single source of truth for finding project root directory
5
5
 
6
6
  Overview: Uses pyprojroot package to provide reliable project root detection across
7
- different environments (development, CI/CD, user installations). Delegates all
8
- project root detection logic to the industry-standard pyprojroot library which
9
- handles various project markers and edge cases that we cannot anticipate.
7
+ different environments (development, CI/CD, user installations). Falls back to
8
+ manual detection if pyprojroot is not available (e.g., in test environments).
9
+ Searches for standard project markers like .git, .thailint.yaml, and pyproject.toml.
10
10
 
11
- Dependencies: pyprojroot for robust project root detection
11
+ Dependencies: pyprojroot (optional, with manual fallback)
12
12
 
13
13
  Exports: is_project_root(), get_project_root()
14
14
 
15
15
  Interfaces: Path-based functions for checking and finding project roots
16
16
 
17
- Implementation: Pure delegation to pyprojroot with fallback to start_path when no root found
17
+ Implementation: pyprojroot delegation with manual fallback for test environments
18
+
19
+ Suppressions:
20
+ - type:ignore[arg-type]: pyprojroot external library typing issue with Path conversion
18
21
  """
19
22
 
20
23
  from pathlib import Path
21
24
 
22
- from pyprojroot import find_root
25
+ # Try to import pyprojroot, but don't fail if it's not available
26
+ try:
27
+ from pyprojroot import find_root
28
+
29
+ HAS_PYPROJROOT = True
30
+ except ImportError:
31
+ HAS_PYPROJROOT = False
32
+
33
+
34
+ def _has_marker(path: Path, marker_name: str, is_dir: bool = False) -> bool:
35
+ """Check if a directory contains a specific marker.
36
+
37
+ Args:
38
+ path: Directory path to check
39
+ marker_name: Name of marker file or directory
40
+ is_dir: True if marker is a directory, False if it's a file
41
+
42
+ Returns:
43
+ True if marker exists, False otherwise
44
+ """
45
+ marker_path = path / marker_name
46
+ if is_dir:
47
+ return marker_path.is_dir()
48
+ return marker_path.is_file()
23
49
 
24
50
 
25
51
  def is_project_root(path: Path) -> bool:
26
52
  """Check if a directory is a project root.
27
53
 
28
- Uses pyprojroot to detect if the given path is a project root by checking
29
- if finding the root from this path returns the same path.
54
+ Uses pyprojroot if available, otherwise checks for common project markers
55
+ like .git, .thailint.yaml, or pyproject.toml.
30
56
 
31
57
  Args:
32
58
  path: Directory path to check
@@ -43,6 +69,21 @@ def is_project_root(path: Path) -> bool:
43
69
  if not path.exists() or not path.is_dir():
44
70
  return False
45
71
 
72
+ if HAS_PYPROJROOT:
73
+ return _check_root_with_pyprojroot(path)
74
+
75
+ return _check_root_with_markers(path)
76
+
77
+
78
+ def _check_root_with_pyprojroot(path: Path) -> bool:
79
+ """Check if path is project root using pyprojroot.
80
+
81
+ Args:
82
+ path: Directory path to check
83
+
84
+ Returns:
85
+ True if path is a project root, False otherwise
86
+ """
46
87
  try:
47
88
  # Find root from this path - if it equals this path, it's a root
48
89
  found_root = find_root(path)
@@ -52,14 +93,74 @@ def is_project_root(path: Path) -> bool:
52
93
  return False
53
94
 
54
95
 
96
+ def _check_root_with_markers(path: Path) -> bool:
97
+ """Check if path contains project root markers.
98
+
99
+ Args:
100
+ path: Directory path to check
101
+
102
+ Returns:
103
+ True if path contains .git, .thailint.yaml, or pyproject.toml
104
+ """
105
+ return (
106
+ _has_marker(path, ".git", is_dir=True)
107
+ or _has_marker(path, ".thailint.yaml", is_dir=False)
108
+ or _has_marker(path, "pyproject.toml", is_dir=False)
109
+ )
110
+
111
+
112
+ def _try_find_with_criterion(criterion: object, start_path: Path) -> Path | None:
113
+ """Try to find project root with a specific criterion.
114
+
115
+ Args:
116
+ criterion: pyprojroot criterion function (e.g., has_dir(".git"))
117
+ start_path: Path to start searching from
118
+
119
+ Returns:
120
+ Found project root or None if not found
121
+ """
122
+ try:
123
+ return find_root(criterion, start=start_path) # type: ignore[arg-type]
124
+ except (OSError, RuntimeError):
125
+ return None
126
+
127
+
128
+ def _find_root_manual(start_path: Path) -> Path:
129
+ """Manually find project root by walking up directory tree.
130
+
131
+ Fallback implementation when pyprojroot is not available.
132
+
133
+ Args:
134
+ start_path: Directory to start searching from
135
+
136
+ Returns:
137
+ Path to project root, or start_path if no markers found
138
+ """
139
+ current = start_path.resolve()
140
+
141
+ # Walk up the directory tree
142
+ for parent in [current] + list(current.parents):
143
+ # Check for project markers
144
+ if (
145
+ _has_marker(parent, ".git", is_dir=True)
146
+ or _has_marker(parent, ".thailint.yaml", is_dir=False)
147
+ or _has_marker(parent, "pyproject.toml", is_dir=False)
148
+ ):
149
+ return parent
150
+
151
+ # No markers found, return start path
152
+ return current
153
+
154
+
55
155
  def get_project_root(start_path: Path | None = None) -> Path:
56
156
  """Find project root by walking up the directory tree.
57
157
 
58
158
  This is the single source of truth for project root detection.
59
159
  All code that needs to find the project root should use this function.
60
160
 
61
- Uses pyprojroot which searches for standard project markers defined by the
62
- pyprojroot library (git repos, Python projects, etc).
161
+ Uses pyprojroot if available, otherwise uses manual detection searching for
162
+ standard project markers (.git directory, pyproject.toml, .thailint.yaml, etc)
163
+ starting from start_path and walking upward.
63
164
 
64
165
  Args:
65
166
  start_path: Directory to start searching from. If None, uses current working directory.
@@ -76,9 +177,30 @@ def get_project_root(start_path: Path | None = None) -> Path:
76
177
 
77
178
  current = start_path.resolve()
78
179
 
79
- try:
80
- # Use pyprojroot to find the project root
81
- return find_root(current)
82
- except (OSError, RuntimeError):
83
- # No project markers found, return the start path
84
- return current
180
+ if HAS_PYPROJROOT:
181
+ return _find_root_with_pyprojroot(current)
182
+
183
+ # Manual fallback for test environments
184
+ return _find_root_manual(current)
185
+
186
+
187
+ def _find_root_with_pyprojroot(current: Path) -> Path:
188
+ """Find project root using pyprojroot library.
189
+
190
+ Args:
191
+ current: Current path to start searching from
192
+
193
+ Returns:
194
+ Path to project root, or current if no markers found
195
+ """
196
+ from pyprojroot import has_dir, has_file
197
+
198
+ # Search for project root markers in priority order
199
+ # Try .git first (most reliable), then .thailint.yaml, then pyproject.toml
200
+ for criterion in [has_dir(".git"), has_file(".thailint.yaml"), has_file("pyproject.toml")]:
201
+ root = _try_find_with_criterion(criterion, current)
202
+ if root is not None:
203
+ return root
204
+
205
+ # No markers found, return start path
206
+ return current