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.
- {thailint-0.11.0 → thailint-0.12.0}/PKG-INFO +9 -3
- {thailint-0.11.0 → thailint-0.12.0}/README.md +8 -2
- {thailint-0.11.0 → thailint-0.12.0}/pyproject.toml +1 -1
- {thailint-0.11.0 → thailint-0.12.0}/src/cli/linters/code_smells.py +114 -7
- {thailint-0.11.0 → thailint-0.12.0}/src/cli/utils.py +29 -9
- thailint-0.12.0/src/linters/stringly_typed/__init__.py +36 -0
- {thailint-0.11.0 → thailint-0.12.0}/src/linters/stringly_typed/config.py +28 -3
- thailint-0.12.0/src/linters/stringly_typed/context_filter.py +451 -0
- thailint-0.12.0/src/linters/stringly_typed/function_call_violation_builder.py +137 -0
- thailint-0.12.0/src/linters/stringly_typed/ignore_checker.py +102 -0
- thailint-0.12.0/src/linters/stringly_typed/ignore_utils.py +51 -0
- thailint-0.12.0/src/linters/stringly_typed/linter.py +344 -0
- {thailint-0.11.0 → thailint-0.12.0}/src/linters/stringly_typed/python/__init__.py +9 -5
- {thailint-0.11.0 → thailint-0.12.0}/src/linters/stringly_typed/python/analyzer.py +155 -9
- thailint-0.12.0/src/linters/stringly_typed/python/call_tracker.py +172 -0
- thailint-0.12.0/src/linters/stringly_typed/python/comparison_tracker.py +252 -0
- thailint-0.12.0/src/linters/stringly_typed/storage.py +630 -0
- thailint-0.12.0/src/linters/stringly_typed/storage_initializer.py +45 -0
- thailint-0.12.0/src/linters/stringly_typed/typescript/__init__.py +28 -0
- thailint-0.12.0/src/linters/stringly_typed/typescript/analyzer.py +157 -0
- thailint-0.12.0/src/linters/stringly_typed/typescript/call_tracker.py +329 -0
- thailint-0.12.0/src/linters/stringly_typed/typescript/comparison_tracker.py +372 -0
- thailint-0.12.0/src/linters/stringly_typed/violation_generator.py +376 -0
- thailint-0.11.0/src/linters/stringly_typed/__init__.py +0 -23
- {thailint-0.11.0 → thailint-0.12.0}/CHANGELOG.md +0 -0
- {thailint-0.11.0 → thailint-0.12.0}/LICENSE +0 -0
- {thailint-0.11.0 → thailint-0.12.0}/src/__init__.py +0 -0
- {thailint-0.11.0 → thailint-0.12.0}/src/analyzers/__init__.py +0 -0
- {thailint-0.11.0 → thailint-0.12.0}/src/analyzers/typescript_base.py +0 -0
- {thailint-0.11.0 → thailint-0.12.0}/src/api.py +0 -0
- {thailint-0.11.0 → thailint-0.12.0}/src/cli/__init__.py +0 -0
- {thailint-0.11.0 → thailint-0.12.0}/src/cli/__main__.py +0 -0
- {thailint-0.11.0 → thailint-0.12.0}/src/cli/config.py +0 -0
- {thailint-0.11.0 → thailint-0.12.0}/src/cli/linters/__init__.py +0 -0
- {thailint-0.11.0 → thailint-0.12.0}/src/cli/linters/code_patterns.py +0 -0
- {thailint-0.11.0 → thailint-0.12.0}/src/cli/linters/documentation.py +0 -0
- {thailint-0.11.0 → thailint-0.12.0}/src/cli/linters/shared.py +0 -0
- {thailint-0.11.0 → thailint-0.12.0}/src/cli/linters/structure.py +0 -0
- {thailint-0.11.0 → thailint-0.12.0}/src/cli/linters/structure_quality.py +0 -0
- {thailint-0.11.0 → thailint-0.12.0}/src/cli/main.py +0 -0
- {thailint-0.11.0 → thailint-0.12.0}/src/cli_main.py +0 -0
- {thailint-0.11.0 → thailint-0.12.0}/src/config.py +0 -0
- {thailint-0.11.0 → thailint-0.12.0}/src/core/__init__.py +0 -0
- {thailint-0.11.0 → thailint-0.12.0}/src/core/base.py +0 -0
- {thailint-0.11.0 → thailint-0.12.0}/src/core/cli_utils.py +0 -0
- {thailint-0.11.0 → thailint-0.12.0}/src/core/config_parser.py +0 -0
- {thailint-0.11.0 → thailint-0.12.0}/src/core/linter_utils.py +0 -0
- {thailint-0.11.0 → thailint-0.12.0}/src/core/registry.py +0 -0
- {thailint-0.11.0 → thailint-0.12.0}/src/core/rule_discovery.py +0 -0
- {thailint-0.11.0 → thailint-0.12.0}/src/core/types.py +0 -0
- {thailint-0.11.0 → thailint-0.12.0}/src/core/violation_builder.py +0 -0
- {thailint-0.11.0 → thailint-0.12.0}/src/core/violation_utils.py +0 -0
- {thailint-0.11.0 → thailint-0.12.0}/src/formatters/__init__.py +0 -0
- {thailint-0.11.0 → thailint-0.12.0}/src/formatters/sarif.py +0 -0
- {thailint-0.11.0 → thailint-0.12.0}/src/linter_config/__init__.py +0 -0
- {thailint-0.11.0 → thailint-0.12.0}/src/linter_config/ignore.py +0 -0
- {thailint-0.11.0 → thailint-0.12.0}/src/linter_config/loader.py +0 -0
- {thailint-0.11.0 → thailint-0.12.0}/src/linters/__init__.py +0 -0
- {thailint-0.11.0 → thailint-0.12.0}/src/linters/collection_pipeline/__init__.py +0 -0
- {thailint-0.11.0 → thailint-0.12.0}/src/linters/collection_pipeline/config.py +0 -0
- {thailint-0.11.0 → thailint-0.12.0}/src/linters/collection_pipeline/continue_analyzer.py +0 -0
- {thailint-0.11.0 → thailint-0.12.0}/src/linters/collection_pipeline/detector.py +0 -0
- {thailint-0.11.0 → thailint-0.12.0}/src/linters/collection_pipeline/linter.py +0 -0
- {thailint-0.11.0 → thailint-0.12.0}/src/linters/collection_pipeline/suggestion_builder.py +0 -0
- {thailint-0.11.0 → thailint-0.12.0}/src/linters/dry/__init__.py +0 -0
- {thailint-0.11.0 → thailint-0.12.0}/src/linters/dry/base_token_analyzer.py +0 -0
- {thailint-0.11.0 → thailint-0.12.0}/src/linters/dry/block_filter.py +0 -0
- {thailint-0.11.0 → thailint-0.12.0}/src/linters/dry/block_grouper.py +0 -0
- {thailint-0.11.0 → thailint-0.12.0}/src/linters/dry/cache.py +0 -0
- {thailint-0.11.0 → thailint-0.12.0}/src/linters/dry/cache_query.py +0 -0
- {thailint-0.11.0 → thailint-0.12.0}/src/linters/dry/config.py +0 -0
- {thailint-0.11.0 → thailint-0.12.0}/src/linters/dry/config_loader.py +0 -0
- {thailint-0.11.0 → thailint-0.12.0}/src/linters/dry/constant.py +0 -0
- {thailint-0.11.0 → thailint-0.12.0}/src/linters/dry/constant_matcher.py +0 -0
- {thailint-0.11.0 → thailint-0.12.0}/src/linters/dry/constant_violation_builder.py +0 -0
- {thailint-0.11.0 → thailint-0.12.0}/src/linters/dry/deduplicator.py +0 -0
- {thailint-0.11.0 → thailint-0.12.0}/src/linters/dry/duplicate_storage.py +0 -0
- {thailint-0.11.0 → thailint-0.12.0}/src/linters/dry/file_analyzer.py +0 -0
- {thailint-0.11.0 → thailint-0.12.0}/src/linters/dry/inline_ignore.py +0 -0
- {thailint-0.11.0 → thailint-0.12.0}/src/linters/dry/linter.py +0 -0
- {thailint-0.11.0 → thailint-0.12.0}/src/linters/dry/python_analyzer.py +0 -0
- {thailint-0.11.0 → thailint-0.12.0}/src/linters/dry/python_constant_extractor.py +0 -0
- {thailint-0.11.0 → thailint-0.12.0}/src/linters/dry/single_statement_detector.py +0 -0
- {thailint-0.11.0 → thailint-0.12.0}/src/linters/dry/storage_initializer.py +0 -0
- {thailint-0.11.0 → thailint-0.12.0}/src/linters/dry/token_hasher.py +0 -0
- {thailint-0.11.0 → thailint-0.12.0}/src/linters/dry/typescript_analyzer.py +0 -0
- {thailint-0.11.0 → thailint-0.12.0}/src/linters/dry/typescript_constant_extractor.py +0 -0
- {thailint-0.11.0 → thailint-0.12.0}/src/linters/dry/typescript_statement_detector.py +0 -0
- {thailint-0.11.0 → thailint-0.12.0}/src/linters/dry/typescript_value_extractor.py +0 -0
- {thailint-0.11.0 → thailint-0.12.0}/src/linters/dry/violation_builder.py +0 -0
- {thailint-0.11.0 → thailint-0.12.0}/src/linters/dry/violation_filter.py +0 -0
- {thailint-0.11.0 → thailint-0.12.0}/src/linters/dry/violation_generator.py +0 -0
- {thailint-0.11.0 → thailint-0.12.0}/src/linters/file_header/__init__.py +0 -0
- {thailint-0.11.0 → thailint-0.12.0}/src/linters/file_header/atemporal_detector.py +0 -0
- {thailint-0.11.0 → thailint-0.12.0}/src/linters/file_header/base_parser.py +0 -0
- {thailint-0.11.0 → thailint-0.12.0}/src/linters/file_header/bash_parser.py +0 -0
- {thailint-0.11.0 → thailint-0.12.0}/src/linters/file_header/config.py +0 -0
- {thailint-0.11.0 → thailint-0.12.0}/src/linters/file_header/css_parser.py +0 -0
- {thailint-0.11.0 → thailint-0.12.0}/src/linters/file_header/field_validator.py +0 -0
- {thailint-0.11.0 → thailint-0.12.0}/src/linters/file_header/linter.py +0 -0
- {thailint-0.11.0 → thailint-0.12.0}/src/linters/file_header/markdown_parser.py +0 -0
- {thailint-0.11.0 → thailint-0.12.0}/src/linters/file_header/python_parser.py +0 -0
- {thailint-0.11.0 → thailint-0.12.0}/src/linters/file_header/typescript_parser.py +0 -0
- {thailint-0.11.0 → thailint-0.12.0}/src/linters/file_header/violation_builder.py +0 -0
- {thailint-0.11.0 → thailint-0.12.0}/src/linters/file_placement/__init__.py +0 -0
- {thailint-0.11.0 → thailint-0.12.0}/src/linters/file_placement/config_loader.py +0 -0
- {thailint-0.11.0 → thailint-0.12.0}/src/linters/file_placement/directory_matcher.py +0 -0
- {thailint-0.11.0 → thailint-0.12.0}/src/linters/file_placement/linter.py +0 -0
- {thailint-0.11.0 → thailint-0.12.0}/src/linters/file_placement/path_resolver.py +0 -0
- {thailint-0.11.0 → thailint-0.12.0}/src/linters/file_placement/pattern_matcher.py +0 -0
- {thailint-0.11.0 → thailint-0.12.0}/src/linters/file_placement/pattern_validator.py +0 -0
- {thailint-0.11.0 → thailint-0.12.0}/src/linters/file_placement/rule_checker.py +0 -0
- {thailint-0.11.0 → thailint-0.12.0}/src/linters/file_placement/violation_factory.py +0 -0
- {thailint-0.11.0 → thailint-0.12.0}/src/linters/magic_numbers/__init__.py +0 -0
- {thailint-0.11.0 → thailint-0.12.0}/src/linters/magic_numbers/config.py +0 -0
- {thailint-0.11.0 → thailint-0.12.0}/src/linters/magic_numbers/context_analyzer.py +0 -0
- {thailint-0.11.0 → thailint-0.12.0}/src/linters/magic_numbers/linter.py +0 -0
- {thailint-0.11.0 → thailint-0.12.0}/src/linters/magic_numbers/python_analyzer.py +0 -0
- {thailint-0.11.0 → thailint-0.12.0}/src/linters/magic_numbers/typescript_analyzer.py +0 -0
- {thailint-0.11.0 → thailint-0.12.0}/src/linters/magic_numbers/typescript_ignore_checker.py +0 -0
- {thailint-0.11.0 → thailint-0.12.0}/src/linters/magic_numbers/violation_builder.py +0 -0
- {thailint-0.11.0 → thailint-0.12.0}/src/linters/method_property/__init__.py +0 -0
- {thailint-0.11.0 → thailint-0.12.0}/src/linters/method_property/config.py +0 -0
- {thailint-0.11.0 → thailint-0.12.0}/src/linters/method_property/linter.py +0 -0
- {thailint-0.11.0 → thailint-0.12.0}/src/linters/method_property/python_analyzer.py +0 -0
- {thailint-0.11.0 → thailint-0.12.0}/src/linters/method_property/violation_builder.py +0 -0
- {thailint-0.11.0 → thailint-0.12.0}/src/linters/nesting/__init__.py +0 -0
- {thailint-0.11.0 → thailint-0.12.0}/src/linters/nesting/config.py +0 -0
- {thailint-0.11.0 → thailint-0.12.0}/src/linters/nesting/linter.py +0 -0
- {thailint-0.11.0 → thailint-0.12.0}/src/linters/nesting/python_analyzer.py +0 -0
- {thailint-0.11.0 → thailint-0.12.0}/src/linters/nesting/typescript_analyzer.py +0 -0
- {thailint-0.11.0 → thailint-0.12.0}/src/linters/nesting/typescript_function_extractor.py +0 -0
- {thailint-0.11.0 → thailint-0.12.0}/src/linters/nesting/violation_builder.py +0 -0
- {thailint-0.11.0 → thailint-0.12.0}/src/linters/print_statements/__init__.py +0 -0
- {thailint-0.11.0 → thailint-0.12.0}/src/linters/print_statements/config.py +0 -0
- {thailint-0.11.0 → thailint-0.12.0}/src/linters/print_statements/linter.py +0 -0
- {thailint-0.11.0 → thailint-0.12.0}/src/linters/print_statements/python_analyzer.py +0 -0
- {thailint-0.11.0 → thailint-0.12.0}/src/linters/print_statements/typescript_analyzer.py +0 -0
- {thailint-0.11.0 → thailint-0.12.0}/src/linters/print_statements/violation_builder.py +0 -0
- {thailint-0.11.0 → thailint-0.12.0}/src/linters/srp/__init__.py +0 -0
- {thailint-0.11.0 → thailint-0.12.0}/src/linters/srp/class_analyzer.py +0 -0
- {thailint-0.11.0 → thailint-0.12.0}/src/linters/srp/config.py +0 -0
- {thailint-0.11.0 → thailint-0.12.0}/src/linters/srp/heuristics.py +0 -0
- {thailint-0.11.0 → thailint-0.12.0}/src/linters/srp/linter.py +0 -0
- {thailint-0.11.0 → thailint-0.12.0}/src/linters/srp/metrics_evaluator.py +0 -0
- {thailint-0.11.0 → thailint-0.12.0}/src/linters/srp/python_analyzer.py +0 -0
- {thailint-0.11.0 → thailint-0.12.0}/src/linters/srp/typescript_analyzer.py +0 -0
- {thailint-0.11.0 → thailint-0.12.0}/src/linters/srp/typescript_metrics_calculator.py +0 -0
- {thailint-0.11.0 → thailint-0.12.0}/src/linters/srp/violation_builder.py +0 -0
- {thailint-0.11.0 → thailint-0.12.0}/src/linters/stateless_class/__init__.py +0 -0
- {thailint-0.11.0 → thailint-0.12.0}/src/linters/stateless_class/config.py +0 -0
- {thailint-0.11.0 → thailint-0.12.0}/src/linters/stateless_class/linter.py +0 -0
- {thailint-0.11.0 → thailint-0.12.0}/src/linters/stateless_class/python_analyzer.py +0 -0
- {thailint-0.11.0 → thailint-0.12.0}/src/linters/stringly_typed/python/condition_extractor.py +0 -0
- {thailint-0.11.0 → thailint-0.12.0}/src/linters/stringly_typed/python/conditional_detector.py +0 -0
- {thailint-0.11.0 → thailint-0.12.0}/src/linters/stringly_typed/python/constants.py +0 -0
- {thailint-0.11.0 → thailint-0.12.0}/src/linters/stringly_typed/python/match_analyzer.py +0 -0
- {thailint-0.11.0 → thailint-0.12.0}/src/linters/stringly_typed/python/validation_detector.py +0 -0
- {thailint-0.11.0 → thailint-0.12.0}/src/linters/stringly_typed/python/variable_extractor.py +0 -0
- {thailint-0.11.0 → thailint-0.12.0}/src/orchestrator/__init__.py +0 -0
- {thailint-0.11.0 → thailint-0.12.0}/src/orchestrator/core.py +0 -0
- {thailint-0.11.0 → thailint-0.12.0}/src/orchestrator/language_detector.py +0 -0
- {thailint-0.11.0 → thailint-0.12.0}/src/templates/thailint_config_template.yaml +0 -0
- {thailint-0.11.0 → thailint-0.12.0}/src/utils/__init__.py +0 -0
- {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.
|
|
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
|
[](https://opensource.org/licenses/MIT)
|
|
39
39
|
[](https://www.python.org/downloads/)
|
|
40
|
-
[](tests/)
|
|
41
|
+
[](htmlcov/)
|
|
42
42
|
[](https://thai-lint.readthedocs.io/en/latest/?badge=latest)
|
|
43
43
|
[](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
|
[](https://opensource.org/licenses/MIT)
|
|
4
4
|
[](https://www.python.org/downloads/)
|
|
5
|
-
[](tests/)
|
|
6
|
+
[](htmlcov/)
|
|
7
7
|
[](https://thai-lint.readthedocs.io/en/latest/?badge=latest)
|
|
8
8
|
[](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.
|
|
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
|
|
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,
|
|
8
|
-
should be extracted as named constants
|
|
9
|
-
|
|
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
|
-
|
|
190
|
-
return
|
|
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
|
-
|
|
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
|
-
|
|
213
|
+
if explicit_root:
|
|
214
|
+
return _resolve_explicit_project_root(explicit_root, verbose)
|
|
198
215
|
|
|
199
|
-
|
|
200
|
-
|
|
216
|
+
if config_path:
|
|
217
|
+
return _infer_root_from_config(config_path, verbose)
|
|
201
218
|
|
|
202
|
-
return
|
|
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=
|
|
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=
|
|
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
|
),
|