thailint 0.10.0__tar.gz → 0.11.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (154) hide show
  1. {thailint-0.10.0 → thailint-0.11.0}/PKG-INFO +2 -2
  2. {thailint-0.10.0 → thailint-0.11.0}/README.md +1 -1
  3. {thailint-0.10.0 → thailint-0.11.0}/pyproject.toml +4 -3
  4. {thailint-0.10.0 → thailint-0.11.0}/src/__init__.py +1 -0
  5. thailint-0.11.0/src/cli/__init__.py +27 -0
  6. thailint-0.11.0/src/cli/__main__.py +22 -0
  7. thailint-0.11.0/src/cli/config.py +478 -0
  8. thailint-0.11.0/src/cli/linters/__init__.py +58 -0
  9. thailint-0.11.0/src/cli/linters/code_patterns.py +372 -0
  10. thailint-0.11.0/src/cli/linters/code_smells.py +343 -0
  11. thailint-0.11.0/src/cli/linters/documentation.py +155 -0
  12. thailint-0.11.0/src/cli/linters/shared.py +89 -0
  13. thailint-0.11.0/src/cli/linters/structure.py +313 -0
  14. thailint-0.11.0/src/cli/linters/structure_quality.py +316 -0
  15. thailint-0.11.0/src/cli/main.py +120 -0
  16. thailint-0.11.0/src/cli/utils.py +375 -0
  17. thailint-0.11.0/src/cli_main.py +34 -0
  18. {thailint-0.10.0 → thailint-0.11.0}/src/core/types.py +13 -0
  19. thailint-0.11.0/src/core/violation_utils.py +69 -0
  20. {thailint-0.10.0 → thailint-0.11.0}/src/linter_config/ignore.py +32 -16
  21. {thailint-0.10.0 → thailint-0.11.0}/src/linters/collection_pipeline/linter.py +2 -2
  22. {thailint-0.10.0 → thailint-0.11.0}/src/linters/dry/block_filter.py +97 -1
  23. {thailint-0.10.0 → thailint-0.11.0}/src/linters/dry/cache.py +94 -6
  24. {thailint-0.10.0 → thailint-0.11.0}/src/linters/dry/config.py +47 -10
  25. thailint-0.11.0/src/linters/dry/constant.py +92 -0
  26. thailint-0.11.0/src/linters/dry/constant_matcher.py +214 -0
  27. thailint-0.11.0/src/linters/dry/constant_violation_builder.py +98 -0
  28. {thailint-0.10.0 → thailint-0.11.0}/src/linters/dry/linter.py +89 -48
  29. thailint-0.11.0/src/linters/dry/python_analyzer.py +281 -0
  30. thailint-0.11.0/src/linters/dry/python_constant_extractor.py +101 -0
  31. thailint-0.11.0/src/linters/dry/single_statement_detector.py +415 -0
  32. {thailint-0.10.0 → thailint-0.11.0}/src/linters/dry/token_hasher.py +5 -5
  33. thailint-0.11.0/src/linters/dry/typescript_analyzer.py +273 -0
  34. thailint-0.11.0/src/linters/dry/typescript_constant_extractor.py +134 -0
  35. thailint-0.11.0/src/linters/dry/typescript_statement_detector.py +255 -0
  36. thailint-0.11.0/src/linters/dry/typescript_value_extractor.py +66 -0
  37. {thailint-0.10.0 → thailint-0.11.0}/src/linters/file_header/linter.py +2 -2
  38. {thailint-0.10.0 → thailint-0.11.0}/src/linters/file_placement/linter.py +2 -2
  39. {thailint-0.10.0 → thailint-0.11.0}/src/linters/file_placement/pattern_matcher.py +19 -5
  40. {thailint-0.10.0 → thailint-0.11.0}/src/linters/magic_numbers/linter.py +8 -67
  41. thailint-0.11.0/src/linters/magic_numbers/typescript_ignore_checker.py +81 -0
  42. {thailint-0.10.0 → thailint-0.11.0}/src/linters/nesting/linter.py +12 -9
  43. {thailint-0.10.0 → thailint-0.11.0}/src/linters/print_statements/linter.py +7 -24
  44. {thailint-0.10.0 → thailint-0.11.0}/src/linters/srp/class_analyzer.py +9 -9
  45. {thailint-0.10.0 → thailint-0.11.0}/src/linters/srp/heuristics.py +2 -2
  46. {thailint-0.10.0 → thailint-0.11.0}/src/linters/srp/linter.py +2 -2
  47. {thailint-0.10.0 → thailint-0.11.0}/src/linters/stateless_class/linter.py +2 -2
  48. thailint-0.11.0/src/linters/stringly_typed/__init__.py +23 -0
  49. thailint-0.11.0/src/linters/stringly_typed/config.py +165 -0
  50. thailint-0.11.0/src/linters/stringly_typed/python/__init__.py +29 -0
  51. thailint-0.11.0/src/linters/stringly_typed/python/analyzer.py +198 -0
  52. thailint-0.11.0/src/linters/stringly_typed/python/condition_extractor.py +131 -0
  53. thailint-0.11.0/src/linters/stringly_typed/python/conditional_detector.py +176 -0
  54. thailint-0.11.0/src/linters/stringly_typed/python/constants.py +21 -0
  55. thailint-0.11.0/src/linters/stringly_typed/python/match_analyzer.py +88 -0
  56. thailint-0.11.0/src/linters/stringly_typed/python/validation_detector.py +186 -0
  57. thailint-0.11.0/src/linters/stringly_typed/python/variable_extractor.py +96 -0
  58. thailint-0.11.0/src/orchestrator/core.py +462 -0
  59. thailint-0.10.0/src/cli.py +0 -2141
  60. thailint-0.10.0/src/linters/dry/python_analyzer.py +0 -684
  61. thailint-0.10.0/src/linters/dry/typescript_analyzer.py +0 -622
  62. thailint-0.10.0/src/orchestrator/core.py +0 -233
  63. {thailint-0.10.0 → thailint-0.11.0}/CHANGELOG.md +0 -0
  64. {thailint-0.10.0 → thailint-0.11.0}/LICENSE +0 -0
  65. {thailint-0.10.0 → thailint-0.11.0}/src/analyzers/__init__.py +0 -0
  66. {thailint-0.10.0 → thailint-0.11.0}/src/analyzers/typescript_base.py +0 -0
  67. {thailint-0.10.0 → thailint-0.11.0}/src/api.py +0 -0
  68. {thailint-0.10.0 → thailint-0.11.0}/src/config.py +0 -0
  69. {thailint-0.10.0 → thailint-0.11.0}/src/core/__init__.py +0 -0
  70. {thailint-0.10.0 → thailint-0.11.0}/src/core/base.py +0 -0
  71. {thailint-0.10.0 → thailint-0.11.0}/src/core/cli_utils.py +0 -0
  72. {thailint-0.10.0 → thailint-0.11.0}/src/core/config_parser.py +0 -0
  73. {thailint-0.10.0 → thailint-0.11.0}/src/core/linter_utils.py +0 -0
  74. {thailint-0.10.0 → thailint-0.11.0}/src/core/registry.py +0 -0
  75. {thailint-0.10.0 → thailint-0.11.0}/src/core/rule_discovery.py +0 -0
  76. {thailint-0.10.0 → thailint-0.11.0}/src/core/violation_builder.py +0 -0
  77. {thailint-0.10.0 → thailint-0.11.0}/src/formatters/__init__.py +0 -0
  78. {thailint-0.10.0 → thailint-0.11.0}/src/formatters/sarif.py +0 -0
  79. {thailint-0.10.0 → thailint-0.11.0}/src/linter_config/__init__.py +0 -0
  80. {thailint-0.10.0 → thailint-0.11.0}/src/linter_config/loader.py +0 -0
  81. {thailint-0.10.0 → thailint-0.11.0}/src/linters/__init__.py +0 -0
  82. {thailint-0.10.0 → thailint-0.11.0}/src/linters/collection_pipeline/__init__.py +0 -0
  83. {thailint-0.10.0 → thailint-0.11.0}/src/linters/collection_pipeline/config.py +0 -0
  84. {thailint-0.10.0 → thailint-0.11.0}/src/linters/collection_pipeline/continue_analyzer.py +0 -0
  85. {thailint-0.10.0 → thailint-0.11.0}/src/linters/collection_pipeline/detector.py +0 -0
  86. {thailint-0.10.0 → thailint-0.11.0}/src/linters/collection_pipeline/suggestion_builder.py +0 -0
  87. {thailint-0.10.0 → thailint-0.11.0}/src/linters/dry/__init__.py +0 -0
  88. {thailint-0.10.0 → thailint-0.11.0}/src/linters/dry/base_token_analyzer.py +0 -0
  89. {thailint-0.10.0 → thailint-0.11.0}/src/linters/dry/block_grouper.py +0 -0
  90. {thailint-0.10.0 → thailint-0.11.0}/src/linters/dry/cache_query.py +0 -0
  91. {thailint-0.10.0 → thailint-0.11.0}/src/linters/dry/config_loader.py +0 -0
  92. {thailint-0.10.0 → thailint-0.11.0}/src/linters/dry/deduplicator.py +0 -0
  93. {thailint-0.10.0 → thailint-0.11.0}/src/linters/dry/duplicate_storage.py +0 -0
  94. {thailint-0.10.0 → thailint-0.11.0}/src/linters/dry/file_analyzer.py +0 -0
  95. {thailint-0.10.0 → thailint-0.11.0}/src/linters/dry/inline_ignore.py +0 -0
  96. {thailint-0.10.0 → thailint-0.11.0}/src/linters/dry/storage_initializer.py +0 -0
  97. {thailint-0.10.0 → thailint-0.11.0}/src/linters/dry/violation_builder.py +0 -0
  98. {thailint-0.10.0 → thailint-0.11.0}/src/linters/dry/violation_filter.py +0 -0
  99. {thailint-0.10.0 → thailint-0.11.0}/src/linters/dry/violation_generator.py +0 -0
  100. {thailint-0.10.0 → thailint-0.11.0}/src/linters/file_header/__init__.py +0 -0
  101. {thailint-0.10.0 → thailint-0.11.0}/src/linters/file_header/atemporal_detector.py +0 -0
  102. {thailint-0.10.0 → thailint-0.11.0}/src/linters/file_header/base_parser.py +0 -0
  103. {thailint-0.10.0 → thailint-0.11.0}/src/linters/file_header/bash_parser.py +0 -0
  104. {thailint-0.10.0 → thailint-0.11.0}/src/linters/file_header/config.py +0 -0
  105. {thailint-0.10.0 → thailint-0.11.0}/src/linters/file_header/css_parser.py +0 -0
  106. {thailint-0.10.0 → thailint-0.11.0}/src/linters/file_header/field_validator.py +0 -0
  107. {thailint-0.10.0 → thailint-0.11.0}/src/linters/file_header/markdown_parser.py +0 -0
  108. {thailint-0.10.0 → thailint-0.11.0}/src/linters/file_header/python_parser.py +0 -0
  109. {thailint-0.10.0 → thailint-0.11.0}/src/linters/file_header/typescript_parser.py +0 -0
  110. {thailint-0.10.0 → thailint-0.11.0}/src/linters/file_header/violation_builder.py +0 -0
  111. {thailint-0.10.0 → thailint-0.11.0}/src/linters/file_placement/__init__.py +0 -0
  112. {thailint-0.10.0 → thailint-0.11.0}/src/linters/file_placement/config_loader.py +0 -0
  113. {thailint-0.10.0 → thailint-0.11.0}/src/linters/file_placement/directory_matcher.py +0 -0
  114. {thailint-0.10.0 → thailint-0.11.0}/src/linters/file_placement/path_resolver.py +0 -0
  115. {thailint-0.10.0 → thailint-0.11.0}/src/linters/file_placement/pattern_validator.py +0 -0
  116. {thailint-0.10.0 → thailint-0.11.0}/src/linters/file_placement/rule_checker.py +0 -0
  117. {thailint-0.10.0 → thailint-0.11.0}/src/linters/file_placement/violation_factory.py +0 -0
  118. {thailint-0.10.0 → thailint-0.11.0}/src/linters/magic_numbers/__init__.py +0 -0
  119. {thailint-0.10.0 → thailint-0.11.0}/src/linters/magic_numbers/config.py +0 -0
  120. {thailint-0.10.0 → thailint-0.11.0}/src/linters/magic_numbers/context_analyzer.py +0 -0
  121. {thailint-0.10.0 → thailint-0.11.0}/src/linters/magic_numbers/python_analyzer.py +0 -0
  122. {thailint-0.10.0 → thailint-0.11.0}/src/linters/magic_numbers/typescript_analyzer.py +0 -0
  123. {thailint-0.10.0 → thailint-0.11.0}/src/linters/magic_numbers/violation_builder.py +0 -0
  124. {thailint-0.10.0 → thailint-0.11.0}/src/linters/method_property/__init__.py +0 -0
  125. {thailint-0.10.0 → thailint-0.11.0}/src/linters/method_property/config.py +0 -0
  126. {thailint-0.10.0 → thailint-0.11.0}/src/linters/method_property/linter.py +0 -0
  127. {thailint-0.10.0 → thailint-0.11.0}/src/linters/method_property/python_analyzer.py +0 -0
  128. {thailint-0.10.0 → thailint-0.11.0}/src/linters/method_property/violation_builder.py +0 -0
  129. {thailint-0.10.0 → thailint-0.11.0}/src/linters/nesting/__init__.py +0 -0
  130. {thailint-0.10.0 → thailint-0.11.0}/src/linters/nesting/config.py +0 -0
  131. {thailint-0.10.0 → thailint-0.11.0}/src/linters/nesting/python_analyzer.py +0 -0
  132. {thailint-0.10.0 → thailint-0.11.0}/src/linters/nesting/typescript_analyzer.py +0 -0
  133. {thailint-0.10.0 → thailint-0.11.0}/src/linters/nesting/typescript_function_extractor.py +0 -0
  134. {thailint-0.10.0 → thailint-0.11.0}/src/linters/nesting/violation_builder.py +0 -0
  135. {thailint-0.10.0 → thailint-0.11.0}/src/linters/print_statements/__init__.py +0 -0
  136. {thailint-0.10.0 → thailint-0.11.0}/src/linters/print_statements/config.py +0 -0
  137. {thailint-0.10.0 → thailint-0.11.0}/src/linters/print_statements/python_analyzer.py +0 -0
  138. {thailint-0.10.0 → thailint-0.11.0}/src/linters/print_statements/typescript_analyzer.py +0 -0
  139. {thailint-0.10.0 → thailint-0.11.0}/src/linters/print_statements/violation_builder.py +0 -0
  140. {thailint-0.10.0 → thailint-0.11.0}/src/linters/srp/__init__.py +0 -0
  141. {thailint-0.10.0 → thailint-0.11.0}/src/linters/srp/config.py +0 -0
  142. {thailint-0.10.0 → thailint-0.11.0}/src/linters/srp/metrics_evaluator.py +0 -0
  143. {thailint-0.10.0 → thailint-0.11.0}/src/linters/srp/python_analyzer.py +0 -0
  144. {thailint-0.10.0 → thailint-0.11.0}/src/linters/srp/typescript_analyzer.py +0 -0
  145. {thailint-0.10.0 → thailint-0.11.0}/src/linters/srp/typescript_metrics_calculator.py +0 -0
  146. {thailint-0.10.0 → thailint-0.11.0}/src/linters/srp/violation_builder.py +0 -0
  147. {thailint-0.10.0 → thailint-0.11.0}/src/linters/stateless_class/__init__.py +0 -0
  148. {thailint-0.10.0 → thailint-0.11.0}/src/linters/stateless_class/config.py +0 -0
  149. {thailint-0.10.0 → thailint-0.11.0}/src/linters/stateless_class/python_analyzer.py +0 -0
  150. {thailint-0.10.0 → thailint-0.11.0}/src/orchestrator/__init__.py +0 -0
  151. {thailint-0.10.0 → thailint-0.11.0}/src/orchestrator/language_detector.py +0 -0
  152. {thailint-0.10.0 → thailint-0.11.0}/src/templates/thailint_config_template.yaml +0 -0
  153. {thailint-0.10.0 → thailint-0.11.0}/src/utils/__init__.py +0 -0
  154. {thailint-0.10.0 → thailint-0.11.0}/src/utils/project_root.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: thailint
