thailint 0.5.0__py3-none-any.whl → 0.15.3__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (204) hide show
  1. src/__init__.py +1 -0
  2. src/analyzers/__init__.py +4 -3
  3. src/analyzers/ast_utils.py +54 -0
  4. src/analyzers/rust_base.py +155 -0
  5. src/analyzers/rust_context.py +141 -0
  6. src/analyzers/typescript_base.py +4 -0
  7. src/cli/__init__.py +30 -0
  8. src/cli/__main__.py +22 -0
  9. src/cli/config.py +480 -0
  10. src/cli/config_merge.py +241 -0
  11. src/cli/linters/__init__.py +67 -0
  12. src/cli/linters/code_patterns.py +270 -0
  13. src/cli/linters/code_smells.py +342 -0
  14. src/cli/linters/documentation.py +83 -0
  15. src/cli/linters/performance.py +287 -0
  16. src/cli/linters/shared.py +331 -0
  17. src/cli/linters/structure.py +327 -0
  18. src/cli/linters/structure_quality.py +328 -0
  19. src/cli/main.py +120 -0
  20. src/cli/utils.py +395 -0
  21. src/cli_main.py +37 -0
  22. src/config.py +38 -25
  23. src/core/base.py +7 -2
  24. src/core/cli_utils.py +19 -2
  25. src/core/config_parser.py +5 -2
  26. src/core/constants.py +54 -0
  27. src/core/linter_utils.py +95 -6
  28. src/core/python_lint_rule.py +101 -0
  29. src/core/registry.py +1 -1
  30. src/core/rule_discovery.py +147 -84
  31. src/core/types.py +13 -0
  32. src/core/violation_builder.py +78 -15
  33. src/core/violation_utils.py +69 -0
  34. src/formatters/__init__.py +22 -0
  35. src/formatters/sarif.py +202 -0
  36. src/linter_config/directive_markers.py +109 -0
  37. src/linter_config/ignore.py +254 -395
  38. src/linter_config/loader.py +45 -12
  39. src/linter_config/pattern_utils.py +65 -0
  40. src/linter_config/rule_matcher.py +89 -0
  41. src/linters/collection_pipeline/__init__.py +90 -0
  42. src/linters/collection_pipeline/any_all_analyzer.py +281 -0
  43. src/linters/collection_pipeline/ast_utils.py +40 -0
  44. src/linters/collection_pipeline/config.py +75 -0
  45. src/linters/collection_pipeline/continue_analyzer.py +94 -0
  46. src/linters/collection_pipeline/detector.py +360 -0
  47. src/linters/collection_pipeline/filter_map_analyzer.py +402 -0
  48. src/linters/collection_pipeline/linter.py +420 -0
  49. src/linters/collection_pipeline/suggestion_builder.py +130 -0
  50. src/linters/cqs/__init__.py +54 -0
  51. src/linters/cqs/config.py +55 -0
  52. src/linters/cqs/function_analyzer.py +201 -0
  53. src/linters/cqs/input_detector.py +139 -0
  54. src/linters/cqs/linter.py +159 -0
  55. src/linters/cqs/output_detector.py +84 -0
  56. src/linters/cqs/python_analyzer.py +54 -0
  57. src/linters/cqs/types.py +82 -0
  58. src/linters/cqs/typescript_cqs_analyzer.py +61 -0
  59. src/linters/cqs/typescript_function_analyzer.py +192 -0
  60. src/linters/cqs/typescript_input_detector.py +203 -0
  61. src/linters/cqs/typescript_output_detector.py +117 -0
  62. src/linters/cqs/violation_builder.py +94 -0
  63. src/linters/dry/base_token_analyzer.py +16 -9
  64. src/linters/dry/block_filter.py +120 -20
  65. src/linters/dry/block_grouper.py +4 -0
  66. src/linters/dry/cache.py +104 -10
  67. src/linters/dry/cache_query.py +4 -0
  68. src/linters/dry/config.py +54 -11
  69. src/linters/dry/constant.py +92 -0
  70. src/linters/dry/constant_matcher.py +223 -0
  71. src/linters/dry/constant_violation_builder.py +98 -0
  72. src/linters/dry/duplicate_storage.py +5 -4
  73. src/linters/dry/file_analyzer.py +4 -2
  74. src/linters/dry/inline_ignore.py +7 -16
  75. src/linters/dry/linter.py +183 -48
  76. src/linters/dry/python_analyzer.py +60 -439
  77. src/linters/dry/python_constant_extractor.py +100 -0
  78. src/linters/dry/single_statement_detector.py +417 -0
  79. src/linters/dry/token_hasher.py +116 -112
  80. src/linters/dry/typescript_analyzer.py +68 -382
  81. src/linters/dry/typescript_constant_extractor.py +138 -0
  82. src/linters/dry/typescript_statement_detector.py +255 -0
  83. src/linters/dry/typescript_value_extractor.py +70 -0
  84. src/linters/dry/violation_builder.py +4 -0
  85. src/linters/dry/violation_filter.py +5 -4
  86. src/linters/dry/violation_generator.py +71 -14
  87. src/linters/file_header/atemporal_detector.py +68 -50
  88. src/linters/file_header/base_parser.py +93 -0
  89. src/linters/file_header/bash_parser.py +66 -0
  90. src/linters/file_header/config.py +90 -16
  91. src/linters/file_header/css_parser.py +70 -0
  92. src/linters/file_header/field_validator.py +36 -33
  93. src/linters/file_header/linter.py +140 -144
  94. src/linters/file_header/markdown_parser.py +130 -0
  95. src/linters/file_header/python_parser.py +14 -58
  96. src/linters/file_header/typescript_parser.py +73 -0
  97. src/linters/file_header/violation_builder.py +13 -12
  98. src/linters/file_placement/config_loader.py +3 -1
  99. src/linters/file_placement/directory_matcher.py +4 -0
  100. src/linters/file_placement/linter.py +66 -34
  101. src/linters/file_placement/pattern_matcher.py +41 -6
  102. src/linters/file_placement/pattern_validator.py +31 -12
  103. src/linters/file_placement/rule_checker.py +12 -7
  104. src/linters/lazy_ignores/__init__.py +43 -0
  105. src/linters/lazy_ignores/config.py +74 -0
  106. src/linters/lazy_ignores/directive_utils.py +164 -0
  107. src/linters/lazy_ignores/header_parser.py +177 -0
  108. src/linters/lazy_ignores/linter.py +158 -0
  109. src/linters/lazy_ignores/matcher.py +168 -0
  110. src/linters/lazy_ignores/python_analyzer.py +209 -0
  111. src/linters/lazy_ignores/rule_id_utils.py +180 -0
  112. src/linters/lazy_ignores/skip_detector.py +298 -0
  113. src/linters/lazy_ignores/types.py +71 -0
  114. src/linters/lazy_ignores/typescript_analyzer.py +146 -0
  115. src/linters/lazy_ignores/violation_builder.py +135 -0
  116. src/linters/lbyl/__init__.py +31 -0
  117. src/linters/lbyl/config.py +63 -0
  118. src/linters/lbyl/linter.py +67 -0
  119. src/linters/lbyl/pattern_detectors/__init__.py +53 -0
  120. src/linters/lbyl/pattern_detectors/base.py +63 -0
  121. src/linters/lbyl/pattern_detectors/dict_key_detector.py +107 -0
  122. src/linters/lbyl/pattern_detectors/division_check_detector.py +232 -0
  123. src/linters/lbyl/pattern_detectors/file_exists_detector.py +220 -0
  124. src/linters/lbyl/pattern_detectors/hasattr_detector.py +119 -0
  125. src/linters/lbyl/pattern_detectors/isinstance_detector.py +119 -0
  126. src/linters/lbyl/pattern_detectors/len_check_detector.py +173 -0
  127. src/linters/lbyl/pattern_detectors/none_check_detector.py +146 -0
  128. src/linters/lbyl/pattern_detectors/string_validator_detector.py +145 -0
  129. src/linters/lbyl/python_analyzer.py +215 -0
  130. src/linters/lbyl/violation_builder.py +354 -0
  131. src/linters/magic_numbers/context_analyzer.py +227 -225
  132. src/linters/magic_numbers/linter.py +28 -82
  133. src/linters/magic_numbers/python_analyzer.py +4 -16
  134. src/linters/magic_numbers/typescript_analyzer.py +9 -12
  135. src/linters/magic_numbers/typescript_ignore_checker.py +81 -0
  136. src/linters/method_property/__init__.py +49 -0
  137. src/linters/method_property/config.py +138 -0
  138. src/linters/method_property/linter.py +414 -0
  139. src/linters/method_property/python_analyzer.py +473 -0
  140. src/linters/method_property/violation_builder.py +119 -0
  141. src/linters/nesting/linter.py +24 -16
  142. src/linters/nesting/python_analyzer.py +4 -0
  143. src/linters/nesting/typescript_analyzer.py +6 -12
  144. src/linters/nesting/violation_builder.py +1 -0
  145. src/linters/performance/__init__.py +91 -0
  146. src/linters/performance/config.py +43 -0
  147. src/linters/performance/constants.py +49 -0
  148. src/linters/performance/linter.py +149 -0
  149. src/linters/performance/python_analyzer.py +365 -0
  150. src/linters/performance/regex_analyzer.py +312 -0
  151. src/linters/performance/regex_linter.py +139 -0
  152. src/linters/performance/typescript_analyzer.py +236 -0
  153. src/linters/performance/violation_builder.py +160 -0
  154. src/linters/print_statements/config.py +7 -12
  155. src/linters/print_statements/linter.py +26 -43
  156. src/linters/print_statements/python_analyzer.py +91 -93
  157. src/linters/print_statements/typescript_analyzer.py +15 -25
  158. src/linters/print_statements/violation_builder.py +12 -14
  159. src/linters/srp/class_analyzer.py +11 -7
  160. src/linters/srp/heuristics.py +56 -22
  161. src/linters/srp/linter.py +15 -16
  162. src/linters/srp/python_analyzer.py +55 -20
  163. src/linters/srp/typescript_metrics_calculator.py +110 -50
  164. src/linters/stateless_class/__init__.py +25 -0
  165. src/linters/stateless_class/config.py +58 -0
  166. src/linters/stateless_class/linter.py +349 -0
  167. src/linters/stateless_class/python_analyzer.py +290 -0
  168. src/linters/stringly_typed/__init__.py +36 -0
  169. src/linters/stringly_typed/config.py +189 -0
  170. src/linters/stringly_typed/context_filter.py +451 -0
  171. src/linters/stringly_typed/function_call_violation_builder.py +135 -0
  172. src/linters/stringly_typed/ignore_checker.py +100 -0
  173. src/linters/stringly_typed/ignore_utils.py +51 -0
  174. src/linters/stringly_typed/linter.py +376 -0
  175. src/linters/stringly_typed/python/__init__.py +33 -0
  176. src/linters/stringly_typed/python/analyzer.py +348 -0
  177. src/linters/stringly_typed/python/call_tracker.py +175 -0
  178. src/linters/stringly_typed/python/comparison_tracker.py +257 -0
  179. src/linters/stringly_typed/python/condition_extractor.py +134 -0
  180. src/linters/stringly_typed/python/conditional_detector.py +179 -0
  181. src/linters/stringly_typed/python/constants.py +21 -0
  182. src/linters/stringly_typed/python/match_analyzer.py +94 -0
  183. src/linters/stringly_typed/python/validation_detector.py +189 -0
  184. src/linters/stringly_typed/python/variable_extractor.py +96 -0
  185. src/linters/stringly_typed/storage.py +620 -0
  186. src/linters/stringly_typed/storage_initializer.py +45 -0
  187. src/linters/stringly_typed/typescript/__init__.py +28 -0
  188. src/linters/stringly_typed/typescript/analyzer.py +157 -0
  189. src/linters/stringly_typed/typescript/call_tracker.py +335 -0
  190. src/linters/stringly_typed/typescript/comparison_tracker.py +378 -0
  191. src/linters/stringly_typed/violation_generator.py +419 -0
  192. src/orchestrator/core.py +252 -14
  193. src/orchestrator/language_detector.py +5 -3
  194. src/templates/thailint_config_template.yaml +196 -0
  195. src/utils/project_root.py +3 -0
  196. thailint-0.15.3.dist-info/METADATA +187 -0
  197. thailint-0.15.3.dist-info/RECORD +226 -0
  198. thailint-0.15.3.dist-info/entry_points.txt +4 -0
  199. src/cli.py +0 -1665
  200. thailint-0.5.0.dist-info/METADATA +0 -1286
  201. thailint-0.5.0.dist-info/RECORD +0 -96
  202. thailint-0.5.0.dist-info/entry_points.txt +0 -4
  203. {thailint-0.5.0.dist-info → thailint-0.15.3.dist-info}/WHEEL +0 -0
  204. {thailint-0.5.0.dist-info → thailint-0.15.3.dist-info}/licenses/LICENSE +0 -0
