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.py
ADDED
|
@@ -0,0 +1,480 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Purpose: Configuration management commands for thai-lint CLI
|
|
3
|
+
|
|
4
|
+
Scope: Commands for viewing, modifying, and initializing thai-lint configuration
|
|
5
|
+
|
|
6
|
+
Overview: Provides CLI commands for managing thai-lint configuration including show (display
|
|
7
|
+
configuration in text/json/yaml), get (retrieve specific value), set (modify value with
|
|
8
|
+
validation), reset (restore defaults), and init-config (generate new .thailint.yaml with
|
|
9
|
+
presets). Supports both interactive and non-interactive modes for human and AI agent
|
|
10
|
+
workflows. Integrates with the config module for loading, saving, and validation.
|
|
11
|
+
|
|
12
|
+
Dependencies: click for CLI framework, src.config for config operations, pathlib for file paths,
|
|
13
|
+
json and yaml for output formatting
|
|
14
|
+
|
|
15
|
+
Exports: config_group (Click command group), init_config command, show_config, get_config,
|
|
16
|
+
set_config, reset_config commands
|
|
17
|
+
|
|
18
|
+
Interfaces: Click commands registered to main CLI group, config presets (strict/standard/lenient)
|
|
19
|
+
|
|
20
|
+
Implementation: Uses Click decorators for command definition, supports multiple output formats,
|
|
21
|
+
validates configuration changes before saving, uses template file for init-config generation
|
|
22
|
+
"""
|
|
23
|
+
|
|
24
|
+
import logging
|
|
25
|
+
import sys
|
|
26
|
+
from pathlib import Path
|
|
27
|
+
|
|
28
|
+
import click
|
|
29
|
+
import yaml
|
|
30
|
+
|
|
31
|
+
from src.config import ConfigError, save_config, validate_config
|
|
32
|
+
|
|
33
|
+
from .config_merge import perform_merge
|
|
34
|
+
from .main import cli
|
|
35
|
+
|
|
36
|
+
# Configure module logger
|
|
37
|
+
logger = logging.getLogger(__name__)
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
# =============================================================================
|
|
41
|
+
# Config Command Group
|
|
42
|
+
# =============================================================================
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
@cli.group()
|
|
46
|
+
def config() -> None:
|
|
47
|
+
"""Configuration management commands."""
|
|
48
|
+
pass
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
# =============================================================================
|
|
52
|
+
# Config Show Command
|
|
53
|
+
# =============================================================================
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
@config.command("show")
|
|
57
|
+
@click.option(
|
|
58
|
+
"--format",
|
|
59
|
+
"-f",
|
|
60
|
+
type=click.Choice(["text", "json", "yaml"]),
|
|
61
|
+
default="text",
|
|
62
|
+
help="Output format",
|
|
63
|
+
)
|
|
64
|
+
@click.pass_context
|
|
65
|
+
def config_show(ctx: click.Context, format: str) -> None:
|
|
66
|
+
"""Display current configuration.
|
|
67
|
+
|
|
68
|
+
Shows all configuration values in the specified format.
|
|
69
|
+
|
|
70
|
+
Examples:
|
|
71
|
+
|
|
72
|
+
\b
|
|
73
|
+
# Show as text
|
|
74
|
+
thai-lint config show
|
|
75
|
+
|
|
76
|
+
\b
|
|
77
|
+
# Show as JSON
|
|
78
|
+
thai-lint config show --format json
|
|
79
|
+
|
|
80
|
+
\b
|
|
81
|
+
# Show as YAML
|
|
82
|
+
thai-lint config show --format yaml
|
|
83
|
+
"""
|
|
84
|
+
cfg = ctx.obj["config"]
|
|
85
|
+
|
|
86
|
+
formatters = {
|
|
87
|
+
"json": _format_config_json,
|
|
88
|
+
"yaml": _format_config_yaml,
|
|
89
|
+
"text": _format_config_text,
|
|
90
|
+
}
|
|
91
|
+
formatters[format](cfg)
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
def _format_config_json(cfg: dict) -> None:
|
|
95
|
+
"""Format configuration as JSON."""
|
|
96
|
+
import json
|
|
97
|
+
|
|
98
|
+
click.echo(json.dumps(cfg, indent=2))
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
def _format_config_yaml(cfg: dict) -> None:
|
|
102
|
+
"""Format configuration as YAML."""
|
|
103
|
+
click.echo(yaml.dump(cfg, default_flow_style=False, sort_keys=False))
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
def _format_config_text(cfg: dict) -> None:
|
|
107
|
+
"""Format configuration as text."""
|
|
108
|
+
click.echo("Current Configuration:")
|
|
109
|
+
click.echo("-" * 40)
|
|
110
|
+
for key, value in cfg.items():
|
|
111
|
+
click.echo(f"{key:20} : {value}")
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
# =============================================================================
|
|
115
|
+
# Config Get Command
|
|
116
|
+
# =============================================================================
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
@config.command("get")
|
|
120
|
+
@click.argument("key")
|
|
121
|
+
@click.pass_context
|
|
122
|
+
def config_get(ctx: click.Context, key: str) -> None:
|
|
123
|
+
"""Get specific configuration value.
|
|
124
|
+
|
|
125
|
+
KEY: Configuration key to retrieve
|
|
126
|
+
|
|
127
|
+
Examples:
|
|
128
|
+
|
|
129
|
+
\b
|
|
130
|
+
# Get log level
|
|
131
|
+
thai-lint config get log_level
|
|
132
|
+
|
|
133
|
+
\b
|
|
134
|
+
# Get greeting template
|
|
135
|
+
thai-lint config get greeting
|
|
136
|
+
"""
|
|
137
|
+
cfg = ctx.obj["config"]
|
|
138
|
+
|
|
139
|
+
if key not in cfg:
|
|
140
|
+
click.echo(f"Configuration key not found: {key}", err=True)
|
|
141
|
+
sys.exit(1)
|
|
142
|
+
|
|
143
|
+
click.echo(cfg[key])
|
|
144
|
+
|
|
145
|
+
|
|
146
|
+
# =============================================================================
|
|
147
|
+
# Config Set Command
|
|
148
|
+
# =============================================================================
|
|
149
|
+
|
|
150
|
+
|
|
151
|
+
def _convert_value_type(value: str) -> bool | int | float | str:
|
|
152
|
+
"""Convert string value to appropriate type."""
|
|
153
|
+
from contextlib import suppress
|
|
154
|
+
|
|
155
|
+
if value.lower() in ["true", "false"]:
|
|
156
|
+
return value.lower() == "true"
|
|
157
|
+
# Use EAFP pattern for numeric conversion
|
|
158
|
+
for converter in (int, float):
|
|
159
|
+
with suppress(ValueError):
|
|
160
|
+
return converter(value)
|
|
161
|
+
return value
|
|
162
|
+
|
|
163
|
+
|
|
164
|
+
def _validate_and_report_errors(cfg: dict) -> None:
|
|
165
|
+
"""Validate configuration and report errors."""
|
|
166
|
+
is_valid, errors = validate_config(cfg)
|
|
167
|
+
if not is_valid:
|
|
168
|
+
click.echo("Invalid configuration:", err=True)
|
|
169
|
+
for error in errors:
|
|
170
|
+
click.echo(f" - {error}", err=True)
|
|
171
|
+
sys.exit(1)
|
|
172
|
+
|
|
173
|
+
|
|
174
|
+
def _save_and_report_success(
|
|
175
|
+
cfg: dict, key: str, value: bool | int | float | str, config_path: Path | None, verbose: bool
|
|
176
|
+
) -> None:
|
|
177
|
+
"""Save configuration and report success."""
|
|
178
|
+
save_config(cfg, config_path)
|
|
179
|
+
click.echo(f"Set {key} = {value}")
|
|
180
|
+
if verbose:
|
|
181
|
+
logger.info(f"Configuration updated: {key}={value}")
|
|
182
|
+
|
|
183
|
+
|
|
184
|
+
@config.command("set")
|
|
185
|
+
@click.argument("key")
|
|
186
|
+
@click.argument("value")
|
|
187
|
+
@click.pass_context
|
|
188
|
+
def config_set(ctx: click.Context, key: str, value: str) -> None:
|
|
189
|
+
"""Set configuration value.
|
|
190
|
+
|
|
191
|
+
KEY: Configuration key to set
|
|
192
|
+
|
|
193
|
+
VALUE: New value for the key
|
|
194
|
+
|
|
195
|
+
Examples:
|
|
196
|
+
|
|
197
|
+
\b
|
|
198
|
+
# Set log level
|
|
199
|
+
thai-lint config set log_level DEBUG
|
|
200
|
+
|
|
201
|
+
\b
|
|
202
|
+
# Set greeting template
|
|
203
|
+
thai-lint config set greeting "Hi"
|
|
204
|
+
|
|
205
|
+
\b
|
|
206
|
+
# Set numeric value
|
|
207
|
+
thai-lint config set max_retries 5
|
|
208
|
+
"""
|
|
209
|
+
cfg = ctx.obj["config"]
|
|
210
|
+
converted_value = _convert_value_type(value)
|
|
211
|
+
cfg[key] = converted_value
|
|
212
|
+
|
|
213
|
+
try:
|
|
214
|
+
_validate_and_report_errors(cfg)
|
|
215
|
+
except Exception as e:
|
|
216
|
+
click.echo(f"Validation error: {e}", err=True)
|
|
217
|
+
sys.exit(1)
|
|
218
|
+
|
|
219
|
+
try:
|
|
220
|
+
config_path = ctx.obj.get("config_path")
|
|
221
|
+
verbose = ctx.obj.get("verbose", False)
|
|
222
|
+
_save_and_report_success(cfg, key, converted_value, config_path, verbose)
|
|
223
|
+
except ConfigError as e:
|
|
224
|
+
click.echo(f"Error saving configuration: {e}", err=True)
|
|
225
|
+
sys.exit(1)
|
|
226
|
+
|
|
227
|
+
|
|
228
|
+
# =============================================================================
|
|
229
|
+
# Config Reset Command
|
|
230
|
+
# =============================================================================
|
|
231
|
+
|
|
232
|
+
|
|
233
|
+
@config.command("reset")
|
|
234
|
+
@click.option("--yes", "-y", is_flag=True, help="Skip confirmation prompt")
|
|
235
|
+
@click.pass_context
|
|
236
|
+
def config_reset(ctx: click.Context, yes: bool) -> None:
|
|
237
|
+
"""Reset configuration to defaults.
|
|
238
|
+
|
|
239
|
+
Examples:
|
|
240
|
+
|
|
241
|
+
\b
|
|
242
|
+
# Reset with confirmation
|
|
243
|
+
thai-lint config reset
|
|
244
|
+
|
|
245
|
+
\b
|
|
246
|
+
# Reset without confirmation
|
|
247
|
+
thai-lint config reset --yes
|
|
248
|
+
"""
|
|
249
|
+
if not yes:
|
|
250
|
+
click.confirm("Reset configuration to defaults?", abort=True)
|
|
251
|
+
|
|
252
|
+
from src.config import DEFAULT_CONFIG
|
|
253
|
+
|
|
254
|
+
try:
|
|
255
|
+
config_path = ctx.obj.get("config_path")
|
|
256
|
+
save_config(DEFAULT_CONFIG.copy(), config_path)
|
|
257
|
+
click.echo("Configuration reset to defaults")
|
|
258
|
+
|
|
259
|
+
if ctx.obj.get("verbose"):
|
|
260
|
+
logger.info("Configuration reset to defaults")
|
|
261
|
+
except ConfigError as e:
|
|
262
|
+
click.echo(f"Error resetting configuration: {e}", err=True)
|
|
263
|
+
sys.exit(1)
|
|
264
|
+
|
|
265
|
+
|
|
266
|
+
# =============================================================================
|
|
267
|
+
# Init Config Command
|
|
268
|
+
# =============================================================================
|
|
269
|
+
|
|
270
|
+
|
|
271
|
+
@cli.command("init-config")
|
|
272
|
+
@click.option(
|
|
273
|
+
"--preset",
|
|
274
|
+
"-p",
|
|
275
|
+
type=click.Choice(["strict", "standard", "lenient"]),
|
|
276
|
+
default="standard",
|
|
277
|
+
help="Configuration preset",
|
|
278
|
+
)
|
|
279
|
+
@click.option("--non-interactive", is_flag=True, help="Skip interactive prompts (for AI agents)")
|
|
280
|
+
@click.option("--force", is_flag=True, help="Overwrite existing .thailint.yaml file")
|
|
281
|
+
@click.option(
|
|
282
|
+
"--output", "-o", type=click.Path(), default=".thailint.yaml", help="Output file path"
|
|
283
|
+
)
|
|
284
|
+
def init_config(preset: str, non_interactive: bool, force: bool, output: str) -> None:
|
|
285
|
+
"""Generate a .thailint.yaml configuration file with preset values.
|
|
286
|
+
|
|
287
|
+
Creates a richly-commented configuration file with sensible defaults
|
|
288
|
+
and optional customizations for different strictness levels.
|
|
289
|
+
|
|
290
|
+
If a config file already exists, missing linter sections will be added
|
|
291
|
+
without modifying existing settings. Use --force to completely overwrite.
|
|
292
|
+
|
|
293
|
+
For AI agents, use --non-interactive mode:
|
|
294
|
+
thailint init-config --non-interactive --preset lenient
|
|
295
|
+
|
|
296
|
+
Presets:
|
|
297
|
+
strict: Minimal allowed numbers (only -1, 0, 1)
|
|
298
|
+
standard: Balanced defaults (includes 2, 3, 4, 5, 10, 100, 1000)
|
|
299
|
+
lenient: Includes time conversions (adds 60, 3600)
|
|
300
|
+
|
|
301
|
+
Examples:
|
|
302
|
+
|
|
303
|
+
\\b
|
|
304
|
+
# Interactive mode (default, for humans)
|
|
305
|
+
thailint init-config
|
|
306
|
+
|
|
307
|
+
\\b
|
|
308
|
+
# Non-interactive mode (for AI agents)
|
|
309
|
+
thailint init-config --non-interactive
|
|
310
|
+
|
|
311
|
+
\\b
|
|
312
|
+
# Generate with lenient preset
|
|
313
|
+
thailint init-config --preset lenient
|
|
314
|
+
|
|
315
|
+
\\b
|
|
316
|
+
# Overwrite existing config (replaces entire file)
|
|
317
|
+
thailint init-config --force
|
|
318
|
+
|
|
319
|
+
\\b
|
|
320
|
+
# Custom output path
|
|
321
|
+
thailint init-config --output my-config.yaml
|
|
322
|
+
"""
|
|
323
|
+
output_path = Path(output)
|
|
324
|
+
|
|
325
|
+
# Interactive mode: Ask user for preferences
|
|
326
|
+
if not non_interactive:
|
|
327
|
+
preset = _run_interactive_preset_selection(preset)
|
|
328
|
+
|
|
329
|
+
# If file exists and not forcing overwrite, merge missing sections
|
|
330
|
+
if output_path.exists() and not force:
|
|
331
|
+
perform_merge(output_path, preset, output, _generate_config_content)
|
|
332
|
+
return
|
|
333
|
+
|
|
334
|
+
# Generate full config based on preset
|
|
335
|
+
config_content = _generate_config_content(preset)
|
|
336
|
+
|
|
337
|
+
# Write config file
|
|
338
|
+
_write_config_file(output_path, config_content, preset, output)
|
|
339
|
+
|
|
340
|
+
|
|
341
|
+
def _run_interactive_preset_selection(default_preset: str) -> str:
|
|
342
|
+
"""Run interactive preset selection.
|
|
343
|
+
|
|
344
|
+
Args:
|
|
345
|
+
default_preset: Default preset to use if user accepts default
|
|
346
|
+
|
|
347
|
+
Returns:
|
|
348
|
+
Selected preset name
|
|
349
|
+
"""
|
|
350
|
+
click.echo("thai-lint Configuration Generator")
|
|
351
|
+
click.echo("=" * 50)
|
|
352
|
+
click.echo("")
|
|
353
|
+
click.echo("This will create a .thailint.yaml configuration file.")
|
|
354
|
+
click.echo("For non-interactive mode (AI agents), use:")
|
|
355
|
+
click.echo(" thailint init-config --non-interactive")
|
|
356
|
+
click.echo("")
|
|
357
|
+
|
|
358
|
+
# Show preset options
|
|
359
|
+
click.echo("Available presets:")
|
|
360
|
+
click.echo(" strict: Only -1, 0, 1 allowed (strictest)")
|
|
361
|
+
click.echo(" standard: -1, 0, 1, 2, 3, 4, 5, 10, 100, 1000 (balanced)")
|
|
362
|
+
click.echo(" lenient: Includes time conversions 60, 3600 (most permissive)")
|
|
363
|
+
click.echo("")
|
|
364
|
+
|
|
365
|
+
preset_choices = click.Choice(["strict", "standard", "lenient"])
|
|
366
|
+
result: str = click.prompt("Choose preset", type=preset_choices, default=default_preset)
|
|
367
|
+
return result
|
|
368
|
+
|
|
369
|
+
|
|
370
|
+
def _generate_config_content(preset: str) -> str:
|
|
371
|
+
"""Generate config file content based on preset.
|
|
372
|
+
|
|
373
|
+
Args:
|
|
374
|
+
preset: Preset name (strict, standard, or lenient)
|
|
375
|
+
|
|
376
|
+
Returns:
|
|
377
|
+
Generated configuration file content
|
|
378
|
+
"""
|
|
379
|
+
# Preset configurations
|
|
380
|
+
presets = {
|
|
381
|
+
"strict": {
|
|
382
|
+
"allowed_numbers": "[-1, 0, 1]",
|
|
383
|
+
"max_small_integer": "3",
|
|
384
|
+
"description": "Strict (only universal values)",
|
|
385
|
+
},
|
|
386
|
+
"standard": {
|
|
387
|
+
"allowed_numbers": "[-1, 0, 1, 2, 3, 4, 5, 10, 100, 1000]",
|
|
388
|
+
"max_small_integer": "10",
|
|
389
|
+
"description": "Standard (balanced defaults)",
|
|
390
|
+
},
|
|
391
|
+
"lenient": {
|
|
392
|
+
"allowed_numbers": "[-1, 0, 1, 2, 3, 4, 5, 10, 60, 100, 1000, 3600]",
|
|
393
|
+
"max_small_integer": "10",
|
|
394
|
+
"description": "Lenient (includes time conversions)",
|
|
395
|
+
},
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
config = presets[preset]
|
|
399
|
+
|
|
400
|
+
# Read template - use parent of parent since we're in src/cli/
|
|
401
|
+
template_path = Path(__file__).parent.parent / "templates" / "thailint_config_template.yaml"
|
|
402
|
+
template = template_path.read_text(encoding="utf-8")
|
|
403
|
+
|
|
404
|
+
# Replace placeholders
|
|
405
|
+
content = template.replace("{{PRESET}}", config["description"])
|
|
406
|
+
content = content.replace("{{ALLOWED_NUMBERS}}", config["allowed_numbers"])
|
|
407
|
+
content = content.replace("{{MAX_SMALL_INTEGER}}", config["max_small_integer"])
|
|
408
|
+
|
|
409
|
+
return content
|
|
410
|
+
|
|
411
|
+
|
|
412
|
+
def _write_config_file(output_path: Path, content: str, preset: str, output: str) -> None:
|
|
413
|
+
"""Write configuration file and show success message.
|
|
414
|
+
|
|
415
|
+
Args:
|
|
416
|
+
output_path: Path to write file to
|
|
417
|
+
content: File content to write
|
|
418
|
+
preset: Selected preset name
|
|
419
|
+
output: Output filename for display
|
|
420
|
+
"""
|
|
421
|
+
try:
|
|
422
|
+
output_path.write_text(content, encoding="utf-8")
|
|
423
|
+
click.echo("")
|
|
424
|
+
click.echo(f"Created {output}")
|
|
425
|
+
click.echo(f"Preset: {preset}")
|
|
426
|
+
click.echo("")
|
|
427
|
+
click.echo("Next steps:")
|
|
428
|
+
click.echo(f" 1. Review and customize {output}")
|
|
429
|
+
click.echo(" 2. Run: thailint magic-numbers .")
|
|
430
|
+
click.echo(" 3. See docs: https://github.com/your-org/thai-lint")
|
|
431
|
+
except OSError as e:
|
|
432
|
+
click.echo(f"Error writing config file: {e}", err=True)
|
|
433
|
+
sys.exit(1)
|
|
434
|
+
|
|
435
|
+
|
|
436
|
+
# =============================================================================
|
|
437
|
+
# Hello Command (Example Command)
|
|
438
|
+
# =============================================================================
|
|
439
|
+
|
|
440
|
+
|
|
441
|
+
@cli.command()
|
|
442
|
+
@click.option("--name", "-n", default="World", help="Name to greet")
|
|
443
|
+
@click.option("--uppercase", "-u", is_flag=True, help="Convert greeting to uppercase")
|
|
444
|
+
@click.pass_context
|
|
445
|
+
def hello(ctx: click.Context, name: str, uppercase: bool) -> None:
|
|
446
|
+
"""Print a greeting message.
|
|
447
|
+
|
|
448
|
+
This is a simple example command demonstrating CLI basics.
|
|
449
|
+
|
|
450
|
+
Examples:
|
|
451
|
+
|
|
452
|
+
\b
|
|
453
|
+
# Basic greeting
|
|
454
|
+
thai-lint hello
|
|
455
|
+
|
|
456
|
+
\b
|
|
457
|
+
# Custom name
|
|
458
|
+
thai-lint hello --name Alice
|
|
459
|
+
|
|
460
|
+
\b
|
|
461
|
+
# Uppercase output
|
|
462
|
+
thai-lint hello --name Bob --uppercase
|
|
463
|
+
"""
|
|
464
|
+
config = ctx.obj["config"]
|
|
465
|
+
verbose = ctx.obj.get("verbose", False)
|
|
466
|
+
|
|
467
|
+
# Get greeting from config or use default
|
|
468
|
+
greeting_template = config.get("greeting", "Hello")
|
|
469
|
+
|
|
470
|
+
# Build greeting message
|
|
471
|
+
message = f"{greeting_template}, {name}!"
|
|
472
|
+
|
|
473
|
+
if uppercase:
|
|
474
|
+
message = message.upper()
|
|
475
|
+
|
|
476
|
+
# Output greeting
|
|
477
|
+
click.echo(message)
|
|
478
|
+
|
|
479
|
+
if verbose:
|
|
480
|
+
logger.info(f"Greeted {name} with template '{greeting_template}'")
|