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,189 @@
1
+ """
2
+ Purpose: Detect membership validation patterns in Python AST
3
+
4
+ Scope: Find 'x in ("a", "b")' and 'x not in (...)' patterns
5
+
6
+ Overview: Provides MembershipValidationDetector class that traverses Python AST to find
7
+ membership validation patterns where strings are used instead of enums. Detects
8
+ Compare nodes with In/NotIn operators and string literal collections (tuple, set,
9
+ list). Returns structured MembershipPattern dataclass instances with string values,
10
+ operator type, location, and optional variable name. Filters out non-string
11
+ collections, single-element collections, and variable references.
12
+
13
+ Dependencies: ast module for AST parsing, dataclasses for pattern structure,
14
+ variable_extractor for variable name extraction
15
+
16
+ Exports: MembershipValidationDetector class, MembershipPattern dataclass
17
+
18
+ Interfaces: MembershipValidationDetector.find_patterns(tree) -> list[MembershipPattern]
19
+
20
+ Implementation: AST NodeVisitor pattern with Compare node handling for In/NotIn operators
21
+
22
+ Suppressions:
23
+ - invalid-name: visit_Compare follows AST NodeVisitor method naming convention
24
+ """
25
+
26
+ import ast
27
+ from dataclasses import dataclass
28
+
29
+ from .constants import MIN_VALUES_FOR_PATTERN
30
+ from .variable_extractor import extract_variable_name
31
+
32
+
33
+ @dataclass
34
+ class MembershipPattern:
35
+ """Represents a detected membership validation pattern.
36
+
37
+ Captures the essential information about a stringly-typed membership check
38
+ including the string values being compared, the operator used, source location,
39
+ and the variable being tested if identifiable.
40
+ """
41
+
42
+ string_values: set[str]
43
+ """Set of string values in the membership test."""
44
+
45
+ operator: str
46
+ """Operator used: 'in' or 'not in'."""
47
+
48
+ line_number: int
49
+ """Line number where the pattern occurs (1-indexed)."""
50
+
51
+ column: int
52
+ """Column number where the pattern starts (0-indexed)."""
53
+
54
+ variable_name: str | None
55
+ """Variable name being tested, if identifiable from a simple expression."""
56
+
57
+
58
+ class MembershipValidationDetector(ast.NodeVisitor):
59
+ """Detects membership validation patterns in Python AST.
60
+
61
+ Finds patterns like 'x in ("a", "b")' and 'x not in {"c", "d"}' where
62
+ strings are used for validation instead of proper enums.
63
+ """
64
+
65
+ def __init__(self) -> None:
66
+ """Initialize the detector."""
67
+ self.patterns: list[MembershipPattern] = []
68
+
69
+ def find_patterns(self, tree: ast.AST) -> list[MembershipPattern]:
70
+ """Find all membership validation patterns in the AST.
71
+
72
+ Args:
73
+ tree: The AST to analyze
74
+
75
+ Returns:
76
+ List of MembershipPattern instances for each detected pattern
77
+ """
78
+ self.patterns = []
79
+ self.visit(tree)
80
+ return self.patterns
81
+
82
+ def visit_Compare(self, node: ast.Compare) -> None: # pylint: disable=invalid-name
83
+ """Visit a Compare node to check for membership patterns.
84
+
85
+ Handles Compare nodes with In or NotIn operators where the
86
+ comparator is a literal collection of strings.
87
+
88
+ Args:
89
+ node: The Compare node to analyze
90
+ """
91
+ for op_index, operator in enumerate(node.ops):
92
+ self._check_membership_operator(node, operator, op_index)
93
+ self.generic_visit(node)
94
+
95
+ def _check_membership_operator(
96
+ self, node: ast.Compare, operator: ast.cmpop, op_index: int
97
+ ) -> None:
98
+ """Check if an operator forms a valid membership pattern.
99
+
100
+ Args:
101
+ node: The Compare node containing the operator
102
+ operator: The comparison operator to check
103
+ op_index: Index of the operator in the Compare node
104
+ """
105
+ if not isinstance(operator, (ast.In, ast.NotIn)):
106
+ return
107
+
108
+ comparator = node.comparators[op_index]
109
+ string_values = _extract_string_values(comparator)
110
+
111
+ if string_values is None or len(string_values) < MIN_VALUES_FOR_PATTERN:
112
+ return
113
+
114
+ self._add_pattern(node, operator, string_values)
115
+
116
+ def _add_pattern(self, node: ast.Compare, operator: ast.cmpop, string_values: set[str]) -> None:
117
+ """Create and add a membership pattern to results.
118
+
119
+ Args:
120
+ node: The Compare node containing the pattern
121
+ operator: The In or NotIn operator
122
+ string_values: Set of string values detected
123
+ """
124
+ operator_str = "in" if isinstance(operator, ast.In) else "not in"
125
+ variable_name = extract_variable_name(node.left)
126
+
127
+ pattern = MembershipPattern(
128
+ string_values=string_values,
129
+ operator=operator_str,
130
+ line_number=node.lineno,
131
+ column=node.col_offset,
132
+ variable_name=variable_name,
133
+ )
134
+ self.patterns.append(pattern)
135
+
136
+
137
+ def _extract_string_values(node: ast.AST) -> set[str] | None:
138
+ """Extract string values from a collection literal.
139
+
140
+ Args:
141
+ node: AST node representing the collection
142
+
143
+ Returns:
144
+ Set of string values if all elements are strings, None otherwise
145
+ """
146
+ elements = _get_collection_elements(node)
147
+ if elements is None or len(elements) == 0:
148
+ return None
149
+
150
+ return _collect_string_constants(elements)
151
+
152
+
153
+ def _get_collection_elements(node: ast.AST) -> list[ast.expr] | None:
154
+ """Get elements from a collection literal node.
155
+
156
+ Args:
157
+ node: AST node that may be a collection literal
158
+
159
+ Returns:
160
+ List of element nodes if node is a collection, None otherwise
161
+ """
162
+ if isinstance(node, ast.Tuple):
163
+ return list(node.elts)
164
+ if isinstance(node, ast.Set):
165
+ return list(node.elts)
166
+ if isinstance(node, ast.List):
167
+ return list(node.elts)
168
+ return None
169
+
170
+
171
+ def _collect_string_constants(elements: list[ast.expr]) -> set[str] | None:
172
+ """Collect string constants from a list of AST expression nodes.
173
+
174
+ Args:
175
+ elements: List of expression nodes from a collection
176
+
177
+ Returns:
178
+ Set of string values if all elements are string constants, None otherwise
179
+ """
180
+ string_values: set[str] = set()
181
+
182
+ for element in elements:
183
+ if not isinstance(element, ast.Constant):
184
+ return None
185
+ if not isinstance(element.value, str):
186
+ return None
187
+ string_values.add(element.value)
188
+
189
+ return string_values
@@ -0,0 +1,96 @@
1
+ """
2
+ Purpose: Extract variable names from Python AST nodes
3
+
4
+ Scope: AST node analysis for identifying variable names in expressions
5
+
6
+ Overview: Provides functions to extract variable names from various Python AST expression
7
+ types including simple names, attribute access chains, and method calls. Handles
8
+ complex expressions by returning None when the variable cannot be simply identified.
9
+ Supports extraction from Name nodes, Attribute chains (e.g., self.status), and Call
10
+ nodes for method calls (e.g., x.lower()).
11
+
12
+ Dependencies: ast module for AST node types
13
+
14
+ Exports: extract_variable_name, extract_attribute_chain functions
15
+
16
+ Interfaces: extract_variable_name(node) -> str | None for general extraction,
17
+ extract_attribute_chain(node) -> str for attribute chain extraction
18
+
19
+ Implementation: Pattern matching on AST node types with recursive chain handling
20
+ """
21
+
22
+ import ast
23
+
24
+
25
+ def extract_variable_name(node: ast.AST) -> str | None:
26
+ """Extract variable name from an expression node.
27
+
28
+ Identifies the variable being used in an expression, handling
29
+ simple names, attribute access, and method calls.
30
+
31
+ Args:
32
+ node: AST node representing an expression
33
+
34
+ Returns:
35
+ Variable name if identifiable, None for complex expressions
36
+ """
37
+ if isinstance(node, ast.Name):
38
+ return node.id
39
+
40
+ if isinstance(node, ast.Attribute):
41
+ return extract_attribute_chain(node)
42
+
43
+ if isinstance(node, ast.Call):
44
+ return _extract_call_variable(node)
45
+
46
+ return None
47
+
48
+
49
+ def extract_attribute_chain(node: ast.Attribute) -> str:
50
+ """Extract full attribute chain as string.
51
+
52
+ Builds a dotted string representation of attribute access,
53
+ e.g., 'self.status' or 'obj.attr.subattr'.
54
+
55
+ Args:
56
+ node: Attribute node to extract from
57
+
58
+ Returns:
59
+ String representation of attribute chain
60
+ """
61
+ parts: list[str] = [node.attr]
62
+ current = node.value
63
+
64
+ while isinstance(current, ast.Attribute):
65
+ parts.append(current.attr)
66
+ current = current.value
67
+
68
+ if isinstance(current, ast.Name):
69
+ parts.append(current.id)
70
+
71
+ parts.reverse()
72
+ return ".".join(parts)
73
+
74
+
75
+ def _extract_call_variable(node: ast.Call) -> str | None:
76
+ """Extract variable from a method call expression.
77
+
78
+ For expressions like x.lower(), returns 'x'.
79
+ For complex calls like get_value().lower(), returns None.
80
+
81
+ Args:
82
+ node: Call node to extract from
83
+
84
+ Returns:
85
+ Variable name if identifiable, None otherwise
86
+ """
87
+ if not isinstance(node.func, ast.Attribute):
88
+ return None
89
+
90
+ value = node.func.value
91
+ if isinstance(value, ast.Name):
92
+ return value.id
93
+ if isinstance(value, ast.Attribute):
94
+ return extract_attribute_chain(value)
95
+
96
+ return None