@@ -0,0 +1,135 @@
1
+ """
2
+ Purpose: Build violations for function call patterns with limited string values
3
+
4
+ Scope: Function call violation message and suggestion generation
5
+
6
+ Overview: Handles building violation objects for function calls that consistently receive
7
+ a limited set of string values, suggesting they should use enums. Generates messages
8
+ with cross-file references and actionable suggestions. Separated from main violation
9
+ generator to maintain SRP compliance with focused responsibility.
10
+
11
+ Dependencies: Violation, Severity, StoredFunctionCall, StringlyTypedConfig
12
+
13
+ Exports: build_function_call_violations function
14
+
15
+ Interfaces: build_function_call_violations(calls, unique_values) -> list[Violation]
16
+
17
+ Implementation: Builds violations with cross-file references and enum suggestions
18
+ """
19
+
20
+ from pathlib import Path
21
+
22
+ from src.core.types import Severity, Violation
23
+
24
+ from .storage import StoredFunctionCall
25
+
26
+
27
+ def build_function_call_violations(
28
+ calls: list[StoredFunctionCall], unique_values: set[str]
29
+ ) -> list[Violation]:
30
+ """Build violations for all calls to a function with limited values.
31
+
32
+ Args:
33
+ calls: All calls to the function/param
34
+ unique_values: Set of unique string values passed
35
+
36
+ Returns:
37
+ List of violations for each call site
38
+ """
39
+ return [_build_violation(call, calls, unique_values) for call in calls]
40
+
41
+
42
+ def _build_cross_references(call: StoredFunctionCall, all_calls: list[StoredFunctionCall]) -> str:
43
+ """Build cross-reference string for other function call locations.
44
+
45
+ Args:
46
+ call: Current call
47
+ all_calls: All calls with same function/param
48
+
49
+ Returns:
50
+ Comma-separated list of file:line references
51
+ """
52
+ refs = []
53
+ for other in all_calls:
54
+ if other.file_path != call.file_path or other.line_number != call.line_number:
55
+ refs.append(f"{Path(other.file_path).name}:{other.line_number}")
56
+
57
+ return ", ".join(refs[:5]) # Limit to 5 references
58
+
59
+
60
+ def _build_violation(
61
+ call: StoredFunctionCall,
62
+ all_calls: list[StoredFunctionCall],
63
+ unique_values: set[str],
64
+ ) -> Violation:
65
+ """Build a single violation for a function call.
66
+
67
+ Args:
68
+ call: The specific call to create violation for
69
+ all_calls: All calls to the same function/param
70
+ unique_values: Set of unique string values passed
71
+
72
+ Returns:
73
+ Violation instance
74
+ """
75
+ message = _build_message(call, all_calls, unique_values)
76
+ suggestion = _build_suggestion(call, unique_values)
77
+
78
+ return Violation(
79
+ rule_id="stringly-typed.limited-values",
80
+ file_path=str(call.file_path),
81
+ line=call.line_number,
82
+ column=call.column,
83
+ message=message,
84
+ severity=Severity.ERROR,
85
+ suggestion=suggestion,
86
+ )
87
+
88
+
89
+ def _build_message(
90
+ call: StoredFunctionCall,
91
+ all_calls: list[StoredFunctionCall],
92
+ unique_values: set[str],
93
+ ) -> str:
94
+ """Build violation message for function call pattern.
95
+
96
+ Args:
97
+ call: Current function call
98
+ all_calls: All calls to the same function/param
99
+ unique_values: Set of unique values passed
100
+
101
+ Returns:
102
+ Human-readable violation message
103
+ """
104
+ file_count = len({c.file_path for c in all_calls})
105
+ values_str = ", ".join(f"'{v}'" for v in sorted(unique_values))
106
+ param_desc = f"parameter {call.param_index}" if call.param_index > 0 else "first parameter"
107
+
108
+ message = (
109
+ f"Function '{call.function_name}' {param_desc} is called with "
110
+ f"only {len(unique_values)} unique string values [{values_str}] "
111
+ f"across {file_count} file(s)."
112
+ )
113
+
114
+ other_refs = _build_cross_references(call, all_calls)
115
+ if other_refs:
116
+ message += f" Also called in: {other_refs}."
117
+
118
+ return message
119
+
120
+
121
+ def _build_suggestion(call: StoredFunctionCall, unique_values: set[str]) -> str:
122
+ """Build fix suggestion for function call pattern.
123
+
124
+ Args:
125
+ call: The function call
126
+ unique_values: Set of unique values passed
127
+
128
+ Returns:
129
+ Human-readable suggestion
130
+ """
131
+ return (
132
+ f"Consider defining an enum or type union with the "
133
+ f"{len(unique_values)} possible values for '{call.function_name}' "
134
+ f"parameter {call.param_index}."
135
+ )
@@ -0,0 +1,100 @@
1
+ """
2
+ Purpose: Ignore directive checking for stringly-typed linter violations
3
+
4
+ Scope: Line-level, block-level, and file-level ignore directive support
5
+
6
+ Overview: Provides ignore directive checking functionality for the stringly-typed linter.
7
+ Wraps the centralized IgnoreDirectiveParser to filter violations based on inline comments
8
+ like `# thailint: ignore[stringly-typed]`. Supports line-level, block-level
9
+ (ignore-start/ignore-end), file-level (ignore-file), and next-line directives.
10
+ Handles both Python (# comment) and TypeScript (// comment) syntax.
11
+
12
+ Dependencies: IgnoreDirectiveParser from src.linter_config.ignore, Violation type, pathlib
13
+
14
+ Exports: IgnoreChecker class
15
+
16
+ Interfaces: IgnoreChecker.filter_violations(violations) -> list[Violation]
17
+
18
+ Implementation: Uses cached IgnoreDirectiveParser singleton, reads file content on demand,
19
+ supports both stringly-typed.* and stringly-typed specific rule matching
20
+ """
21
+
22
+ from contextlib import suppress
23
+ from pathlib import Path
24
+
25
+ from src.core.types import Violation
26
+ from src.linter_config.ignore import get_ignore_parser
27
+
28
+
29
+ class IgnoreChecker:
30
+ """Checks for ignore directives in stringly-typed linter violations.
31
+
32
+ Wraps the centralized IgnoreDirectiveParser to filter stringly-typed
33
+ violations based on inline ignore comments.
34
+ """
35
+
36
+ def __init__(self, project_root: Path | None = None) -> None:
37
+ """Initialize with project root for ignore parser.
38
+
39
+ Args:
40
+ project_root: Optional project root directory. Defaults to cwd.
41
+ """
42
+ self._ignore_parser = get_ignore_parser(project_root)
43
+ self._file_content_cache: dict[str, str] = {}
44
+
45
+ def filter_violations(self, violations: list[Violation]) -> list[Violation]:
46
+ """Filter violations based on ignore directives.
47
+
48
+ Args:
49
+ violations: List of violations to filter
50
+
51
+ Returns:
52
+ List of violations not suppressed by ignore directives
53
+ """
54
+ return [v for v in violations if not self._should_ignore(v)]
55
+
56
+ def _should_ignore(self, violation: Violation) -> bool:
57
+ """Check if a violation should be ignored.
58
+
59
+ Args:
60
+ violation: Violation to check
61
+
62
+ Returns:
63
+ True if violation should be ignored
64
+ """
65
+ file_content = self._get_file_content(violation.file_path)
66
+ return self._ignore_parser.should_ignore_violation(violation, file_content)
67
+
68
+ def _get_file_content(self, file_path: str) -> str:
69
+ """Get file content with caching.
70
+
71
+ Args:
72
+ file_path: Path to file
73
+
74
+ Returns:
75
+ File content or empty string if unreadable
76
+ """
77
+ with suppress(KeyError):
78
+ return self._file_content_cache[file_path]
79
+
80
+ content = self._read_file_content(file_path)
81
+ self._file_content_cache[file_path] = content
82
+ return content
83
+
84
+ def _read_file_content(self, file_path: str) -> str:
85
+ """Read file content from disk.
86
+
87
+ Args:
88
+ file_path: Path to file
89
+
90
+ Returns:
91
+ File content or empty string if unreadable
92
+ """
93
+ try:
94
+ return Path(file_path).read_text(encoding="utf-8")
95
+ except (OSError, UnicodeDecodeError):
96
+ return ""
97
+
98
+ def clear_cache(self) -> None:
99
+ """Clear file content cache."""
100
+ self._file_content_cache.clear()
@@ -0,0 +1,51 @@
1
+ """
2
+ Purpose: Shared ignore pattern matching utilities
3
+
4
+ Scope: Common ignore pattern checking for stringly-typed linter components
5
+
6
+ Overview: Provides shared utility functions for checking if file paths match ignore patterns.
7
+ Used by both the main linter and violation generator to avoid duplicating ignore pattern
8
+ matching logic. Centralizes the ignore pattern matching algorithm.
9
+
10
+ Dependencies: pathlib.Path, fnmatch
11
+
12
+ Exports: is_ignored function
13
+
14
+ Interfaces: is_ignored(file_path, ignore_patterns) -> bool
15
+
16
+ Implementation: Glob pattern matching with fnmatch for flexible ignore patterns
17
+ """
18
+
19
+ import fnmatch
20
+ from pathlib import Path
21
+
22
+
23
+ def is_ignored(file_path: str | Path, ignore_patterns: list[str]) -> bool:
24
+ """Check if file path matches any ignore pattern.
25
+
26
+ Supports glob patterns like:
27
+ - **/tests/** - matches any file in tests directories
28
+ - **/*_test.py - matches any file ending in _test.py
29
+ - tests/ - simple substring match
30
+
31
+ Args:
32
+ file_path: Path to check (string or Path object)
33
+ ignore_patterns: List of patterns to match against
34
+
35
+ Returns:
36
+ True if file should be ignored
37
+ """
38
+ if not ignore_patterns:
39
+ return False
40
+
41
+ path_str = str(file_path)
42
+
43
+ for pattern in ignore_patterns:
44
+ # Use fnmatch for glob-style patterns
45
+ if fnmatch.fnmatch(path_str, pattern):
46
+ return True
47
+ # Also check if pattern appears as substring (for simple patterns)
48
+ if pattern in path_str:
49
+ return True
50
+
51
+ return False
@@ -0,0 +1,376 @@
1
+ """
2
+ Purpose: Main stringly-typed linter rule with cross-file detection
3
+
4
+ Scope: StringlyTypedRule implementing MultiLanguageLintRule for cross-file pattern detection
5
+
6
+ Overview: Implements stringly-typed linter rule following MultiLanguageLintRule interface with
7
+ cross-file detection using SQLite storage. Orchestrates pattern detection by delegating to
8
+ language-specific analyzers (Python, TypeScript). During check() phase, patterns are collected
9
+ into storage. During finalize() phase, storage is queried for patterns appearing across
10
+ multiple files and violations are generated. Maintains minimal orchestration logic to comply
11
+ with SRP.
12
+
13
+ Dependencies: MultiLanguageLintRule, BaseLintContext, PythonStringlyTypedAnalyzer,
14
+ StringlyTypedStorage, StorageInitializer, ViolationGenerator, StringlyTypedConfig
15
+
16
+ Exports: StringlyTypedRule class
17
+
18
+ Interfaces: StringlyTypedRule.check(context) -> list[Violation],
19
+ StringlyTypedRule.finalize() -> list[Violation]
20
+
21
+ Implementation: Two-phase pattern: check() stores data, finalize() generates violations.
22
+ Delegates all logic to helper classes, maintains only orchestration and state.
23
+
24
+ Suppressions:
25
+ - B101: Type narrowing assertions after guards (storage initialized, file_path/content set)
26
+ - srp: Rule class orchestrates cross-file detection with storage, analyzers, and generators.
27
+ Splitting would fragment the two-phase detection workflow.
28
+ """
29
+
30
+ from __future__ import annotations
31
+
32
+ from dataclasses import dataclass
33
+ from pathlib import Path
34
+
35
+ from src.core.base import BaseLintContext, MultiLanguageLintRule
36
+ from src.core.linter_utils import load_linter_config
37
+ from src.core.types import Violation
38
+
39
+ from .config import StringlyTypedConfig
40
+ from .ignore_utils import is_ignored
41
+ from .python.analyzer import (
42
+ AnalysisResult,
43
+ ComparisonResult,
44
+ FunctionCallResult,
45
+ PythonStringlyTypedAnalyzer,
46
+ )
47
+ from .storage import StoredComparison, StoredFunctionCall, StoredPattern, StringlyTypedStorage
48
+ from .storage_initializer import StorageInitializer
49
+ from .typescript.analyzer import TypeScriptStringlyTypedAnalyzer
50
+ from .violation_generator import ViolationGenerator
51
+
52
+
53
+ def compute_string_set_hash(values: set[str]) -> int:
54
+ """Compute consistent hash for a set of strings.
55
+
56
+ Args:
57
+ values: Set of string values to hash
58
+
59
+ Returns:
60
+ Hash value based on sorted, lowercased strings
61
+ """
62
+ return hash(tuple(sorted(s.lower() for s in values)))
63
+
64
+
65
+ def _is_ready_for_analysis(context: BaseLintContext, storage: StringlyTypedStorage | None) -> bool:
66
+ """Check if context and storage are ready for analysis."""
67
+ return bool(context.file_path and context.file_content and storage)
68
+
69
+
70
+ def _convert_to_stored_pattern(result: AnalysisResult) -> StoredPattern:
71
+ """Convert AnalysisResult to StoredPattern.
72
+
73
+ Args:
74
+ result: Analysis result from language analyzer
75
+
76
+ Returns:
77
+ StoredPattern for storage
78
+ """
79
+ return StoredPattern(
80
+ file_path=result.file_path,
81
+ line_number=result.line_number,
82
+ column=result.column,
83
+ variable_name=result.variable_name,
84
+ string_set_hash=compute_string_set_hash(result.string_values),
85
+ string_values=sorted(result.string_values),
86
+ pattern_type=result.pattern_type,
87
+ details=result.details,
88
+ )
89
+
90
+
91
+ def _convert_to_stored_function_call(result: FunctionCallResult) -> StoredFunctionCall:
92
+ """Convert FunctionCallResult to StoredFunctionCall.
93
+
94
+ Args:
95
+ result: Function call result from language analyzer
96
+
97
+ Returns:
98
+ StoredFunctionCall for storage
99
+ """
100
+ return StoredFunctionCall(
101
+ file_path=result.file_path,
102
+ line_number=result.line_number,
103
+ column=result.column,
104
+ function_name=result.function_name,
105
+ param_index=result.param_index,
106
+ string_value=result.string_value,
107
+ )
108
+
109
+
110
+ def _convert_to_stored_comparison(result: ComparisonResult) -> StoredComparison:
111
+ """Convert ComparisonResult to StoredComparison.
112
+
113
+ Args:
114
+ result: Comparison result from language analyzer
115
+
116
+ Returns:
117
+ StoredComparison for storage
118
+ """
119
+ return StoredComparison(
120
+ file_path=result.file_path,
121
+ line_number=result.line_number,
122
+ column=result.column,
123
+ variable_name=result.variable_name,
124
+ compared_value=result.compared_value,
125
+ operator=result.operator,
126
+ )
127
+
128
+
129
+ @dataclass
130
+ class StringlyTypedComponents:
131
+ """Component dependencies for stringly-typed linter."""
132
+
133
+ storage_initializer: StorageInitializer
134
+ violation_generator: ViolationGenerator
135
+ python_analyzer: PythonStringlyTypedAnalyzer
136
+ typescript_analyzer: TypeScriptStringlyTypedAnalyzer
137
+
138
+
139
+ class StringlyTypedRule(MultiLanguageLintRule): # thailint: ignore[srp]
140
+ """Detects stringly-typed patterns across project files.
141
+
142
+ Uses two-phase pattern:
143
+ 1. check() - Collects patterns into SQLite storage (returns empty list)
144
+ 2. finalize() - Queries storage and generates violations for cross-file patterns
145
+ """
146
+
147
+ def __init__(self) -> None:
148
+ """Initialize the stringly-typed rule with helper components."""
149
+ self._storage: StringlyTypedStorage | None = None
150
+ self._initialized = False
151
+ self._config: StringlyTypedConfig | None = None
152
+
153
+ # Helper components grouped to reduce instance attributes
154
+ self._helpers = StringlyTypedComponents(
155
+ storage_initializer=StorageInitializer(),
156
+ violation_generator=ViolationGenerator(),
157
+ python_analyzer=PythonStringlyTypedAnalyzer(),
158
+ typescript_analyzer=TypeScriptStringlyTypedAnalyzer(),
159
+ )
160
+
161
+ @property
162
+ def _active_storage(self) -> StringlyTypedStorage:
163
+ """Get storage, asserting it has been initialized.
164
+
165
+ Returns:
166
+ The initialized storage instance.
167
+
168
+ Raises:
169
+ AssertionError: If storage has not been initialized.
170
+ """
171
+ assert self._storage is not None, "Storage not initialized" # nosec B101
172
+ return self._storage
173
+
174
+ @property
175
+ def rule_id(self) -> str:
176
+ """Unique identifier for this rule."""
177
+ return "stringly-typed.repeated-validation"
178
+
179
+ @property
180
+ def rule_name(self) -> str:
181
+ """Human-readable name for this rule."""
182
+ return "Stringly-Typed Pattern"
183
+
184
+ @property
185
+ def description(self) -> str:
186
+ """Description of what this rule checks."""
187
+ return "Detects stringly-typed code patterns that should use enums"
188
+
189
+ def _load_config(self, context: BaseLintContext) -> StringlyTypedConfig:
190
+ """Load configuration from context.
191
+
192
+ Args:
193
+ context: Lint context with metadata
194
+
195
+ Returns:
196
+ StringlyTypedConfig instance
197
+ """
198
+ return load_linter_config(context, "stringly_typed", StringlyTypedConfig)
199
+
200
+ def _check_python(
201
+ self, context: BaseLintContext, config: StringlyTypedConfig
202
+ ) -> list[Violation]:
203
+ """Analyze Python code and store patterns.
204
+
205
+ Args:
206
+ context: Lint context with file content
207
+ config: Stringly-typed configuration
208
+
209
+ Returns:
210
+ Empty list (violations generated in finalize)
211
+ """
212
+ self._ensure_storage_initialized(context, config)
213
+ self._analyze_python_file(context, config)
214
+ return []
215
+
216
+ def _check_typescript(
217
+ self, context: BaseLintContext, config: StringlyTypedConfig
218
+ ) -> list[Violation]:
219
+ """Analyze TypeScript code and store patterns.
220
+
221
+ Args:
222
+ context: Lint context with file content
223
+ config: Stringly-typed configuration
224
+
225
+ Returns:
226
+ Empty list (violations generated in finalize)
227
+ """
228
+ self._ensure_storage_initialized(context, config)
229
+ self._analyze_typescript_file(context, config)
230
+ return []
231
+
232
+ def _analyze_typescript_file(
233
+ self, context: BaseLintContext, config: StringlyTypedConfig
234
+ ) -> None:
235
+ """Analyze TypeScript file and store patterns.
236
+
237
+ Uses single-parse optimization to avoid duplicate parsing overhead.
238
+
239
+ Args:
240
+ context: Lint context with file content
241
+ config: Stringly-typed configuration
242
+ """
243
+ if not self._should_analyze(context, config):
244
+ return
245
+ # _should_analyze ensures file_path and file_content are set
246
+ assert context.file_path is not None # nosec B101
247
+ assert context.file_content is not None # nosec B101
248
+
249
+ self._helpers.typescript_analyzer.config = config
250
+ call_results, comparison_results = self._helpers.typescript_analyzer.analyze_all(
251
+ context.file_content, context.file_path
252
+ )
253
+ self._store_typescript_results(call_results, comparison_results)
254
+
255
+ def _store_typescript_results(
256
+ self,
257
+ call_results: list[FunctionCallResult],
258
+ comparison_results: list[ComparisonResult],
259
+ ) -> None:
260
+ """Store TypeScript analysis results.
261
+
262
+ Args:
263
+ call_results: Function call patterns found
264
+ comparison_results: Comparison patterns found
265
+ """
266
+ stored_calls = [_convert_to_stored_function_call(r) for r in call_results]
267
+ self._active_storage.add_function_calls(stored_calls)
268
+ stored_comparisons = [_convert_to_stored_comparison(r) for r in comparison_results]
269
+ self._active_storage.add_comparisons(stored_comparisons)
270
+
271
+ def _ensure_storage_initialized(
272
+ self, context: BaseLintContext, config: StringlyTypedConfig
273
+ ) -> None:
274
+ """Initialize storage and analyzers on first call.
275
+
276
+ Args:
277
+ context: Lint context
278
+ config: Stringly-typed configuration
279
+ """
280
+ if not self._initialized:
281
+ self._storage = self._helpers.storage_initializer.initialize(context, config)
282
+ self._config = config
283
+ self._initialized = True
284
+
285
+ def _analyze_python_file(self, context: BaseLintContext, config: StringlyTypedConfig) -> None:
286
+ """Analyze Python file and store patterns.
287
+
288
+ Args:
289
+ context: Lint context with file content
290
+ config: Stringly-typed configuration
291
+ """
292
+ if not self._should_analyze(context, config):
293
+ return
294
+ # _should_analyze ensures file_path and file_content are set
295
+ assert context.file_path is not None # nosec B101
296
+ assert context.file_content is not None # nosec B101
297
+
298
+ file_path = context.file_path
299
+ file_content = context.file_content
300
+ self._helpers.python_analyzer.config = config
301
+
302
+ self._store_validation_patterns(file_content, file_path)
303
+ self._store_function_calls(file_content, file_path)
304
+ self._store_comparisons(file_content, file_path)
305
+
306
+ def _should_analyze(self, context: BaseLintContext, config: StringlyTypedConfig) -> bool:
307
+ """Check if file should be analyzed.
308
+
309
+ Args:
310
+ context: Lint context
311
+ config: Configuration
312
+
313
+ Returns:
314
+ True if file should be analyzed
315
+ """
316
+ if not _is_ready_for_analysis(context, self._storage):
317
+ return False
318
+ # _is_ready_for_analysis ensures file_path is set
319
+ assert context.file_path is not None # nosec B101
320
+ return not is_ignored(context.file_path, config.ignore)
321
+
322
+ def _store_validation_patterns(self, file_content: str, file_path: Path) -> None:
323
+ """Analyze and store validation patterns.
324
+
325
+ Args:
326
+ file_content: Python source code
327
+ file_path: Path to file
328
+ """
329
+ results = self._helpers.python_analyzer.analyze(file_content, file_path)
330
+ self._active_storage.add_patterns([_convert_to_stored_pattern(r) for r in results])
331
+
332
+ def _store_function_calls(self, file_content: str, file_path: Path) -> None:
333
+ """Analyze and store function call patterns.
334
+
335
+ Args:
336
+ file_content: Python source code
337
+ file_path: Path to file
338
+ """
339
+ call_results = self._helpers.python_analyzer.analyze_function_calls(file_content, file_path)
340
+ stored_calls = [_convert_to_stored_function_call(r) for r in call_results]
341
+ self._active_storage.add_function_calls(stored_calls)
342
+
343
+ def _store_comparisons(self, file_content: str, file_path: Path) -> None:
344
+ """Analyze and store Python comparison patterns.
345
+
346
+ Args:
347
+ file_content: Python source code
348
+ file_path: Path to file
349
+ """
350
+ comparison_results = self._helpers.python_analyzer.analyze_comparisons(
351
+ file_content, file_path
352
+ )
353
+ stored_comparisons = [_convert_to_stored_comparison(r) for r in comparison_results]
354
+ self._active_storage.add_comparisons(stored_comparisons)
355
+
356
+ def finalize(self) -> list[Violation]:
357
+ """Generate violations after all files processed.
358
+
359
+ Returns:
360
+ List of violations for patterns appearing in multiple files
361
+ """
362
+ if not self._storage or not self._config:
363
+ return []
364
+
365
+ # Generate violations from cross-file patterns
366
+ violations = self._helpers.violation_generator.generate_violations(
367
+ self._storage, self.rule_id, self._config
368
+ )
369
+
370
+ # Cleanup and reset state for next run
371
+ self._storage.close()
372
+ self._storage = None
373
+ self._config = None
374
+ self._initialized = False
375
+
376
+ return violations
@@ -0,0 +1,33 @@
1
+ """
2
+ Purpose: Python-specific detection for stringly-typed patterns
3
+
4
+ Scope: Python AST analysis for membership validation, equality chains, and function calls
5
+
6
+ Overview: Exposes Python analysis components for detecting stringly-typed patterns in Python
7
+ source code. Includes validation_detector for finding 'x in ("a", "b")' patterns,
8
+ conditional_detector for finding if/elif chains and match statements, call_tracker for
9
+ finding function calls with string arguments, and analyzer for coordinating detection
10
+ across Python files. Uses AST traversal to identify where plain strings are used instead
11
+ of proper enums or typed alternatives.
12
+
13
+ Dependencies: ast module for Python AST parsing
14
+
15
+ Exports: MembershipValidationDetector, ConditionalPatternDetector, FunctionCallTracker,
16
+ PythonStringlyTypedAnalyzer
17
+
18
+ Interfaces: Detector and analyzer classes for Python stringly-typed pattern detection
19
+
20
+ Implementation: AST NodeVisitor pattern for traversing Python syntax trees
21
+ """
22
+
23
+ from .analyzer import PythonStringlyTypedAnalyzer
24
+ from .call_tracker import FunctionCallTracker
25
+ from .conditional_detector import ConditionalPatternDetector
26
+ from .validation_detector import MembershipValidationDetector
27
+
28
+ __all__ = [
29
+ "ConditionalPatternDetector",
30
+ "FunctionCallTracker",
31
+ "MembershipValidationDetector",
32
+ "PythonStringlyTypedAnalyzer",
33
+ ]