thailint 0.5.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 +38 -25
- src/core/base.py +7 -2
- src/core/cli_utils.py +19 -2
- src/core/config_parser.py +5 -2
- 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 +120 -20
- src/linters/dry/block_grouper.py +4 -0
- src/linters/dry/cache.py +104 -10
- src/linters/dry/cache_query.py +4 -0
- src/linters/dry/config.py +54 -11
- 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 +5 -4
- src/linters/dry/file_analyzer.py +4 -2
- src/linters/dry/inline_ignore.py +7 -16
- src/linters/dry/linter.py +183 -48
- src/linters/dry/python_analyzer.py +60 -439
- src/linters/dry/python_constant_extractor.py +100 -0
- src/linters/dry/single_statement_detector.py +417 -0
- src/linters/dry/token_hasher.py +116 -112
- src/linters/dry/typescript_analyzer.py +68 -382
- 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 +5 -4
- src/linters/dry/violation_generator.py +71 -14
- src/linters/file_header/atemporal_detector.py +68 -50
- src/linters/file_header/base_parser.py +93 -0
- src/linters/file_header/bash_parser.py +66 -0
- src/linters/file_header/config.py +90 -16
- src/linters/file_header/css_parser.py +70 -0
- src/linters/file_header/field_validator.py +36 -33
- src/linters/file_header/linter.py +140 -144
- src/linters/file_header/markdown_parser.py +130 -0
- src/linters/file_header/python_parser.py +14 -58
- src/linters/file_header/typescript_parser.py +73 -0
- src/linters/file_header/violation_builder.py +13 -12
- src/linters/file_placement/config_loader.py +3 -1
- src/linters/file_placement/directory_matcher.py +4 -0
- src/linters/file_placement/linter.py +66 -34
- 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/context_analyzer.py +227 -225
- src/linters/magic_numbers/linter.py +28 -82
- src/linters/magic_numbers/python_analyzer.py +4 -16
- src/linters/magic_numbers/typescript_analyzer.py +9 -12
- src/linters/magic_numbers/typescript_ignore_checker.py +81 -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/linter.py +24 -16
- src/linters/nesting/python_analyzer.py +4 -0
- src/linters/nesting/typescript_analyzer.py +6 -12
- 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/config.py +7 -12
- src/linters/print_statements/linter.py +26 -43
- src/linters/print_statements/python_analyzer.py +91 -93
- src/linters/print_statements/typescript_analyzer.py +15 -25
- src/linters/print_statements/violation_builder.py +12 -14
- src/linters/srp/class_analyzer.py +11 -7
- src/linters/srp/heuristics.py +56 -22
- src/linters/srp/linter.py +15 -16
- 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 +252 -14
- src/orchestrator/language_detector.py +5 -3
- src/templates/thailint_config_template.yaml +196 -0
- src/utils/project_root.py +3 -0
- thailint-0.15.3.dist-info/METADATA +187 -0
- thailint-0.15.3.dist-info/RECORD +226 -0
- thailint-0.15.3.dist-info/entry_points.txt +4 -0
- src/cli.py +0 -1665
- thailint-0.5.0.dist-info/METADATA +0 -1286
- thailint-0.5.0.dist-info/RECORD +0 -96
- thailint-0.5.0.dist-info/entry_points.txt +0 -4
- {thailint-0.5.0.dist-info → thailint-0.15.3.dist-info}/WHEEL +0 -0
- {thailint-0.5.0.dist-info → thailint-0.15.3.dist-info}/licenses/LICENSE +0 -0
src/cli/config_merge.py
ADDED
|
@@ -0,0 +1,241 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Purpose: Configuration merge utilities for init-config command
|
|
3
|
+
|
|
4
|
+
Scope: Functions for merging missing linter sections into existing config files
|
|
5
|
+
|
|
6
|
+
Overview: Provides utilities for the init-config command to add missing linter sections
|
|
7
|
+
to existing configuration files without overwriting user customizations. Handles
|
|
8
|
+
template parsing, section extraction, missing section identification, and content
|
|
9
|
+
merging while preserving comments and formatting.
|
|
10
|
+
|
|
11
|
+
Dependencies: re for pattern matching, yaml for config parsing, click for output,
|
|
12
|
+
pathlib for file operations
|
|
13
|
+
|
|
14
|
+
Exports: perform_merge, LINTER_SECTIONS
|
|
15
|
+
|
|
16
|
+
Interfaces: perform_merge(output_path, preset, output, generate_config_fn) -> None
|
|
17
|
+
|
|
18
|
+
Implementation: Text-based parsing and merging to preserve YAML comments
|
|
19
|
+
"""
|
|
20
|
+
|
|
21
|
+
import re
|
|
22
|
+
import sys
|
|
23
|
+
from collections.abc import Callable
|
|
24
|
+
from pathlib import Path
|
|
25
|
+
|
|
26
|
+
import click
|
|
27
|
+
import yaml
|
|
28
|
+
|
|
29
|
+
# Known linter section names that should be in the template
|
|
30
|
+
LINTER_SECTIONS = [
|
|
31
|
+
"magic-numbers",
|
|
32
|
+
"nesting",
|
|
33
|
+
"srp",
|
|
34
|
+
"dry",
|
|
35
|
+
"file-placement",
|
|
36
|
+
"print-statements",
|
|
37
|
+
"stringly-typed",
|
|
38
|
+
"file-header",
|
|
39
|
+
"method-property",
|
|
40
|
+
"stateless-class",
|
|
41
|
+
"pipeline",
|
|
42
|
+
"lazy-ignores",
|
|
43
|
+
]
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def _is_section_header_line(line: str) -> bool:
|
|
47
|
+
"""Check if line is a section header (# ==== style comment)."""
|
|
48
|
+
return line.startswith("# ===")
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def _get_linter_section_name(line: str) -> str | None:
|
|
52
|
+
"""Extract linter section name from line if it's a known linter section."""
|
|
53
|
+
section_match = re.match(r"^([a-z][a-z0-9-]*):$", line)
|
|
54
|
+
if section_match and section_match.group(1) in LINTER_SECTIONS:
|
|
55
|
+
return section_match.group(1)
|
|
56
|
+
return None
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def _is_buffer_line(line: str) -> bool:
|
|
60
|
+
"""Check if line should be buffered (comment or empty)."""
|
|
61
|
+
stripped = line.strip()
|
|
62
|
+
return stripped.startswith("#") or stripped == ""
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
def _save_current_section(
|
|
66
|
+
sections: dict[str, str], current_section: str | None, current_content: list[str]
|
|
67
|
+
) -> None:
|
|
68
|
+
"""Save current section to sections dict if valid."""
|
|
69
|
+
if current_section and current_content:
|
|
70
|
+
sections[current_section] = "\n".join(current_content)
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def _handle_section_header(
|
|
74
|
+
line: str, sections: dict[str, str], current_section: str | None, current_content: list[str]
|
|
75
|
+
) -> tuple[str | None, list[str], list[str]]:
|
|
76
|
+
"""Handle a section header line (# === style)."""
|
|
77
|
+
_save_current_section(sections, current_section, current_content)
|
|
78
|
+
return None, [], [line]
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
def _start_linter_section(
|
|
82
|
+
section_name: str, line: str, header_buffer: list[str]
|
|
83
|
+
) -> tuple[str | None, list[str], list[str]]:
|
|
84
|
+
"""Start a new linter section with the header buffer and section line."""
|
|
85
|
+
return section_name, header_buffer + [line], []
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
def _handle_content_line(
|
|
89
|
+
line: str, current_section: str | None, current_content: list[str], header_buffer: list[str]
|
|
90
|
+
) -> tuple[str | None, list[str], list[str]]:
|
|
91
|
+
"""Handle a regular content line."""
|
|
92
|
+
if current_section:
|
|
93
|
+
current_content.append(line)
|
|
94
|
+
return current_section, current_content, header_buffer
|
|
95
|
+
if _is_buffer_line(line):
|
|
96
|
+
header_buffer.append(line)
|
|
97
|
+
return current_section, current_content, header_buffer
|
|
98
|
+
return current_section, current_content, []
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
def _process_template_line(
|
|
102
|
+
line: str,
|
|
103
|
+
sections: dict[str, str],
|
|
104
|
+
current_section: str | None,
|
|
105
|
+
current_content: list[str],
|
|
106
|
+
header_buffer: list[str],
|
|
107
|
+
) -> tuple[str | None, list[str], list[str]]:
|
|
108
|
+
"""Process a single template line and update state."""
|
|
109
|
+
if _is_section_header_line(line):
|
|
110
|
+
return _handle_section_header(line, sections, current_section, current_content)
|
|
111
|
+
|
|
112
|
+
section_name = _get_linter_section_name(line)
|
|
113
|
+
if section_name:
|
|
114
|
+
_save_current_section(sections, current_section, current_content)
|
|
115
|
+
return _start_linter_section(section_name, line, header_buffer)
|
|
116
|
+
|
|
117
|
+
return _handle_content_line(line, current_section, current_content, header_buffer)
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
def extract_linter_sections(template: str) -> dict[str, str]:
|
|
121
|
+
"""Extract each linter section from template as text blocks.
|
|
122
|
+
|
|
123
|
+
Args:
|
|
124
|
+
template: Full template content
|
|
125
|
+
|
|
126
|
+
Returns:
|
|
127
|
+
Dict mapping section name to section content (with header comments)
|
|
128
|
+
"""
|
|
129
|
+
sections: dict[str, str] = {}
|
|
130
|
+
lines = template.split("\n")
|
|
131
|
+
current_section: str | None = None
|
|
132
|
+
current_content: list[str] = []
|
|
133
|
+
header_buffer: list[str] = []
|
|
134
|
+
|
|
135
|
+
for line in lines:
|
|
136
|
+
current_section, current_content, header_buffer = _process_template_line(
|
|
137
|
+
line, sections, current_section, current_content, header_buffer
|
|
138
|
+
)
|
|
139
|
+
|
|
140
|
+
# Save last section
|
|
141
|
+
if current_section and current_content:
|
|
142
|
+
sections[current_section] = "\n".join(current_content)
|
|
143
|
+
|
|
144
|
+
return sections
|
|
145
|
+
|
|
146
|
+
|
|
147
|
+
def identify_missing_sections(existing_config: dict, all_sections: list[str]) -> list[str]:
|
|
148
|
+
"""Identify which linter sections are missing from existing config.
|
|
149
|
+
|
|
150
|
+
Args:
|
|
151
|
+
existing_config: Parsed existing config dict
|
|
152
|
+
all_sections: List of all linter section names
|
|
153
|
+
|
|
154
|
+
Returns:
|
|
155
|
+
List of section names missing from existing config
|
|
156
|
+
"""
|
|
157
|
+
return [s for s in all_sections if s not in existing_config]
|
|
158
|
+
|
|
159
|
+
|
|
160
|
+
def _find_global_settings_position(content: str) -> int:
|
|
161
|
+
"""Find position of GLOBAL SETTINGS section in content."""
|
|
162
|
+
marker = "# ============================================================================\n# GLOBAL SETTINGS"
|
|
163
|
+
return content.find(marker)
|
|
164
|
+
|
|
165
|
+
|
|
166
|
+
def _insert_before_global_settings(content: str, sections_text: str, insert_pos: int) -> str:
|
|
167
|
+
"""Insert sections before GLOBAL SETTINGS marker."""
|
|
168
|
+
return content[:insert_pos] + sections_text + "\n\n" + content[insert_pos:]
|
|
169
|
+
|
|
170
|
+
|
|
171
|
+
def merge_config_sections(existing_content: str, missing_sections: dict[str, str]) -> str:
|
|
172
|
+
"""Merge missing sections into existing config content.
|
|
173
|
+
|
|
174
|
+
Args:
|
|
175
|
+
existing_content: Original config file content
|
|
176
|
+
missing_sections: Dict of section name -> section content to add
|
|
177
|
+
|
|
178
|
+
Returns:
|
|
179
|
+
Merged config content with missing sections appended
|
|
180
|
+
"""
|
|
181
|
+
if not missing_sections:
|
|
182
|
+
return existing_content
|
|
183
|
+
|
|
184
|
+
sections_text = "\n".join(missing_sections.values())
|
|
185
|
+
insert_pos = _find_global_settings_position(existing_content)
|
|
186
|
+
|
|
187
|
+
if insert_pos > 0:
|
|
188
|
+
return _insert_before_global_settings(existing_content, sections_text, insert_pos)
|
|
189
|
+
return existing_content.rstrip() + "\n\n" + sections_text + "\n"
|
|
190
|
+
|
|
191
|
+
|
|
192
|
+
def _parse_existing_config(content: str, output: str) -> dict:
|
|
193
|
+
"""Parse existing config file content as YAML."""
|
|
194
|
+
try:
|
|
195
|
+
return yaml.safe_load(content) or {}
|
|
196
|
+
except yaml.YAMLError:
|
|
197
|
+
click.echo(f"Error: Could not parse {output} as YAML", err=True)
|
|
198
|
+
click.echo("Use --force to overwrite with a fresh config", err=True)
|
|
199
|
+
sys.exit(1)
|
|
200
|
+
|
|
201
|
+
|
|
202
|
+
def _build_missing_sections_dict(
|
|
203
|
+
missing_names: list[str], template_sections: dict[str, str]
|
|
204
|
+
) -> dict[str, str]:
|
|
205
|
+
"""Build dict of missing section name -> content."""
|
|
206
|
+
return {name: template_sections[name] for name in missing_names if name in template_sections}
|
|
207
|
+
|
|
208
|
+
|
|
209
|
+
def _report_merge_results(missing_names: list[str], output: str) -> None:
|
|
210
|
+
"""Report which sections were added."""
|
|
211
|
+
click.echo(f"Added {len(missing_names)} missing linter section(s) to {output}:")
|
|
212
|
+
for name in missing_names:
|
|
213
|
+
click.echo(f" - {name}")
|
|
214
|
+
|
|
215
|
+
|
|
216
|
+
def perform_merge(
|
|
217
|
+
output_path: Path, preset: str, output: str, generate_config_fn: Callable[[str], str]
|
|
218
|
+
) -> None:
|
|
219
|
+
"""Merge missing linter sections into existing config.
|
|
220
|
+
|
|
221
|
+
Args:
|
|
222
|
+
output_path: Path to existing config file
|
|
223
|
+
preset: Preset to use for missing sections
|
|
224
|
+
output: Output filename for display
|
|
225
|
+
generate_config_fn: Function to generate config content from preset
|
|
226
|
+
"""
|
|
227
|
+
existing_content = output_path.read_text(encoding="utf-8")
|
|
228
|
+
existing_config = _parse_existing_config(existing_content, output)
|
|
229
|
+
|
|
230
|
+
template_sections = extract_linter_sections(generate_config_fn(preset))
|
|
231
|
+
missing_names = identify_missing_sections(existing_config, list(template_sections.keys()))
|
|
232
|
+
|
|
233
|
+
if not missing_names:
|
|
234
|
+
click.echo(f"{output} already contains all linter sections")
|
|
235
|
+
return
|
|
236
|
+
|
|
237
|
+
missing_sections = _build_missing_sections_dict(missing_names, template_sections)
|
|
238
|
+
merged_content = merge_config_sections(existing_content, missing_sections)
|
|
239
|
+
output_path.write_text(merged_content, encoding="utf-8")
|
|
240
|
+
|
|
241
|
+
_report_merge_results(missing_names, output)
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Purpose: CLI linters package that registers all linter commands to the main CLI group
|
|
3
|
+
|
|
4
|
+
Scope: Export and registration of all linter CLI commands (nesting, srp, dry, magic-numbers, etc.)
|
|
5
|
+
|
|
6
|
+
Overview: Package initialization that imports all linter command modules to trigger their registration
|
|
7
|
+
with the main CLI group via Click decorators. Each submodule defines commands using @cli.command()
|
|
8
|
+
decorators that automatically register with the CLI when imported. Organized by logical grouping:
|
|
9
|
+
structure_quality (nesting, srp), code_smells (dry, magic-numbers), code_patterns (print-statements,
|
|
10
|
+
method-property, stateless-class), structure (file-placement, pipeline), documentation (file-header).
|
|
11
|
+
|
|
12
|
+
Dependencies: Click for CLI framework, src.cli.main for CLI group, individual linter modules
|
|
13
|
+
|
|
14
|
+
Exports: All linter command functions for reference and testing
|
|
15
|
+
|
|
16
|
+
Interfaces: Click command decorators, integration with main CLI group
|
|
17
|
+
|
|
18
|
+
Implementation: Module imports trigger command registration via Click decorator side effects
|
|
19
|
+
|
|
20
|
+
Suppressions:
|
|
21
|
+
- F401: Module imports trigger Click command registration via decorator side effects
|
|
22
|
+
"""
|
|
23
|
+
|
|
24
|
+
# Import all linter command modules to register them with the CLI
|
|
25
|
+
# Each module uses @cli.command() decorators that register on import
|
|
26
|
+
from src.cli.linters import ( # noqa: F401
|
|
27
|
+
code_patterns,
|
|
28
|
+
code_smells,
|
|
29
|
+
documentation,
|
|
30
|
+
performance,
|
|
31
|
+
structure,
|
|
32
|
+
structure_quality,
|
|
33
|
+
)
|
|
34
|
+
|
|
35
|
+
# Re-export command functions for testing and reference
|
|
36
|
+
from src.cli.linters.code_patterns import (
|
|
37
|
+
method_property,
|
|
38
|
+
print_statements,
|
|
39
|
+
stateless_class,
|
|
40
|
+
)
|
|
41
|
+
from src.cli.linters.code_smells import dry, magic_numbers
|
|
42
|
+
from src.cli.linters.documentation import file_header
|
|
43
|
+
from src.cli.linters.performance import perf, regex_in_loop, string_concat_loop
|
|
44
|
+
from src.cli.linters.structure import file_placement, pipeline
|
|
45
|
+
from src.cli.linters.structure_quality import nesting, srp
|
|
46
|
+
|
|
47
|
+
__all__ = [
|
|
48
|
+
# Structure quality commands
|
|
49
|
+
"nesting",
|
|
50
|
+
"srp",
|
|
51
|
+
# Code smell commands
|
|
52
|
+
"dry",
|
|
53
|
+
"magic_numbers",
|
|
54
|
+
# Code pattern commands
|
|
55
|
+
"print_statements",
|
|
56
|
+
"method_property",
|
|
57
|
+
"stateless_class",
|
|
58
|
+
# Structure commands
|
|
59
|
+
"file_placement",
|
|
60
|
+
"pipeline",
|
|
61
|
+
# Documentation commands
|
|
62
|
+
"file_header",
|
|
63
|
+
# Performance commands
|
|
64
|
+
"perf",
|
|
65
|
+
"string_concat_loop",
|
|
66
|
+
"regex_in_loop",
|
|
67
|
+
]
|
|
@@ -0,0 +1,270 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Purpose: CLI commands for code pattern linters (print-statements, method-property, stateless-class, lazy-ignores, lbyl)
|
|
3
|
+
|
|
4
|
+
Scope: Commands that detect code patterns and anti-patterns in Python code
|
|
5
|
+
|
|
6
|
+
Overview: Provides CLI commands for code pattern linting: print-statements detects print() and
|
|
7
|
+
console.log calls that should use proper logging, method-property finds methods that should be
|
|
8
|
+
@property decorators, stateless-class detects classes without state that should be module
|
|
9
|
+
functions, lazy-ignores detects unjustified linting suppressions, and lbyl detects Look Before
|
|
10
|
+
You Leap anti-patterns. Each command supports standard options (config, format, recursive) and
|
|
11
|
+
integrates with the orchestrator for execution.
|
|
12
|
+
|
|
13
|
+
Dependencies: click for CLI framework, src.cli.main for CLI group, src.cli.utils for shared utilities
|
|
14
|
+
|
|
15
|
+
Exports: print_statements, method_property, stateless_class, lazy_ignores, lbyl commands
|
|
16
|
+
|
|
17
|
+
Interfaces: Click CLI commands registered to main CLI group
|
|
18
|
+
|
|
19
|
+
Implementation: Click decorators for command definition, orchestrator-based linting execution
|
|
20
|
+
"""
|
|
21
|
+
|
|
22
|
+
import logging
|
|
23
|
+
import sys
|
|
24
|
+
from pathlib import Path
|
|
25
|
+
from typing import TYPE_CHECKING, NoReturn
|
|
26
|
+
|
|
27
|
+
from src.cli.linters.shared import ExecuteParams, create_linter_command
|
|
28
|
+
from src.cli.utils import execute_linting_on_paths, setup_base_orchestrator, validate_paths_exist
|
|
29
|
+
from src.core.cli_utils import format_violations
|
|
30
|
+
from src.core.types import Violation
|
|
31
|
+
|
|
32
|
+
if TYPE_CHECKING:
|
|
33
|
+
from src.orchestrator.core import Orchestrator
|
|
34
|
+
|
|
35
|
+
# Configure module logger
|
|
36
|
+
logger = logging.getLogger(__name__)
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
# =============================================================================
|
|
40
|
+
# Print Statements Command
|
|
41
|
+
# =============================================================================
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def _setup_print_statements_orchestrator(
|
|
45
|
+
path_objs: list[Path], config_file: str | None, verbose: bool, project_root: Path | None = None
|
|
46
|
+
) -> "Orchestrator":
|
|
47
|
+
"""Set up orchestrator for print-statements command."""
|
|
48
|
+
return setup_base_orchestrator(path_objs, config_file, verbose, project_root)
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def _run_print_statements_lint(
|
|
52
|
+
orchestrator: "Orchestrator", path_objs: list[Path], recursive: bool, parallel: bool = False
|
|
53
|
+
) -> list[Violation]:
|
|
54
|
+
"""Execute print-statements lint on files or directories."""
|
|
55
|
+
all_violations = execute_linting_on_paths(orchestrator, path_objs, recursive, parallel)
|
|
56
|
+
return [v for v in all_violations if "print-statement" in v.rule_id]
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def _execute_print_statements_lint(params: ExecuteParams) -> NoReturn:
|
|
60
|
+
"""Execute print-statements lint."""
|
|
61
|
+
validate_paths_exist(params.path_objs)
|
|
62
|
+
orchestrator = _setup_print_statements_orchestrator(
|
|
63
|
+
params.path_objs, params.config_file, params.verbose, params.project_root
|
|
64
|
+
)
|
|
65
|
+
print_statements_violations = _run_print_statements_lint(
|
|
66
|
+
orchestrator, params.path_objs, params.recursive, params.parallel
|
|
67
|
+
)
|
|
68
|
+
|
|
69
|
+
if params.verbose:
|
|
70
|
+
logger.info(f"Found {len(print_statements_violations)} print statement violation(s)")
|
|
71
|
+
|
|
72
|
+
format_violations(print_statements_violations, params.format)
|
|
73
|
+
sys.exit(1 if print_statements_violations else 0)
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
print_statements = create_linter_command(
|
|
77
|
+
"print-statements",
|
|
78
|
+
_execute_print_statements_lint,
|
|
79
|
+
"Check for print/console statements in code.",
|
|
80
|
+
"Detects print() calls in Python and console.log/warn/error/debug/info calls\n"
|
|
81
|
+
" in TypeScript/JavaScript that should be replaced with proper logging.",
|
|
82
|
+
)
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
# =============================================================================
|
|
86
|
+
# Method Property Command
|
|
87
|
+
# =============================================================================
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
def _setup_method_property_orchestrator(
|
|
91
|
+
path_objs: list[Path], config_file: str | None, verbose: bool, project_root: Path | None = None
|
|
92
|
+
) -> "Orchestrator":
|
|
93
|
+
"""Set up orchestrator for method-property command."""
|
|
94
|
+
return setup_base_orchestrator(path_objs, config_file, verbose, project_root)
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
def _run_method_property_lint(
|
|
98
|
+
orchestrator: "Orchestrator", path_objs: list[Path], recursive: bool, parallel: bool = False
|
|
99
|
+
) -> list[Violation]:
|
|
100
|
+
"""Execute method-property lint on files or directories."""
|
|
101
|
+
all_violations = execute_linting_on_paths(orchestrator, path_objs, recursive, parallel)
|
|
102
|
+
return [v for v in all_violations if "method-property" in v.rule_id]
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
def _execute_method_property_lint(params: ExecuteParams) -> NoReturn:
|
|
106
|
+
"""Execute method-property lint."""
|
|
107
|
+
validate_paths_exist(params.path_objs)
|
|
108
|
+
orchestrator = _setup_method_property_orchestrator(
|
|
109
|
+
params.path_objs, params.config_file, params.verbose, params.project_root
|
|
110
|
+
)
|
|
111
|
+
method_property_violations = _run_method_property_lint(
|
|
112
|
+
orchestrator, params.path_objs, params.recursive, params.parallel
|
|
113
|
+
)
|
|
114
|
+
|
|
115
|
+
if params.verbose:
|
|
116
|
+
logger.info(f"Found {len(method_property_violations)} method-property violation(s)")
|
|
117
|
+
|
|
118
|
+
format_violations(method_property_violations, params.format)
|
|
119
|
+
sys.exit(1 if method_property_violations else 0)
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
method_property = create_linter_command(
|
|
123
|
+
"method-property",
|
|
124
|
+
_execute_method_property_lint,
|
|
125
|
+
"Check for methods that should be @property decorators.",
|
|
126
|
+
"Detects Python methods that could be converted to properties following\n"
|
|
127
|
+
" Pythonic conventions: methods returning self._attribute, get_* prefixed\n"
|
|
128
|
+
" methods (Java-style getters), or simple computed values with no side effects.",
|
|
129
|
+
)
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
# =============================================================================
|
|
133
|
+
# Stateless Class Command
|
|
134
|
+
# =============================================================================
|
|
135
|
+
|
|
136
|
+
|
|
137
|
+
def _setup_stateless_class_orchestrator(
|
|
138
|
+
path_objs: list[Path], config_file: str | None, verbose: bool, project_root: Path | None = None
|
|
139
|
+
) -> "Orchestrator":
|
|
140
|
+
"""Set up orchestrator for stateless-class command."""
|
|
141
|
+
return setup_base_orchestrator(path_objs, config_file, verbose, project_root)
|
|
142
|
+
|
|
143
|
+
|
|
144
|
+
def _run_stateless_class_lint(
|
|
145
|
+
orchestrator: "Orchestrator", path_objs: list[Path], recursive: bool, parallel: bool = False
|
|
146
|
+
) -> list[Violation]:
|
|
147
|
+
"""Execute stateless-class lint on files or directories."""
|
|
148
|
+
all_violations = execute_linting_on_paths(orchestrator, path_objs, recursive, parallel)
|
|
149
|
+
return [v for v in all_violations if "stateless-class" in v.rule_id]
|
|
150
|
+
|
|
151
|
+
|
|
152
|
+
def _execute_stateless_class_lint(params: ExecuteParams) -> NoReturn:
|
|
153
|
+
"""Execute stateless-class lint."""
|
|
154
|
+
validate_paths_exist(params.path_objs)
|
|
155
|
+
orchestrator = _setup_stateless_class_orchestrator(
|
|
156
|
+
params.path_objs, params.config_file, params.verbose, params.project_root
|
|
157
|
+
)
|
|
158
|
+
stateless_class_violations = _run_stateless_class_lint(
|
|
159
|
+
orchestrator, params.path_objs, params.recursive, params.parallel
|
|
160
|
+
)
|
|
161
|
+
|
|
162
|
+
if params.verbose:
|
|
163
|
+
logger.info(f"Found {len(stateless_class_violations)} stateless-class violation(s)")
|
|
164
|
+
|
|
165
|
+
format_violations(stateless_class_violations, params.format)
|
|
166
|
+
sys.exit(1 if stateless_class_violations else 0)
|
|
167
|
+
|
|
168
|
+
|
|
169
|
+
stateless_class = create_linter_command(
|
|
170
|
+
"stateless-class",
|
|
171
|
+
_execute_stateless_class_lint,
|
|
172
|
+
"Check for stateless classes that should be module functions.",
|
|
173
|
+
"Detects Python classes that have no constructor (__init__), no instance\n"
|
|
174
|
+
" state, and 2+ methods - indicating they should be refactored to module-level\n"
|
|
175
|
+
" functions instead of using a class as a namespace.",
|
|
176
|
+
)
|
|
177
|
+
|
|
178
|
+
|
|
179
|
+
# =============================================================================
|
|
180
|
+
# Lazy Ignores Command
|
|
181
|
+
# =============================================================================
|
|
182
|
+
|
|
183
|
+
|
|
184
|
+
def _setup_lazy_ignores_orchestrator(
|
|
185
|
+
path_objs: list[Path], config_file: str | None, verbose: bool, project_root: Path | None = None
|
|
186
|
+
) -> "Orchestrator":
|
|
187
|
+
"""Set up orchestrator for lazy-ignores command."""
|
|
188
|
+
return setup_base_orchestrator(path_objs, config_file, verbose, project_root)
|
|
189
|
+
|
|
190
|
+
|
|
191
|
+
def _run_lazy_ignores_lint(
|
|
192
|
+
orchestrator: "Orchestrator", path_objs: list[Path], recursive: bool, parallel: bool = False
|
|
193
|
+
) -> list[Violation]:
|
|
194
|
+
"""Execute lazy-ignores lint on files or directories."""
|
|
195
|
+
all_violations = execute_linting_on_paths(orchestrator, path_objs, recursive, parallel)
|
|
196
|
+
return [v for v in all_violations if v.rule_id.startswith("lazy-ignores")]
|
|
197
|
+
|
|
198
|
+
|
|
199
|
+
def _execute_lazy_ignores_lint(params: ExecuteParams) -> NoReturn:
|
|
200
|
+
"""Execute lazy-ignores lint."""
|
|
201
|
+
validate_paths_exist(params.path_objs)
|
|
202
|
+
orchestrator = _setup_lazy_ignores_orchestrator(
|
|
203
|
+
params.path_objs, params.config_file, params.verbose, params.project_root
|
|
204
|
+
)
|
|
205
|
+
lazy_ignores_violations = _run_lazy_ignores_lint(
|
|
206
|
+
orchestrator, params.path_objs, params.recursive, params.parallel
|
|
207
|
+
)
|
|
208
|
+
|
|
209
|
+
if params.verbose:
|
|
210
|
+
logger.info(f"Found {len(lazy_ignores_violations)} lazy-ignores violation(s)")
|
|
211
|
+
|
|
212
|
+
format_violations(lazy_ignores_violations, params.format)
|
|
213
|
+
sys.exit(1 if lazy_ignores_violations else 0)
|
|
214
|
+
|
|
215
|
+
|
|
216
|
+
lazy_ignores = create_linter_command(
|
|
217
|
+
"lazy-ignores",
|
|
218
|
+
_execute_lazy_ignores_lint,
|
|
219
|
+
"Check for unjustified linting suppressions.",
|
|
220
|
+
"Detects ignore directives (noqa, type:ignore, pylint:disable, nosec) that lack\n"
|
|
221
|
+
" corresponding entries in the file header's Suppressions section. Enforces a\n"
|
|
222
|
+
" header-based suppression model requiring human approval for all linting bypasses.",
|
|
223
|
+
)
|
|
224
|
+
|
|
225
|
+
|
|
226
|
+
# =============================================================================
|
|
227
|
+
# LBYL Command
|
|
228
|
+
# =============================================================================
|
|
229
|
+
|
|
230
|
+
|
|
231
|
+
def _setup_lbyl_orchestrator(
|
|
232
|
+
path_objs: list[Path], config_file: str | None, verbose: bool, project_root: Path | None = None
|
|
233
|
+
) -> "Orchestrator":
|
|
234
|
+
"""Set up orchestrator for lbyl command."""
|
|
235
|
+
return setup_base_orchestrator(path_objs, config_file, verbose, project_root)
|
|
236
|
+
|
|
237
|
+
|
|
238
|
+
def _run_lbyl_lint(
|
|
239
|
+
orchestrator: "Orchestrator", path_objs: list[Path], recursive: bool, parallel: bool = False
|
|
240
|
+
) -> list[Violation]:
|
|
241
|
+
"""Execute lbyl lint on files or directories."""
|
|
242
|
+
all_violations = execute_linting_on_paths(orchestrator, path_objs, recursive, parallel)
|
|
243
|
+
return [v for v in all_violations if v.rule_id.startswith("lbyl")]
|
|
244
|
+
|
|
245
|
+
|
|
246
|
+
def _execute_lbyl_lint(params: ExecuteParams) -> NoReturn:
|
|
247
|
+
"""Execute lbyl lint."""
|
|
248
|
+
validate_paths_exist(params.path_objs)
|
|
249
|
+
orchestrator = _setup_lbyl_orchestrator(
|
|
250
|
+
params.path_objs, params.config_file, params.verbose, params.project_root
|
|
251
|
+
)
|
|
252
|
+
lbyl_violations = _run_lbyl_lint(
|
|
253
|
+
orchestrator, params.path_objs, params.recursive, params.parallel
|
|
254
|
+
)
|
|
255
|
+
|
|
256
|
+
if params.verbose:
|
|
257
|
+
logger.info(f"Found {len(lbyl_violations)} LBYL violation(s)")
|
|
258
|
+
|
|
259
|
+
format_violations(lbyl_violations, params.format)
|
|
260
|
+
sys.exit(1 if lbyl_violations else 0)
|
|
261
|
+
|
|
262
|
+
|
|
263
|
+
lbyl = create_linter_command(
|
|
264
|
+
"lbyl",
|
|
265
|
+
_execute_lbyl_lint,
|
|
266
|
+
"Check for Look Before You Leap anti-patterns.",
|
|
267
|
+
"Detects LBYL (Look Before You Leap) anti-patterns in Python code that should\n"
|
|
268
|
+
" be refactored to EAFP (Easier to Ask Forgiveness than Permission) style.\n"
|
|
269
|
+
" Examples: 'if key in dict: dict[key]' should use try/except KeyError.",
|
|
270
|
+
)
|