thailint 0.11.0__tar.gz → 0.12.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 (165) hide show
  1. {thailint-0.11.0 → thailint-0.12.0}/PKG-INFO +9 -3
  2. {thailint-0.11.0 → thailint-0.12.0}/README.md +8 -2
  3. {thailint-0.11.0 → thailint-0.12.0}/pyproject.toml +1 -1
  4. {thailint-0.11.0 → thailint-0.12.0}/src/cli/linters/code_smells.py +114 -7
  5. {thailint-0.11.0 → thailint-0.12.0}/src/cli/utils.py +29 -9
  6. thailint-0.12.0/src/linters/stringly_typed/__init__.py +36 -0
  7. {thailint-0.11.0 → thailint-0.12.0}/src/linters/stringly_typed/config.py +28 -3
  8. thailint-0.12.0/src/linters/stringly_typed/context_filter.py +451 -0
  9. thailint-0.12.0/src/linters/stringly_typed/function_call_violation_builder.py +137 -0
  10. thailint-0.12.0/src/linters/stringly_typed/ignore_checker.py +102 -0
  11. thailint-0.12.0/src/linters/stringly_typed/ignore_utils.py +51 -0
  12. thailint-0.12.0/src/linters/stringly_typed/linter.py +344 -0
  13. {thailint-0.11.0 → thailint-0.12.0}/src/linters/stringly_typed/python/__init__.py +9 -5
  14. {thailint-0.11.0 → thailint-0.12.0}/src/linters/stringly_typed/python/analyzer.py +155 -9
  15. thailint-0.12.0/src/linters/stringly_typed/python/call_tracker.py +172 -0
  16. thailint-0.12.0/src/linters/stringly_typed/python/comparison_tracker.py +252 -0
  17. thailint-0.12.0/src/linters/stringly_typed/storage.py +630 -0
  18. thailint-0.12.0/src/linters/stringly_typed/storage_initializer.py +45 -0
  19. thailint-0.12.0/src/linters/stringly_typed/typescript/__init__.py +28 -0
  20. thailint-0.12.0/src/linters/stringly_typed/typescript/analyzer.py +157 -0
  21. thailint-0.12.0/src/linters/stringly_typed/typescript/call_tracker.py +329 -0
  22. thailint-0.12.0/src/linters/stringly_typed/typescript/comparison_tracker.py +372 -0
  23. thailint-0.12.0/src/linters/stringly_typed/violation_generator.py +376 -0
  24. thailint-0.11.0/src/linters/stringly_typed/__init__.py +0 -23
  25. {thailint-0.11.0 → thailint-0.12.0}/CHANGELOG.md +0 -0
  26. {thailint-0.11.0 → thailint-0.12.0}/LICENSE +0 -0
  27. {thailint-0.11.0 → thailint-0.12.0}/src/__init__.py +0 -0
  28. {thailint-0.11.0 → thailint-0.12.0}/src/analyzers/__init__.py +0 -0
  29. {thailint-0.11.0 → thailint-0.12.0}/src/analyzers/typescript_base.py +0 -0
  30. {thailint-0.11.0 → thailint-0.12.0}/src/api.py +0 -0
  31. {thailint-0.11.0 → thailint-0.12.0}/src/cli/__init__.py +0 -0
  32. {thailint-0.11.0 → thailint-0.12.0}/src/cli/__main__.py +0 -0
  33. {thailint-0.11.0 → thailint-0.12.0}/src/cli/config.py +0 -0
  34. {thailint-0.11.0 → thailint-0.12.0}/src/cli/linters/__init__.py +0 -0
  35. {thailint-0.11.0 → thailint-0.12.0}/src/cli/linters/code_patterns.py +0 -0
  36. {thailint-0.11.0 → thailint-0.12.0}/src/cli/linters/documentation.py +0 -0
  37. {thailint-0.11.0 → thailint-0.12.0}/src/cli/linters/shared.py +0 -0
  38. {thailint-0.11.0 → thailint-0.12.0}/src/cli/linters/structure.py +0 -0
  39. {thailint-0.11.0 → thailint-0.12.0}/src/cli/linters/structure_quality.py +0 -0
  40. {thailint-0.11.0 → thailint-0.12.0}/src/cli/main.py +0 -0
  41. {thailint-0.11.0 → thailint-0.12.0}/src/cli_main.py +0 -0
  42. {thailint-0.11.0 → thailint-0.12.0}/src/config.py +0 -0
  43. {thailint-0.11.0 → thailint-0.12.0}/src/core/__init__.py +0 -0
  44. {thailint-0.11.0 → thailint-0.12.0}/src/core/base.py +0 -0
  45. {thailint-0.11.0 → thailint-0.12.0}/src/core/cli_utils.py +0 -0
  46. {thailint-0.11.0 → thailint-0.12.0}/src/core/config_parser.py +0 -0
  47. {thailint-0.11.0 → thailint-0.12.0}/src/core/linter_utils.py +0 -0
  48. {thailint-0.11.0 → thailint-0.12.0}/src/core/registry.py +0 -0
  49. {thailint-0.11.0 → thailint-0.12.0}/src/core/rule_discovery.py +0 -0
  50. {thailint-0.11.0 → thailint-0.12.0}/src/core/types.py +0 -0
  51. {thailint-0.11.0 → thailint-0.12.0}/src/core/violation_builder.py +0 -0
  52. {thailint-0.11.0 → thailint-0.12.0}/src/core/violation_utils.py +0 -0
  53. {thailint-0.11.0 → thailint-0.12.0}/src/formatters/__init__.py +0 -0
  54. {thailint-0.11.0 → thailint-0.12.0}/src/formatters/sarif.py +0 -0
  55. {thailint-0.11.0 → thailint-0.12.0}/src/linter_config/__init__.py +0 -0
  56. {thailint-0.11.0 → thailint-0.12.0}/src/linter_config/ignore.py +0 -0
  57. {thailint-0.11.0 → thailint-0.12.0}/src/linter_config/loader.py +0 -0
  58. {thailint-0.11.0 → thailint-0.12.0}/src/linters/__init__.py +0 -0
  59. {thailint-0.11.0 → thailint-0.12.0}/src/linters/collection_pipeline/__init__.py +0 -0
  60. {thailint-0.11.0 → thailint-0.12.0}/src/linters/collection_pipeline/config.py +0 -0
  61. {thailint-0.11.0 → thailint-0.12.0}/src/linters/collection_pipeline/continue_analyzer.py +0 -0
  62. {thailint-0.11.0 → thailint-0.12.0}/src/linters/collection_pipeline/detector.py +0 -0
  63. {thailint-0.11.0 → thailint-0.12.0}/src/linters/collection_pipeline/linter.py +0 -0
  64. {thailint-0.11.0 → thailint-0.12.0}/src/linters/collection_pipeline/suggestion_builder.py +0 -0
  65. {thailint-0.11.0 → thailint-0.12.0}/src/linters/dry/__init__.py +0 -0
  66. {thailint-0.11.0 → thailint-0.12.0}/src/linters/dry/base_token_analyzer.py +0 -0
  67. {thailint-0.11.0 → thailint-0.12.0}/src/linters/dry/block_filter.py +0 -0
  68. {thailint-0.11.0 → thailint-0.12.0}/src/linters/dry/block_grouper.py +0 -0
  69. {thailint-0.11.0 → thailint-0.12.0}/src/linters/dry/cache.py +0 -0
  70. {thailint-0.11.0 → thailint-0.12.0}/src/linters/dry/cache_query.py +0 -0
  71. {thailint-0.11.0 → thailint-0.12.0}/src/linters/dry/config.py +0 -0
  72. {thailint-0.11.0 → thailint-0.12.0}/src/linters/dry/config_loader.py +0 -0
  73. {thailint-0.11.0 → thailint-0.12.0}/src/linters/dry/constant.py +0 -0
  74. {thailint-0.11.0 → thailint-0.12.0}/src/linters/dry/constant_matcher.py +0 -0
  75. {thailint-0.11.0 → thailint-0.12.0}/src/linters/dry/constant_violation_builder.py +0 -0
  76. {thailint-0.11.0 → thailint-0.12.0}/src/linters/dry/deduplicator.py +0 -0
  77. {thailint-0.11.0 → thailint-0.12.0}/src/linters/dry/duplicate_storage.py +0 -0
  78. {thailint-0.11.0 → thailint-0.12.0}/src/linters/dry/file_analyzer.py +0 -0
  79. {thailint-0.11.0 → thailint-0.12.0}/src/linters/dry/inline_ignore.py +0 -0
  80. {thailint-0.11.0 → thailint-0.12.0}/src/linters/dry/linter.py +0 -0
  81. {thailint-0.11.0 → thailint-0.12.0}/src/linters/dry/python_analyzer.py +0 -0
  82. {thailint-0.11.0 → thailint-0.12.0}/src/linters/dry/python_constant_extractor.py +0 -0
  83. {thailint-0.11.0 → thailint-0.12.0}/src/linters/dry/single_statement_detector.py +0 -0
  84. {thailint-0.11.0 → thailint-0.12.0}/src/linters/dry/storage_initializer.py +0 -0
  85. {thailint-0.11.0 → thailint-0.12.0}/src/linters/dry/token_hasher.py +0 -0
  86. {thailint-0.11.0 → thailint-0.12.0}/src/linters/dry/typescript_analyzer.py +0 -0
  87. {thailint-0.11.0 → thailint-0.12.0}/src/linters/dry/typescript_constant_extractor.py +0 -0
  88. {thailint-0.11.0 → thailint-0.12.0}/src/linters/dry/typescript_statement_detector.py +0 -0
  89. {thailint-0.11.0 → thailint-0.12.0}/src/linters/dry/typescript_value_extractor.py +0 -0
  90. {thailint-0.11.0 → thailint-0.12.0}/src/linters/dry/violation_builder.py +0 -0
  91. {thailint-0.11.0 → thailint-0.12.0}/src/linters/dry/violation_filter.py +0 -0
  92. {thailint-0.11.0 → thailint-0.12.0}/src/linters/dry/violation_generator.py +0 -0
  93. {thailint-0.11.0 → thailint-0.12.0}/src/linters/file_header/__init__.py +0 -0
  94. {thailint-0.11.0 → thailint-0.12.0}/src/linters/file_header/atemporal_detector.py +0 -0
  95. {thailint-0.11.0 → thailint-0.12.0}/src/linters/file_header/base_parser.py +0 -0
  96. {thailint-0.11.0 → thailint-0.12.0}/src/linters/file_header/bash_parser.py +0 -0
  97. {thailint-0.11.0 → thailint-0.12.0}/src/linters/file_header/config.py +0 -0
  98. {thailint-0.11.0 → thailint-0.12.0}/src/linters/file_header/css_parser.py +0 -0
  99. {thailint-0.11.0 → thailint-0.12.0}/src/linters/file_header/field_validator.py +0 -0
  100. {thailint-0.11.0 → thailint-0.12.0}/src/linters/file_header/linter.py +0 -0
  101. {thailint-0.11.0 → thailint-0.12.0}/src/linters/file_header/markdown_parser.py +0 -0
  102. {thailint-0.11.0 → thailint-0.12.0}/src/linters/file_header/python_parser.py +0 -0
  103. {thailint-0.11.0 → thailint-0.12.0}/src/linters/file_header/typescript_parser.py +0 -0
  104. {thailint-0.11.0 → thailint-0.12.0}/src/linters/file_header/violation_builder.py +0 -0
  105. {thailint-0.11.0 → thailint-0.12.0}/src/linters/file_placement/__init__.py +0 -0
  106. {thailint-0.11.0 → thailint-0.12.0}/src/linters/file_placement/config_loader.py +0 -0
  107. {thailint-0.11.0 → thailint-0.12.0}/src/linters/file_placement/directory_matcher.py +0 -0
  108. {thailint-0.11.0 → thailint-0.12.0}/src/linters/file_placement/linter.py +0 -0
  109. {thailint-0.11.0 → thailint-0.12.0}/src/linters/file_placement/path_resolver.py +0 -0
  110. {thailint-0.11.0 → thailint-0.12.0}/src/linters/file_placement/pattern_matcher.py +0 -0
  111. {thailint-0.11.0 → thailint-0.12.0}/src/linters/file_placement/pattern_validator.py +0 -0
  112. {thailint-0.11.0 → thailint-0.12.0}/src/linters/file_placement/rule_checker.py +0 -0
  113. {thailint-0.11.0 → thailint-0.12.0}/src/linters/file_placement/violation_factory.py +0 -0
  114. {thailint-0.11.0 → thailint-0.12.0}/src/linters/magic_numbers/__init__.py +0 -0
  115. {thailint-0.11.0 → thailint-0.12.0}/src/linters/magic_numbers/config.py +0 -0
  116. {thailint-0.11.0 → thailint-0.12.0}/src/linters/magic_numbers/context_analyzer.py +0 -0
  117. {thailint-0.11.0 → thailint-0.12.0}/src/linters/magic_numbers/linter.py +0 -0
  118. {thailint-0.11.0 → thailint-0.12.0}/src/linters/magic_numbers/python_analyzer.py +0 -0
  119. {thailint-0.11.0 → thailint-0.12.0}/src/linters/magic_numbers/typescript_analyzer.py +0 -0
  120. {thailint-0.11.0 → thailint-0.12.0}/src/linters/magic_numbers/typescript_ignore_checker.py +0 -0
  121. {thailint-0.11.0 → thailint-0.12.0}/src/linters/magic_numbers/violation_builder.py +0 -0
  122. {thailint-0.11.0 → thailint-0.12.0}/src/linters/method_property/__init__.py +0 -0
  123. {thailint-0.11.0 → thailint-0.12.0}/src/linters/method_property/config.py +0 -0
  124. {thailint-0.11.0 → thailint-0.12.0}/src/linters/method_property/linter.py +0 -0
  125. {thailint-0.11.0 → thailint-0.12.0}/src/linters/method_property/python_analyzer.py +0 -0
  126. {thailint-0.11.0 → thailint-0.12.0}/src/linters/method_property/violation_builder.py +0 -0
  127. {thailint-0.11.0 → thailint-0.12.0}/src/linters/nesting/__init__.py +0 -0
  128. {thailint-0.11.0 → thailint-0.12.0}/src/linters/nesting/config.py +0 -0
  129. {thailint-0.11.0 → thailint-0.12.0}/src/linters/nesting/linter.py +0 -0
  130. {thailint-0.11.0 → thailint-0.12.0}/src/linters/nesting/python_analyzer.py +0 -0
  131. {thailint-0.11.0 → thailint-0.12.0}/src/linters/nesting/typescript_analyzer.py +0 -0
  132. {thailint-0.11.0 → thailint-0.12.0}/src/linters/nesting/typescript_function_extractor.py +0 -0
  133. {thailint-0.11.0 → thailint-0.12.0}/src/linters/nesting/violation_builder.py +0 -0
  134. {thailint-0.11.0 → thailint-0.12.0}/src/linters/print_statements/__init__.py +0 -0
  135. {thailint-0.11.0 → thailint-0.12.0}/src/linters/print_statements/config.py +0 -0
  136. {thailint-0.11.0 → thailint-0.12.0}/src/linters/print_statements/linter.py +0 -0
  137. {thailint-0.11.0 → thailint-0.12.0}/src/linters/print_statements/python_analyzer.py +0 -0
  138. {thailint-0.11.0 → thailint-0.12.0}/src/linters/print_statements/typescript_analyzer.py +0 -0
  139. {thailint-0.11.0 → thailint-0.12.0}/src/linters/print_statements/violation_builder.py +0 -0
  140. {thailint-0.11.0 → thailint-0.12.0}/src/linters/srp/__init__.py +0 -0
  141. {thailint-0.11.0 → thailint-0.12.0}/src/linters/srp/class_analyzer.py +0 -0
  142. {thailint-0.11.0 → thailint-0.12.0}/src/linters/srp/config.py +0 -0
  143. {thailint-0.11.0 → thailint-0.12.0}/src/linters/srp/heuristics.py +0 -0
  144. {thailint-0.11.0 → thailint-0.12.0}/src/linters/srp/linter.py +0 -0
  145. {thailint-0.11.0 → thailint-0.12.0}/src/linters/srp/metrics_evaluator.py +0 -0
  146. {thailint-0.11.0 → thailint-0.12.0}/src/linters/srp/python_analyzer.py +0 -0
  147. {thailint-0.11.0 → thailint-0.12.0}/src/linters/srp/typescript_analyzer.py +0 -0
  148. {thailint-0.11.0 → thailint-0.12.0}/src/linters/srp/typescript_metrics_calculator.py +0 -0
  149. {thailint-0.11.0 → thailint-0.12.0}/src/linters/srp/violation_builder.py +0 -0
  150. {thailint-0.11.0 → thailint-0.12.0}/src/linters/stateless_class/__init__.py +0 -0
  151. {thailint-0.11.0 → thailint-0.12.0}/src/linters/stateless_class/config.py +0 -0
  152. {thailint-0.11.0 → thailint-0.12.0}/src/linters/stateless_class/linter.py +0 -0
  153. {thailint-0.11.0 → thailint-0.12.0}/src/linters/stateless_class/python_analyzer.py +0 -0
  154. {thailint-0.11.0 → thailint-0.12.0}/src/linters/stringly_typed/python/condition_extractor.py +0 -0
  155. {thailint-0.11.0 → thailint-0.12.0}/src/linters/stringly_typed/python/conditional_detector.py +0 -0
  156. {thailint-0.11.0 → thailint-0.12.0}/src/linters/stringly_typed/python/constants.py +0 -0
  157. {thailint-0.11.0 → thailint-0.12.0}/src/linters/stringly_typed/python/match_analyzer.py +0 -0
  158. {thailint-0.11.0 → thailint-0.12.0}/src/linters/stringly_typed/python/validation_detector.py +0 -0
  159. {thailint-0.11.0 → thailint-0.12.0}/src/linters/stringly_typed/python/variable_extractor.py +0 -0
  160. {thailint-0.11.0 → thailint-0.12.0}/src/orchestrator/__init__.py +0 -0
  161. {thailint-0.11.0 → thailint-0.12.0}/src/orchestrator/core.py +0 -0
  162. {thailint-0.11.0 → thailint-0.12.0}/src/orchestrator/language_detector.py +0 -0
  163. {thailint-0.11.0 → thailint-0.12.0}/src/templates/thailint_config_template.yaml +0 -0
  164. {thailint-0.11.0 → thailint-0.12.0}/src/utils/__init__.py +0 -0
  165. {thailint-0.11.0 → thailint-0.12.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.11.0
3
+ Version: 0.12.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,8 +37,8 @@ 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-884%2F884%20passing-brightgreen.svg)](tests/)
41
- [![Coverage](https://img.shields.io/badge/coverage-88%25-brightgreen.svg)](htmlcov/)
40
+ [![Tests](https://img.shields.io/badge/tests-1076%2F1076%20passing-brightgreen.svg)](tests/)
41
+ [![Coverage](https://img.shields.io/badge/coverage-89%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)
44
44
 
@@ -116,6 +116,12 @@ thailint complements your existing linting stack by catching the patterns AI too
116
116
  - No instance state (self.attr) detection
117
117
  - Excludes ABC, Protocol, and decorated classes
118
118
  - Helpful refactoring suggestions
119
+ - **Stringly-Typed Linting** - Detect string patterns that should use enums
120
+ - Python and TypeScript support
121
+ - Cross-file detection with SQLite storage
122
+ - Detects membership validation, equality chains, function call patterns
123
+ - False positive filtering (200+ exclusion patterns)
124
+ - Inline ignore directive support
119
125
  - **Pluggable Architecture** - Easy to extend with custom linters
120
126
  - **Multi-Language Support** - Python, TypeScript, JavaScript, and more
121
127
  - **Flexible Configuration** - YAML/JSON configs with pattern matching
@@ -2,8 +2,8 @@
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-884%2F884%20passing-brightgreen.svg)](tests/)
6
- [![Coverage](https://img.shields.io/badge/coverage-88%25-brightgreen.svg)](htmlcov/)
5
+ [![Tests](https://img.shields.io/badge/tests-1076%2F1076%20passing-brightgreen.svg)](tests/)
6
+ [![Coverage](https://img.shields.io/badge/coverage-89%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)
9
9
 
@@ -81,6 +81,12 @@ thailint complements your existing linting stack by catching the patterns AI too
81
81
  - No instance state (self.attr) detection
82
82
  - Excludes ABC, Protocol, and decorated classes
83
83
  - Helpful refactoring suggestions
84
+ - **Stringly-Typed Linting** - Detect string patterns that should use enums
85
+ - Python and TypeScript support
86
+ - Cross-file detection with SQLite storage
87
+ - Detects membership validation, equality chains, function call patterns
88
+ - False positive filtering (200+ exclusion patterns)
89
+ - Inline ignore directive support
84
90
  - **Pluggable Architecture** - Easy to extend with custom linters
85
91
  - **Multi-Language Support** - Python, TypeScript, JavaScript, and more
86
92
  - **Flexible Configuration** - YAML/JSON configs with pattern matching
@@ -17,7 +17,7 @@ build-backend = "poetry.core.masonry.api"
17
17
 
18
18
  [tool.poetry]
19
19
  name = "thailint"
20
- version = "0.11.0"
20
+ version = "0.12.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"
@@ -1,18 +1,18 @@
1
1
  """
2
- Purpose: CLI commands for code smell linters (dry, magic-numbers)
2
+ Purpose: CLI commands for code smell linters (dry, magic-numbers, stringly-typed)
3
3
 
4
- Scope: Commands that detect code smells like duplicate code and magic numbers
4
+ Scope: Commands that detect code smells like duplicate code, magic numbers, and stringly-typed patterns
5
5
 
6
6
  Overview: Provides CLI commands for code smell detection: dry finds duplicate code blocks using
7
- token-based hashing with SQLite caching, and magic-numbers detects unnamed numeric literals that
8
- should be extracted as named constants. Each command supports standard options (config, format,
9
- recursive) plus linter-specific options (min-lines, no-cache, clear-cache) and integrates with
10
- the orchestrator for execution.
7
+ token-based hashing with SQLite caching, magic-numbers detects unnamed numeric literals that
8
+ should be extracted as named constants, and stringly-typed detects string patterns that should
9
+ use enums. Each command supports standard options (config, format, recursive) plus linter-specific
10
+ options and integrates with the orchestrator for execution.
11
11
 
12
12
  Dependencies: click for CLI framework, src.cli.main for CLI group, src.cli.utils for shared utilities,
13
13
  src.cli.linters.shared for linter-specific helpers, yaml for config loading
14
14
 
15
- Exports: dry command, magic_numbers command
15
+ Exports: dry command, magic_numbers command, stringly_typed command
16
16
 
17
17
  Interfaces: Click CLI commands registered to main CLI group
18
18
 
@@ -341,3 +341,110 @@ def _execute_magic_numbers_lint( # pylint: disable=too-many-arguments,too-many-
341
341
 
342
342
  format_violations(magic_numbers_violations, format)
343
343
  sys.exit(1 if magic_numbers_violations else 0)
344
+
345
+
346
+ # =============================================================================
347
+ # Stringly-Typed Command
348
+ # =============================================================================
349
+
350
+
351
+ def _setup_stringly_typed_orchestrator(
352
+ path_objs: list[Path], config_file: str | None, verbose: bool, project_root: Path | None = None
353
+ ) -> "Orchestrator":
354
+ """Set up orchestrator for stringly-typed command."""
355
+ return setup_base_orchestrator(path_objs, config_file, verbose, project_root)
356
+
357
+
358
+ def _run_stringly_typed_lint(
359
+ orchestrator: "Orchestrator", path_objs: list[Path], recursive: bool
360
+ ) -> list[Violation]:
361
+ """Execute stringly-typed lint on files or directories."""
362
+ all_violations = execute_linting_on_paths(orchestrator, path_objs, recursive)
363
+ return [v for v in all_violations if "stringly-typed" in v.rule_id]
364
+
365
+
366
+ @cli.command("stringly-typed")
367
+ @click.argument("paths", nargs=-1, type=click.Path())
368
+ @click.option("--config", "-c", "config_file", type=click.Path(), help="Path to config file")
369
+ @format_option
370
+ @click.option("--recursive/--no-recursive", default=True, help="Scan directories recursively")
371
+ @click.pass_context
372
+ def stringly_typed( # pylint: disable=too-many-arguments,too-many-positional-arguments
373
+ ctx: click.Context,
374
+ paths: tuple[str, ...],
375
+ config_file: str | None,
376
+ format: str,
377
+ recursive: bool,
378
+ ) -> None:
379
+ """Check for stringly-typed patterns in code.
380
+
381
+ Detects string patterns in Python and TypeScript/JavaScript code that should
382
+ use enums or typed alternatives. Finds membership validation, equality chains,
383
+ and function calls with limited string values across multiple files.
384
+
385
+ PATHS: Files or directories to lint (defaults to current directory if none provided)
386
+
387
+ Examples:
388
+
389
+ \b
390
+ # Check current directory (all files recursively)
391
+ thai-lint stringly-typed
392
+
393
+ \b
394
+ # Check specific directory
395
+ thai-lint stringly-typed src/
396
+
397
+ \b
398
+ # Check single file
399
+ thai-lint stringly-typed src/handlers.py
400
+
401
+ \b
402
+ # Check multiple files
403
+ thai-lint stringly-typed src/handlers.py src/services.py
404
+
405
+ \b
406
+ # Get JSON output
407
+ thai-lint stringly-typed --format json .
408
+
409
+ \b
410
+ # Get SARIF output for IDE integration
411
+ thai-lint stringly-typed --format sarif .
412
+
413
+ \b
414
+ # Use custom config file
415
+ thai-lint stringly-typed --config .thailint.yaml src/
416
+ """
417
+ verbose: bool = ctx.obj.get("verbose", False)
418
+ project_root = get_project_root_from_context(ctx)
419
+
420
+ if not paths:
421
+ paths = (".",)
422
+
423
+ path_objs = [Path(p) for p in paths]
424
+
425
+ try:
426
+ _execute_stringly_typed_lint(
427
+ path_objs, config_file, format, recursive, verbose, project_root
428
+ )
429
+ except Exception as e:
430
+ handle_linting_error(e, verbose)
431
+
432
+
433
+ def _execute_stringly_typed_lint( # pylint: disable=too-many-arguments,too-many-positional-arguments
434
+ path_objs: list[Path],
435
+ config_file: str | None,
436
+ format: str,
437
+ recursive: bool,
438
+ verbose: bool,
439
+ project_root: Path | None = None,
440
+ ) -> NoReturn:
441
+ """Execute stringly-typed lint."""
442
+ validate_paths_exist(path_objs)
443
+ orchestrator = _setup_stringly_typed_orchestrator(path_objs, config_file, verbose, project_root)
444
+ stringly_violations = _run_stringly_typed_lint(orchestrator, path_objs, recursive)
445
+
446
+ if verbose:
447
+ logger.info(f"Found {len(stringly_violations)} stringly-typed violation(s)")
448
+
449
+ format_violations(stringly_violations, format)
450
+ sys.exit(1 if stringly_violations else 0)
@@ -172,34 +172,54 @@ def _autodetect_project_root(
172
172
  return auto_root
173
173
 
174
174
 
175
- def get_project_root_from_context(ctx: click.Context) -> Path:
175
+ def get_project_root_from_context(ctx: click.Context) -> Path | None:
176
176
  """Get or determine project root from Click context.
177
177
 
178
178
  This function defers the actual determination until needed to avoid
179
179
  importing pyprojroot in test environments where it may not be available.
180
180
 
181
+ Returns None when no explicit root is specified (via --project-root or --config),
182
+ allowing the orchestrator to auto-detect from target paths instead of CWD.
183
+
181
184
  Args:
182
185
  ctx: Click context containing CLI options
183
186
 
184
187
  Returns:
185
- Path to determined project root
188
+ Path to determined project root, or None for auto-detection from target paths
186
189
  """
187
190
  # Check if already determined and cached
188
191
  if "project_root" in ctx.obj:
189
- cached_root: Path = ctx.obj["project_root"]
190
- return cached_root
192
+ cached: Path | None = ctx.obj["project_root"]
193
+ return cached
194
+
195
+ project_root = _determine_project_root_for_context(ctx)
196
+ ctx.obj["project_root"] = project_root
197
+ return project_root
198
+
199
+
200
+ def _determine_project_root_for_context(ctx: click.Context) -> Path | None:
201
+ """Determine project root from context options.
202
+
203
+ Args:
204
+ ctx: Click context containing CLI options
191
205
 
192
- # Determine project root using stored CLI options
206
+ Returns:
207
+ Path if explicit root or config specified, None for auto-detection
208
+ """
193
209
  explicit_root = ctx.obj.get("cli_project_root")
194
210
  config_path = ctx.obj.get("cli_config_path")
195
211
  verbose = ctx.obj.get("verbose", False)
196
212
 
197
- project_root = _determine_project_root(explicit_root, config_path, verbose)
213
+ if explicit_root:
214
+ return _resolve_explicit_project_root(explicit_root, verbose)
198
215
 
199
- # Cache for future use
200
- ctx.obj["project_root"] = project_root
216
+ if config_path:
217
+ return _infer_root_from_config(config_path, verbose)
201
218
 
202
- return project_root
219
+ # No explicit root - return None for auto-detection from target paths
220
+ if verbose:
221
+ logger.debug("No explicit project root, will auto-detect from target paths")
222
+ return None
203
223
 
204
224
 
205
225
  # =============================================================================
@@ -0,0 +1,36 @@
1
+ """
2
+ Purpose: Stringly-typed linter package exports
3
+
4
+ Scope: Public API for stringly-typed linter module
5
+
6
+ Overview: Provides the public interface for the stringly-typed linter package. Exports
7
+ StringlyTypedConfig for configuration and StringlyTypedRule for linting. The stringly-typed
8
+ linter detects code patterns where plain strings are used instead of proper enums or typed
9
+ alternatives, helping identify potential type safety improvements. Supports cross-file
10
+ detection to find repeated string patterns across the codebase. Includes IgnoreChecker
11
+ for inline ignore directive support.
12
+
13
+ Dependencies: .config for StringlyTypedConfig, .linter for StringlyTypedRule,
14
+ .storage for StringlyTypedStorage, .ignore_checker for IgnoreChecker
15
+
16
+ Exports: StringlyTypedConfig, StringlyTypedRule, StringlyTypedStorage, StoredPattern,
17
+ IgnoreChecker
18
+
19
+ Interfaces: Configuration loading via StringlyTypedConfig.from_dict(),
20
+ StringlyTypedRule.check() and finalize() for linting, IgnoreChecker.filter_violations()
21
+
22
+ Implementation: Module-level exports with __all__ definition
23
+ """
24
+
25
+ from src.linters.stringly_typed.config import StringlyTypedConfig
26
+ from src.linters.stringly_typed.ignore_checker import IgnoreChecker
27
+ from src.linters.stringly_typed.linter import StringlyTypedRule
28
+ from src.linters.stringly_typed.storage import StoredPattern, StringlyTypedStorage
29
+
30
+ __all__ = [
31
+ "StringlyTypedConfig",
32
+ "IgnoreChecker",
33
+ "StringlyTypedRule",
34
+ "StringlyTypedStorage",
35
+ "StoredPattern",
36
+ ]
@@ -33,6 +33,23 @@ DEFAULT_MIN_OCCURRENCES = 2
33
33
  DEFAULT_MIN_VALUES_FOR_ENUM = 2
34
34
  DEFAULT_MAX_VALUES_FOR_ENUM = 6
35
35
 
36
+ # Default ignore patterns - test directories are excluded by default
37
+ # because test fixtures commonly use string literals for mocking
38
+ DEFAULT_IGNORE_PATTERNS: list[str] = [
39
+ "**/tests/**",
40
+ "**/test/**",
41
+ "**/*_test.py",
42
+ "**/*_test.ts",
43
+ "**/*.test.ts",
44
+ "**/*.test.tsx",
45
+ "**/*.spec.ts",
46
+ "**/*.spec.tsx",
47
+ "**/*.stories.ts",
48
+ "**/*.stories.tsx",
49
+ "**/conftest.py",
50
+ "**/fixtures/**",
51
+ ]
52
+
36
53
 
37
54
  @dataclass
38
55
  class StringlyTypedConfig: # pylint: disable=too-many-instance-attributes
@@ -62,7 +79,7 @@ class StringlyTypedConfig: # pylint: disable=too-many-instance-attributes
62
79
  """Whether to require cross-file occurrences to flag violations."""
63
80
 
64
81
  ignore: list[str] = field(default_factory=list)
65
- """File patterns to ignore."""
82
+ """File patterns to ignore. Defaults merged with test directories in from_dict."""
66
83
 
67
84
  allowed_string_sets: list[list[str]] = field(default_factory=list)
68
85
  """String sets that are allowed and should not be flagged."""
@@ -114,13 +131,17 @@ class StringlyTypedConfig: # pylint: disable=too-many-instance-attributes
114
131
  Returns:
115
132
  StringlyTypedConfig instance
116
133
  """
134
+ # Merge user ignore patterns with defaults
135
+ user_ignore = config.get("ignore", [])
136
+ merged_ignore = DEFAULT_IGNORE_PATTERNS.copy() + user_ignore
137
+
117
138
  return cls(
118
139
  enabled=config.get("enabled", True),
119
140
  min_occurrences=config.get("min_occurrences", DEFAULT_MIN_OCCURRENCES),
120
141
  min_values_for_enum=config.get("min_values_for_enum", DEFAULT_MIN_VALUES_FOR_ENUM),
121
142
  max_values_for_enum=config.get("max_values_for_enum", DEFAULT_MAX_VALUES_FOR_ENUM),
122
143
  require_cross_file=config.get("require_cross_file", True),
123
- ignore=config.get("ignore", []),
144
+ ignore=merged_ignore,
124
145
  allowed_string_sets=config.get("allowed_string_sets", []),
125
146
  exclude_variables=config.get("exclude_variables", []),
126
147
  )
@@ -138,6 +159,10 @@ class StringlyTypedConfig: # pylint: disable=too-many-instance-attributes
138
159
  Returns:
139
160
  StringlyTypedConfig instance with merged values
140
161
  """
162
+ # Merge user ignore patterns with defaults
163
+ user_ignore = lang_config.get("ignore", base_config.get("ignore", []))
164
+ merged_ignore = DEFAULT_IGNORE_PATTERNS.copy() + user_ignore
165
+
141
166
  return cls(
142
167
  enabled=lang_config.get("enabled", base_config.get("enabled", True)),
143
168
  min_occurrences=lang_config.get(
@@ -155,7 +180,7 @@ class StringlyTypedConfig: # pylint: disable=too-many-instance-attributes
155
180
  require_cross_file=lang_config.get(
156
181
  "require_cross_file", base_config.get("require_cross_file", True)
157
182
  ),
158
- ignore=lang_config.get("ignore", base_config.get("ignore", [])),
183
+ ignore=merged_ignore,
159
184
  allowed_string_sets=lang_config.get(
160
185
  "allowed_string_sets", base_config.get("allowed_string_sets", [])
161
186
  ),