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.
- src/__init__.py +1 -0
- src/analyzers/__init__.py +4 -3
- src/analyzers/ast_utils.py +54 -0
- src/analyzers/rust_base.py +155 -0
- src/analyzers/rust_context.py +141 -0
- src/analyzers/typescript_base.py +4 -0
- src/cli/__init__.py +30 -0
- src/cli/__main__.py +22 -0
- src/cli/config.py +480 -0
- src/cli/config_merge.py +241 -0
- src/cli/linters/__init__.py +67 -0
- src/cli/linters/code_patterns.py +270 -0
- src/cli/linters/code_smells.py +342 -0
- src/cli/linters/documentation.py +83 -0
- src/cli/linters/performance.py +287 -0
- src/cli/linters/shared.py +331 -0
- src/cli/linters/structure.py +327 -0
- src/cli/linters/structure_quality.py +328 -0
- src/cli/main.py +120 -0
- src/cli/utils.py +395 -0
- src/cli_main.py +37 -0
- src/config.py +44 -27
- src/core/base.py +95 -5
- src/core/cli_utils.py +19 -2
- src/core/config_parser.py +36 -6
- src/core/constants.py +54 -0
- src/core/linter_utils.py +95 -6
- src/core/python_lint_rule.py +101 -0
- src/core/registry.py +1 -1
- src/core/rule_discovery.py +147 -84
- src/core/types.py +13 -0
- src/core/violation_builder.py +78 -15
- src/core/violation_utils.py +69 -0
- src/formatters/__init__.py +22 -0
- src/formatters/sarif.py +202 -0
- src/linter_config/directive_markers.py +109 -0
- src/linter_config/ignore.py +254 -395
- src/linter_config/loader.py +45 -12
- src/linter_config/pattern_utils.py +65 -0
- src/linter_config/rule_matcher.py +89 -0
- src/linters/collection_pipeline/__init__.py +90 -0
- src/linters/collection_pipeline/any_all_analyzer.py +281 -0
- src/linters/collection_pipeline/ast_utils.py +40 -0
- src/linters/collection_pipeline/config.py +75 -0
- src/linters/collection_pipeline/continue_analyzer.py +94 -0
- src/linters/collection_pipeline/detector.py +360 -0
- src/linters/collection_pipeline/filter_map_analyzer.py +402 -0
- src/linters/collection_pipeline/linter.py +420 -0
- src/linters/collection_pipeline/suggestion_builder.py +130 -0
- src/linters/cqs/__init__.py +54 -0
- src/linters/cqs/config.py +55 -0
- src/linters/cqs/function_analyzer.py +201 -0
- src/linters/cqs/input_detector.py +139 -0
- src/linters/cqs/linter.py +159 -0
- src/linters/cqs/output_detector.py +84 -0
- src/linters/cqs/python_analyzer.py +54 -0
- src/linters/cqs/types.py +82 -0
- src/linters/cqs/typescript_cqs_analyzer.py +61 -0
- src/linters/cqs/typescript_function_analyzer.py +192 -0
- src/linters/cqs/typescript_input_detector.py +203 -0
- src/linters/cqs/typescript_output_detector.py +117 -0
- src/linters/cqs/violation_builder.py +94 -0
- src/linters/dry/base_token_analyzer.py +16 -9
- src/linters/dry/block_filter.py +125 -22
- src/linters/dry/block_grouper.py +4 -0
- src/linters/dry/cache.py +142 -94
- src/linters/dry/cache_query.py +4 -0
- src/linters/dry/config.py +68 -21
- src/linters/dry/constant.py +92 -0
- src/linters/dry/constant_matcher.py +223 -0
- src/linters/dry/constant_violation_builder.py +98 -0
- src/linters/dry/duplicate_storage.py +20 -82
- src/linters/dry/file_analyzer.py +15 -50
- src/linters/dry/inline_ignore.py +7 -16
- src/linters/dry/linter.py +182 -54
- src/linters/dry/python_analyzer.py +108 -336
- src/linters/dry/python_constant_extractor.py +100 -0
- src/linters/dry/single_statement_detector.py +417 -0
- src/linters/dry/storage_initializer.py +9 -18
- src/linters/dry/token_hasher.py +129 -71
- src/linters/dry/typescript_analyzer.py +68 -380
- src/linters/dry/typescript_constant_extractor.py +138 -0
- src/linters/dry/typescript_statement_detector.py +255 -0
- src/linters/dry/typescript_value_extractor.py +70 -0
- src/linters/dry/violation_builder.py +4 -0
- src/linters/dry/violation_filter.py +9 -5
- src/linters/dry/violation_generator.py +71 -14
- src/linters/file_header/__init__.py +24 -0
- src/linters/file_header/atemporal_detector.py +105 -0
- src/linters/file_header/base_parser.py +93 -0
- src/linters/file_header/bash_parser.py +66 -0
- src/linters/file_header/config.py +140 -0
- src/linters/file_header/css_parser.py +70 -0
- src/linters/file_header/field_validator.py +72 -0
- src/linters/file_header/linter.py +309 -0
- src/linters/file_header/markdown_parser.py +130 -0
- src/linters/file_header/python_parser.py +42 -0
- src/linters/file_header/typescript_parser.py +73 -0
- src/linters/file_header/violation_builder.py +79 -0
- src/linters/file_placement/config_loader.py +3 -1
- src/linters/file_placement/directory_matcher.py +4 -0
- src/linters/file_placement/linter.py +74 -31
- src/linters/file_placement/pattern_matcher.py +41 -6
- src/linters/file_placement/pattern_validator.py +31 -12
- src/linters/file_placement/rule_checker.py +12 -7
- src/linters/lazy_ignores/__init__.py +43 -0
- src/linters/lazy_ignores/config.py +74 -0
- src/linters/lazy_ignores/directive_utils.py +164 -0
- src/linters/lazy_ignores/header_parser.py +177 -0
- src/linters/lazy_ignores/linter.py +158 -0
- src/linters/lazy_ignores/matcher.py +168 -0
- src/linters/lazy_ignores/python_analyzer.py +209 -0
- src/linters/lazy_ignores/rule_id_utils.py +180 -0
- src/linters/lazy_ignores/skip_detector.py +298 -0
- src/linters/lazy_ignores/types.py +71 -0
- src/linters/lazy_ignores/typescript_analyzer.py +146 -0
- src/linters/lazy_ignores/violation_builder.py +135 -0
- src/linters/lbyl/__init__.py +31 -0
- src/linters/lbyl/config.py +63 -0
- src/linters/lbyl/linter.py +67 -0
- src/linters/lbyl/pattern_detectors/__init__.py +53 -0
- src/linters/lbyl/pattern_detectors/base.py +63 -0
- src/linters/lbyl/pattern_detectors/dict_key_detector.py +107 -0
- src/linters/lbyl/pattern_detectors/division_check_detector.py +232 -0
- src/linters/lbyl/pattern_detectors/file_exists_detector.py +220 -0
- src/linters/lbyl/pattern_detectors/hasattr_detector.py +119 -0
- src/linters/lbyl/pattern_detectors/isinstance_detector.py +119 -0
- src/linters/lbyl/pattern_detectors/len_check_detector.py +173 -0
- src/linters/lbyl/pattern_detectors/none_check_detector.py +146 -0
- src/linters/lbyl/pattern_detectors/string_validator_detector.py +145 -0
- src/linters/lbyl/python_analyzer.py +215 -0
- src/linters/lbyl/violation_builder.py +354 -0
- src/linters/magic_numbers/__init__.py +48 -0
- src/linters/magic_numbers/config.py +82 -0
- src/linters/magic_numbers/context_analyzer.py +249 -0
- src/linters/magic_numbers/linter.py +462 -0
- src/linters/magic_numbers/python_analyzer.py +64 -0
- src/linters/magic_numbers/typescript_analyzer.py +215 -0
- src/linters/magic_numbers/typescript_ignore_checker.py +81 -0
- src/linters/magic_numbers/violation_builder.py +98 -0
- src/linters/method_property/__init__.py +49 -0
- src/linters/method_property/config.py +138 -0
- src/linters/method_property/linter.py +414 -0
- src/linters/method_property/python_analyzer.py +473 -0
- src/linters/method_property/violation_builder.py +119 -0
- src/linters/nesting/__init__.py +6 -2
- src/linters/nesting/config.py +6 -3
- src/linters/nesting/linter.py +31 -34
- src/linters/nesting/python_analyzer.py +4 -0
- src/linters/nesting/typescript_analyzer.py +6 -11
- src/linters/nesting/violation_builder.py +1 -0
- src/linters/performance/__init__.py +91 -0
- src/linters/performance/config.py +43 -0
- src/linters/performance/constants.py +49 -0
- src/linters/performance/linter.py +149 -0
- src/linters/performance/python_analyzer.py +365 -0
- src/linters/performance/regex_analyzer.py +312 -0
- src/linters/performance/regex_linter.py +139 -0
- src/linters/performance/typescript_analyzer.py +236 -0
- src/linters/performance/violation_builder.py +160 -0
- src/linters/print_statements/__init__.py +53 -0
- src/linters/print_statements/config.py +78 -0
- src/linters/print_statements/linter.py +413 -0
- src/linters/print_statements/python_analyzer.py +153 -0
- src/linters/print_statements/typescript_analyzer.py +125 -0
- src/linters/print_statements/violation_builder.py +96 -0
- src/linters/srp/__init__.py +3 -3
- src/linters/srp/class_analyzer.py +11 -7
- src/linters/srp/config.py +12 -6
- src/linters/srp/heuristics.py +56 -22
- src/linters/srp/linter.py +47 -39
- src/linters/srp/python_analyzer.py +55 -20
- src/linters/srp/typescript_metrics_calculator.py +110 -50
- src/linters/stateless_class/__init__.py +25 -0
- src/linters/stateless_class/config.py +58 -0
- src/linters/stateless_class/linter.py +349 -0
- src/linters/stateless_class/python_analyzer.py +290 -0
- src/linters/stringly_typed/__init__.py +36 -0
- src/linters/stringly_typed/config.py +189 -0
- src/linters/stringly_typed/context_filter.py +451 -0
- src/linters/stringly_typed/function_call_violation_builder.py +135 -0
- src/linters/stringly_typed/ignore_checker.py +100 -0
- src/linters/stringly_typed/ignore_utils.py +51 -0
- src/linters/stringly_typed/linter.py +376 -0
- src/linters/stringly_typed/python/__init__.py +33 -0
- src/linters/stringly_typed/python/analyzer.py +348 -0
- src/linters/stringly_typed/python/call_tracker.py +175 -0
- src/linters/stringly_typed/python/comparison_tracker.py +257 -0
- src/linters/stringly_typed/python/condition_extractor.py +134 -0
- src/linters/stringly_typed/python/conditional_detector.py +179 -0
- src/linters/stringly_typed/python/constants.py +21 -0
- src/linters/stringly_typed/python/match_analyzer.py +94 -0
- src/linters/stringly_typed/python/validation_detector.py +189 -0
- src/linters/stringly_typed/python/variable_extractor.py +96 -0
- src/linters/stringly_typed/storage.py +620 -0
- src/linters/stringly_typed/storage_initializer.py +45 -0
- src/linters/stringly_typed/typescript/__init__.py +28 -0
- src/linters/stringly_typed/typescript/analyzer.py +157 -0
- src/linters/stringly_typed/typescript/call_tracker.py +335 -0
- src/linters/stringly_typed/typescript/comparison_tracker.py +378 -0
- src/linters/stringly_typed/violation_generator.py +419 -0
- src/orchestrator/core.py +264 -16
- src/orchestrator/language_detector.py +5 -3
- src/templates/thailint_config_template.yaml +354 -0
- src/utils/project_root.py +138 -16
- thailint-0.15.3.dist-info/METADATA +187 -0
- thailint-0.15.3.dist-info/RECORD +226 -0
- {thailint-0.2.0.dist-info → thailint-0.15.3.dist-info}/WHEEL +1 -1
- thailint-0.15.3.dist-info/entry_points.txt +4 -0
- src/cli.py +0 -1055
- thailint-0.2.0.dist-info/METADATA +0 -980
- thailint-0.2.0.dist-info/RECORD +0 -75
- thailint-0.2.0.dist-info/entry_points.txt +0 -4
- {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).
|
|
8
|
-
|
|
9
|
-
|
|
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
|
|
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:
|
|
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
|
-
|
|
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
|
|
29
|
-
|
|
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
|
|
62
|
-
|
|
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
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
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
|