3
- Version: 0.10.0
3
+ Version: 0.11.0
4
4
  Summary: The AI Linter - Enterprise-grade linting and governance for AI-generated code across multiple languages
5
5
  License: MIT
6
6
  License-File: LICENSE
@@ -37,7 +37,7 @@ Description-Content-Type: text/markdown
37
37
 
38
38
  [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
39
39
  [![Python 3.11+](https://img.shields.io/badge/python-3.11+-blue.svg)](https://www.python.org/downloads/)
40
- [![Tests](https://img.shields.io/badge/tests-795%2F795%20passing-brightgreen.svg)](tests/)
40
+ [![Tests](https://img.shields.io/badge/tests-884%2F884%20passing-brightgreen.svg)](tests/)
41
41
  [![Coverage](https://img.shields.io/badge/coverage-88%25-brightgreen.svg)](htmlcov/)
42
42
  [![Documentation Status](https://readthedocs.org/projects/thai-lint/badge/?version=latest)](https://thai-lint.readthedocs.io/en/latest/?badge=latest)
43
43
  [![SARIF 2.1.0](https://img.shields.io/badge/SARIF-2.1.0-orange.svg)](docs/sarif-output.md)
@@ -2,7 +2,7 @@
2
2
 
3
3
  [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
4
4
  [![Python 3.11+](https://img.shields.io/badge/python-3.11+-blue.svg)](https://www.python.org/downloads/)
5
- [![Tests](https://img.shields.io/badge/tests-795%2F795%20passing-brightgreen.svg)](tests/)
5
+ [![Tests](https://img.shields.io/badge/tests-884%2F884%20passing-brightgreen.svg)](tests/)
6
6
  [![Coverage](https://img.shields.io/badge/coverage-88%25-brightgreen.svg)](htmlcov/)
7
7
  [![Documentation Status](https://readthedocs.org/projects/thai-lint/badge/?version=latest)](https://thai-lint.readthedocs.io/en/latest/?badge=latest)
8
8
  [![SARIF 2.1.0](https://img.shields.io/badge/SARIF-2.1.0-orange.svg)](docs/sarif-output.md)
@@ -17,7 +17,7 @@ build-backend = "poetry.core.masonry.api"
17
17
 
18
18
  [tool.poetry]
19
19
  name = "thailint"
20
- version = "0.10.0"
20
+ version = "0.11.0"
21
21
  description = "The AI Linter - Enterprise-grade linting and governance for AI-generated code across multiple languages"
22
22
  authors = ["Steve Jackson"]
23
23
  license = "MIT"
@@ -104,8 +104,8 @@ loguru = "^0.7.3"
104
104
  pytest-xdist = "^3.8.0"
105
105
 
106
106
  [tool.poetry.scripts]
107
- thailint = "src.cli:cli"
108
- thai-lint = "src.cli:cli"
107
+ thailint = "src.cli_main:cli"
108
+ thai-lint = "src.cli_main:cli"
109
109
 
110
110
  # Ruff configuration
111
111
  [tool.ruff]
@@ -228,6 +228,7 @@ disable = [
228
228
 
229
229
  [tool.pylint.format]
230
230
  max-line-length = 120
231
+ max-module-lines = 500
231
232
 
232
233
  # Flake8 configuration (in .flake8 file, not pyproject.toml)
233
234
  # Note: Flake8 doesn't support pyproject.toml natively
@@ -19,6 +19,7 @@ Exports: __version__, Linter (high-level API), cli (CLI entry point), load_confi
19
19
  Interfaces: Package version string, Linter class API, CLI command group, configuration functions
20
20
  """
21
21
 
22
+ __version__: str
22
23
  try:
23
24
  from importlib.metadata import version
24
25
 
@@ -0,0 +1,27 @@
1
+ """
2
+ Purpose: CLI package entry point and public API for thai-lint command-line interface
3
+
4
+ Scope: Re-export fully configured CLI with all commands registered
5
+
6
+ Overview: Provides the public API for the modular CLI package by re-exporting the CLI group from
7
+ src.cli.main and triggering command registration by importing submodules. Importing from this
8
+ module (src.cli) gives access to the complete CLI with all commands. Maintains backward
9
+ compatibility with code that imports from src.cli while enabling modular organization.
10
+
11
+ Dependencies: src.cli.main for CLI group, src.cli.config for config commands, src.cli.linters
12
+ for linter commands
13
+
14
+ Exports: cli (main Click command group with all commands registered)
15
+
16
+ Interfaces: Single import point for CLI access via 'from src.cli import cli'
17
+
18
+ Implementation: Imports submodules to trigger command registration via Click decorators
19
+ """
20
+
21
+ # Import the CLI group from main module
22
+ # Import config and linters to register their commands with the CLI group
23
+ from src.cli import config as _config_module # noqa: F401
24
+ from src.cli import linters as _linters_module # noqa: F401
25
+ from src.cli.main import cli # noqa: F401
26
+
27
+ __all__ = ["cli"]
@@ -0,0 +1,22 @@
1
+ """
2
+ Purpose: Entry point for running thai-lint CLI as a module (python -m src.cli)
3
+
4
+ Scope: Module execution support for direct CLI invocation
5
+
6
+ Overview: Enables running the CLI via 'python -m src.cli' by invoking the main cli group.
7
+ This file is executed when the package is run as a module, providing an alternative
8
+ entry point to the installed 'thailint' command.
9
+
10
+ Dependencies: src.cli for fully configured CLI
11
+
12
+ Exports: None (execution entry point only)
13
+
14
+ Interfaces: Command-line invocation via 'python -m src.cli [command] [args]'
15
+
16
+ Implementation: Imports and invokes cli() from the package
17
+ """
18
+
19
+ from src.cli import cli
20
+
21
+ if __name__ == "__main__":
22
+ cli()
@@ -0,0 +1,478 @@
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
+
30
+ from src.config import ConfigError, save_config, validate_config
31
+
32
+ from .main import cli
33
+
34
+ # Configure module logger
35
+ logger = logging.getLogger(__name__)
36
+
37
+
38
+ # =============================================================================
39
+ # Config Command Group
40
+ # =============================================================================
41
+
42
+
43
+ @cli.group()
44
+ def config() -> None:
45
+ """Configuration management commands."""
46
+ pass
47
+
48
+
49
+ # =============================================================================
50
+ # Config Show Command
51
+ # =============================================================================
52
+
53
+
54
+ @config.command("show")
55
+ @click.option(
56
+ "--format",
57
+ "-f",
58
+ type=click.Choice(["text", "json", "yaml"]),
59
+ default="text",
60
+ help="Output format",
61
+ )
62
+ @click.pass_context
63
+ def config_show(ctx: click.Context, format: str) -> None:
64
+ """Display current configuration.
65
+
66
+ Shows all configuration values in the specified format.
67
+
68
+ Examples:
69
+
70
+ \b
71
+ # Show as text
72
+ thai-lint config show
73
+
74
+ \b
75
+ # Show as JSON
76
+ thai-lint config show --format json
77
+
78
+ \b
79
+ # Show as YAML
80
+ thai-lint config show --format yaml
81
+ """
82
+ cfg = ctx.obj["config"]
83
+
84
+ formatters = {
85
+ "json": _format_config_json,
86
+ "yaml": _format_config_yaml,
87
+ "text": _format_config_text,
88
+ }
89
+ formatters[format](cfg)
90
+
91
+
92
+ def _format_config_json(cfg: dict) -> None:
93
+ """Format configuration as JSON."""
94
+ import json
95
+
96
+ click.echo(json.dumps(cfg, indent=2))
97
+
98
+
99
+ def _format_config_yaml(cfg: dict) -> None:
100
+ """Format configuration as YAML."""
101
+ import yaml
102
+
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
+ if value.lower() in ["true", "false"]:
154
+ return value.lower() == "true"
155
+ if value.isdigit():
156
+ return int(value)
157
+ if value.replace(".", "", 1).isdigit() and value.count(".") == 1:
158
+ return float(value)
159
+ return value
160
+
161
+
162
+ def _validate_and_report_errors(cfg: dict) -> None:
163
+ """Validate configuration and report errors."""
164
+ is_valid, errors = validate_config(cfg)
165
+ if not is_valid:
166
+ click.echo("Invalid configuration:", err=True)
167
+ for error in errors:
168
+ click.echo(f" - {error}", err=True)
169
+ sys.exit(1)
170
+
171
+
172
+ def _save_and_report_success(
173
+ cfg: dict, key: str, value: bool | int | float | str, config_path: Path | None, verbose: bool
174
+ ) -> None:
175
+ """Save configuration and report success."""
176
+ save_config(cfg, config_path)
177
+ click.echo(f"Set {key} = {value}")
178
+ if verbose:
179
+ logger.info(f"Configuration updated: {key}={value}")
180
+
181
+
182
+ @config.command("set")
183
+ @click.argument("key")
184
+ @click.argument("value")
185
+ @click.pass_context
186
+ def config_set(ctx: click.Context, key: str, value: str) -> None:
187
+ """Set configuration value.
188
+
189
+ KEY: Configuration key to set
190
+
191
+ VALUE: New value for the key
192
+
193
+ Examples:
194
+
195
+ \b
196
+ # Set log level
197
+ thai-lint config set log_level DEBUG
198
+
199
+ \b
200
+ # Set greeting template
201
+ thai-lint config set greeting "Hi"
202
+
203
+ \b
204
+ # Set numeric value
205
+ thai-lint config set max_retries 5
206
+ """
207
+ cfg = ctx.obj["config"]
208
+ converted_value = _convert_value_type(value)
209
+ cfg[key] = converted_value
210
+
211
+ try:
212
+ _validate_and_report_errors(cfg)
213
+ except Exception as e:
214
+ click.echo(f"Validation error: {e}", err=True)
215
+ sys.exit(1)
216
+
217
+ try:
218
+ config_path = ctx.obj.get("config_path")
219
+ verbose = ctx.obj.get("verbose", False)
220
+ _save_and_report_success(cfg, key, converted_value, config_path, verbose)
221
+ except ConfigError as e:
222
+ click.echo(f"Error saving configuration: {e}", err=True)
223
+ sys.exit(1)
224
+
225
+
226
+ # =============================================================================
227
+ # Config Reset Command
228
+ # =============================================================================
229
+
230
+
231
+ @config.command("reset")
232
+ @click.option("--yes", "-y", is_flag=True, help="Skip confirmation prompt")
233
+ @click.pass_context
234
+ def config_reset(ctx: click.Context, yes: bool) -> None:
235
+ """Reset configuration to defaults.
236
+
237
+ Examples:
238
+
239
+ \b
240
+ # Reset with confirmation
241
+ thai-lint config reset
242
+
243
+ \b
244
+ # Reset without confirmation
245
+ thai-lint config reset --yes
246
+ """
247
+ if not yes:
248
+ click.confirm("Reset configuration to defaults?", abort=True)
249
+
250
+ from src.config import DEFAULT_CONFIG
251
+
252
+ try:
253
+ config_path = ctx.obj.get("config_path")
254
+ save_config(DEFAULT_CONFIG.copy(), config_path)
255
+ click.echo("Configuration reset to defaults")
256
+
257
+ if ctx.obj.get("verbose"):
258
+ logger.info("Configuration reset to defaults")
259
+ except ConfigError as e:
260
+ click.echo(f"Error resetting configuration: {e}", err=True)
261
+ sys.exit(1)
262
+
263
+
264
+ # =============================================================================
265
+ # Init Config Command
266
+ # =============================================================================
267
+
268
+
269
+ @cli.command("init-config")
270
+ @click.option(
271
+ "--preset",
272
+ "-p",
273
+ type=click.Choice(["strict", "standard", "lenient"]),
274
+ default="standard",
275
+ help="Configuration preset",
276
+ )
277
+ @click.option("--non-interactive", is_flag=True, help="Skip interactive prompts (for AI agents)")
278
+ @click.option("--force", is_flag=True, help="Overwrite existing .thailint.yaml file")
279
+ @click.option(
280
+ "--output", "-o", type=click.Path(), default=".thailint.yaml", help="Output file path"
281
+ )
282
+ def init_config(preset: str, non_interactive: bool, force: bool, output: str) -> None:
283
+ """Generate a .thailint.yaml configuration file with preset values.
284
+
285
+ Creates a richly-commented configuration file with sensible defaults
286
+ and optional customizations for different strictness levels.
287
+
288
+ For AI agents, use --non-interactive mode:
289
+ thailint init-config --non-interactive --preset lenient
290
+
291
+ Presets:
292
+ strict: Minimal allowed numbers (only -1, 0, 1)
293
+ standard: Balanced defaults (includes 2, 3, 4, 5, 10, 100, 1000)
294
+ lenient: Includes time conversions (adds 60, 3600)
295
+
296
+ Examples:
297
+
298
+ \\b
299
+ # Interactive mode (default, for humans)
300
+ thailint init-config
301
+
302
+ \\b
303
+ # Non-interactive mode (for AI agents)
304
+ thailint init-config --non-interactive
305
+
306
+ \\b
307
+ # Generate with lenient preset
308
+ thailint init-config --preset lenient
309
+
310
+ \\b
311
+ # Overwrite existing config
312
+ thailint init-config --force
313
+
314
+ \\b
315
+ # Custom output path
316
+ thailint init-config --output my-config.yaml
317
+ """
318
+ output_path = Path(output)
319
+
320
+ # Check if file exists (unless --force)
321
+ if output_path.exists() and not force:
322
+ click.echo(f"Error: {output} already exists", err=True)
323
+ click.echo("", err=True)
324
+ click.echo("Use --force to overwrite:", err=True)
325
+ click.echo(" thailint init-config --force", err=True)
326
+ sys.exit(1)
327
+
328
+ # Interactive mode: Ask user for preferences
329
+ if not non_interactive:
330
+ preset = _run_interactive_preset_selection(preset)
331
+
332
+ # Generate config based on preset
333
+ config_content = _generate_config_content(preset)
334
+
335
+ # Write config file
336
+ _write_config_file(output_path, config_content, preset, output)
337
+
338
+
339
+ def _run_interactive_preset_selection(default_preset: str) -> str:
340
+ """Run interactive preset selection.
341
+
342
+ Args:
343
+ default_preset: Default preset to use if user accepts default
344
+
345
+ Returns:
346
+ Selected preset name
347
+ """
348
+ click.echo("thai-lint Configuration Generator")
349
+ click.echo("=" * 50)
350
+ click.echo("")
351
+ click.echo("This will create a .thailint.yaml configuration file.")
352
+ click.echo("For non-interactive mode (AI agents), use:")
353
+ click.echo(" thailint init-config --non-interactive")
354
+ click.echo("")
355
+
356
+ # Show preset options
357
+ click.echo("Available presets:")
358
+ click.echo(" strict: Only -1, 0, 1 allowed (strictest)")
359
+ click.echo(" standard: -1, 0, 1, 2, 3, 4, 5, 10, 100, 1000 (balanced)")
360
+ click.echo(" lenient: Includes time conversions 60, 3600 (most permissive)")
361
+ click.echo("")
362
+
363
+ preset_choices = click.Choice(["strict", "standard", "lenient"])
364
+ result: str = click.prompt("Choose preset", type=preset_choices, default=default_preset)
365
+ return result
366
+
367
+
368
+ def _generate_config_content(preset: str) -> str:
369
+ """Generate config file content based on preset.
370
+
371
+ Args:
372
+ preset: Preset name (strict, standard, or lenient)
373
+
374
+ Returns:
375
+ Generated configuration file content
376
+ """
377
+ # Preset configurations
378
+ presets = {
379
+ "strict": {
380
+ "allowed_numbers": "[-1, 0, 1]",
381
+ "max_small_integer": "3",
382
+ "description": "Strict (only universal values)",
383
+ },
384
+ "standard": {
385
+ "allowed_numbers": "[-1, 0, 1, 2, 3, 4, 5, 10, 100, 1000]",
386
+ "max_small_integer": "10",
387
+ "description": "Standard (balanced defaults)",
388
+ },
389
+ "lenient": {
390
+ "allowed_numbers": "[-1, 0, 1, 2, 3, 4, 5, 10, 60, 100, 1000, 3600]",
391
+ "max_small_integer": "10",
392
+ "description": "Lenient (includes time conversions)",
393
+ },
394
+ }
395
+
396
+ config = presets[preset]
397
+
398
+ # Read template - use parent of parent since we're in src/cli/
399
+ template_path = Path(__file__).parent.parent / "templates" / "thailint_config_template.yaml"
400
+ template = template_path.read_text(encoding="utf-8")
401
+
402
+ # Replace placeholders
403
+ content = template.replace("{{PRESET}}", config["description"])
404
+ content = content.replace("{{ALLOWED_NUMBERS}}", config["allowed_numbers"])
405
+ content = content.replace("{{MAX_SMALL_INTEGER}}", config["max_small_integer"])
406
+
407
+ return content
408
+
409
+
410
+ def _write_config_file(output_path: Path, content: str, preset: str, output: str) -> None:
411
+ """Write configuration file and show success message.
412
+
413
+ Args:
414
+ output_path: Path to write file to
415
+ content: File content to write
416
+ preset: Selected preset name
417
+ output: Output filename for display
418
+ """
419
+ try:
420
+ output_path.write_text(content, encoding="utf-8")
421
+ click.echo("")
422
+ click.echo(f"Created {output}")
423
+ click.echo(f"Preset: {preset}")
424
+ click.echo("")
425
+ click.echo("Next steps:")
426
+ click.echo(f" 1. Review and customize {output}")
427
+ click.echo(" 2. Run: thailint magic-numbers .")
428
+ click.echo(" 3. See docs: https://github.com/your-org/thai-lint")
429
+ except OSError as e:
430
+ click.echo(f"Error writing config file: {e}", err=True)
431
+ sys.exit(1)
432
+
433
+
434
+ # =============================================================================
435
+ # Hello Command (Example Command)
436
+ # =============================================================================
437
+
438
+
439
+ @cli.command()
440
+ @click.option("--name", "-n", default="World", help="Name to greet")
441
+ @click.option("--uppercase", "-u", is_flag=True, help="Convert greeting to uppercase")
442
+ @click.pass_context
443
+ def hello(ctx: click.Context, name: str, uppercase: bool) -> None:
444
+ """Print a greeting message.
445
+
446
+ This is a simple example command demonstrating CLI basics.
447
+
448
+ Examples:
449
+
450
+ \b
451
+ # Basic greeting
452
+ thai-lint hello
453
+
454
+ \b
455
+ # Custom name
456
+ thai-lint hello --name Alice
457
+
458
+ \b
459
+ # Uppercase output
460
+ thai-lint hello --name Bob --uppercase
461
+ """
462
+ config = ctx.obj["config"]
463
+ verbose = ctx.obj.get("verbose", False)
464
+
465
+ # Get greeting from config or use default
466
+ greeting_template = config.get("greeting", "Hello")
467
+
468
+ # Build greeting message
469
+ message = f"{greeting_template}, {name}!"
470
+
471
+ if uppercase:
472
+ message = message.upper()
473
+
474
+ # Output greeting
475
+ click.echo(message)
476
+
477
+ if verbose:
478
+ logger.info(f"Greeted {name} with template '{greeting_template}'")
@@ -0,0 +1,58 @@
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
+
21
+ # Import all linter command modules to register them with the CLI
22
+ # Each module uses @cli.command() decorators that register on import
23
+ from src.cli.linters import ( # noqa: F401
24
+ code_patterns,
25
+ code_smells,
26
+ documentation,
27
+ structure,
28
+ structure_quality,
29
+ )
30
+
31
+ # Re-export command functions for testing and reference
32
+ from src.cli.linters.code_patterns import (
33
+ method_property,
34
+ print_statements,
35
+ stateless_class,
36
+ )
37
+ from src.cli.linters.code_smells import dry, magic_numbers
38
+ from src.cli.linters.documentation import file_header
39
+ from src.cli.linters.structure import file_placement, pipeline
40
+ from src.cli.linters.structure_quality import nesting, srp
41
+
42
+ __all__ = [
43
+ # Structure quality commands
44
+ "nesting",
45
+ "srp",
46
+ # Code smell commands
47
+ "dry",
48
+ "magic_numbers",
49
+ # Code pattern commands
50
+ "print_statements",
51
+ "method_property",
52
+ "stateless_class",
53
+ # Structure commands
54
+ "file_placement",
55
+ "pipeline",
56
+ # Documentation commands
57
+ "file_header",
58
+ ]