thailint 0.2.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 (214) 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 +44 -27
  23. src/core/base.py +95 -5
  24. src/core/cli_utils.py +19 -2
  25. src/core/config_parser.py +36 -6
  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 +125 -22
  65. src/linters/dry/block_grouper.py +4 -0
  66. src/linters/dry/cache.py +142 -94
  67. src/linters/dry/cache_query.py +4 -0
  68. src/linters/dry/config.py +68 -21
  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 +20 -82
  73. src/linters/dry/file_analyzer.py +15 -50
  74. src/linters/dry/inline_ignore.py +7 -16
  75. src/linters/dry/linter.py +182 -54
  76. src/linters/dry/python_analyzer.py +108 -336
  77. src/linters/dry/python_constant_extractor.py +100 -0
  78. src/linters/dry/single_statement_detector.py +417 -0
  79. src/linters/dry/storage_initializer.py +9 -18
  80. src/linters/dry/token_hasher.py +129 -71
  81. src/linters/dry/typescript_analyzer.py +68 -380
  82. src/linters/dry/typescript_constant_extractor.py +138 -0
  83. src/linters/dry/typescript_statement_detector.py +255 -0
  84. src/linters/dry/typescript_value_extractor.py +70 -0
  85. src/linters/dry/violation_builder.py +4 -0
  86. src/linters/dry/violation_filter.py +9 -5
  87. src/linters/dry/violation_generator.py +71 -14
  88. src/linters/file_header/__init__.py +24 -0
  89. src/linters/file_header/atemporal_detector.py +105 -0
  90. src/linters/file_header/base_parser.py +93 -0
  91. src/linters/file_header/bash_parser.py +66 -0
  92. src/linters/file_header/config.py +140 -0
  93. src/linters/file_header/css_parser.py +70 -0
  94. src/linters/file_header/field_validator.py +72 -0
  95. src/linters/file_header/linter.py +309 -0
  96. src/linters/file_header/markdown_parser.py +130 -0
  97. src/linters/file_header/python_parser.py +42 -0
  98. src/linters/file_header/typescript_parser.py +73 -0
  99. src/linters/file_header/violation_builder.py +79 -0
  100. src/linters/file_placement/config_loader.py +3 -1
  101. src/linters/file_placement/directory_matcher.py +4 -0
  102. src/linters/file_placement/linter.py +74 -31
  103. src/linters/file_placement/pattern_matcher.py +41 -6
  104. src/linters/file_placement/pattern_validator.py +31 -12
  105. src/linters/file_placement/rule_checker.py +12 -7
  106. src/linters/lazy_ignores/__init__.py +43 -0
  107. src/linters/lazy_ignores/config.py +74 -0
  108. src/linters/lazy_ignores/directive_utils.py +164 -0
  109. src/linters/lazy_ignores/header_parser.py +177 -0
  110. src/linters/lazy_ignores/linter.py +158 -0
  111. src/linters/lazy_ignores/matcher.py +168 -0
  112. src/linters/lazy_ignores/python_analyzer.py +209 -0
  113. src/linters/lazy_ignores/rule_id_utils.py +180 -0
  114. src/linters/lazy_ignores/skip_detector.py +298 -0
  115. src/linters/lazy_ignores/types.py +71 -0
  116. src/linters/lazy_ignores/typescript_analyzer.py +146 -0
  117. src/linters/lazy_ignores/violation_builder.py +135 -0
  118. src/linters/lbyl/__init__.py +31 -0
  119. src/linters/lbyl/config.py +63 -0
  120. src/linters/lbyl/linter.py +67 -0
  121. src/linters/lbyl/pattern_detectors/__init__.py +53 -0
  122. src/linters/lbyl/pattern_detectors/base.py +63 -0
  123. src/linters/lbyl/pattern_detectors/dict_key_detector.py +107 -0
  124. src/linters/lbyl/pattern_detectors/division_check_detector.py +232 -0
  125. src/linters/lbyl/pattern_detectors/file_exists_detector.py +220 -0
  126. src/linters/lbyl/pattern_detectors/hasattr_detector.py +119 -0
  127. src/linters/lbyl/pattern_detectors/isinstance_detector.py +119 -0
  128. src/linters/lbyl/pattern_detectors/len_check_detector.py +173 -0
  129. src/linters/lbyl/pattern_detectors/none_check_detector.py +146 -0
  130. src/linters/lbyl/pattern_detectors/string_validator_detector.py +145 -0
  131. src/linters/lbyl/python_analyzer.py +215 -0
  132. src/linters/lbyl/violation_builder.py +354 -0
  133. src/linters/magic_numbers/__init__.py +48 -0
  134. src/linters/magic_numbers/config.py +82 -0
  135. src/linters/magic_numbers/context_analyzer.py +249 -0
  136. src/linters/magic_numbers/linter.py +462 -0
  137. src/linters/magic_numbers/python_analyzer.py +64 -0
  138. src/linters/magic_numbers/typescript_analyzer.py +215 -0
  139. src/linters/magic_numbers/typescript_ignore_checker.py +81 -0
  140. src/linters/magic_numbers/violation_builder.py +98 -0
  141. src/linters/method_property/__init__.py +49 -0
  142. src/linters/method_property/config.py +138 -0
  143. src/linters/method_property/linter.py +414 -0
  144. src/linters/method_property/python_analyzer.py +473 -0
  145. src/linters/method_property/violation_builder.py +119 -0
  146. src/linters/nesting/__init__.py +6 -2
  147. src/linters/nesting/config.py +6 -3
  148. src/linters/nesting/linter.py +31 -34
  149. src/linters/nesting/python_analyzer.py +4 -0
  150. src/linters/nesting/typescript_analyzer.py +6 -11
  151. src/linters/nesting/violation_builder.py +1 -0
  152. src/linters/performance/__init__.py +91 -0
  153. src/linters/performance/config.py +43 -0
  154. src/linters/performance/constants.py +49 -0
  155. src/linters/performance/linter.py +149 -0
  156. src/linters/performance/python_analyzer.py +365 -0
  157. src/linters/performance/regex_analyzer.py +312 -0
  158. src/linters/performance/regex_linter.py +139 -0
  159. src/linters/performance/typescript_analyzer.py +236 -0
  160. src/linters/performance/violation_builder.py +160 -0
  161. src/linters/print_statements/__init__.py +53 -0
  162. src/linters/print_statements/config.py +78 -0
  163. src/linters/print_statements/linter.py +413 -0
  164. src/linters/print_statements/python_analyzer.py +153 -0
  165. src/linters/print_statements/typescript_analyzer.py +125 -0
  166. src/linters/print_statements/violation_builder.py +96 -0
  167. src/linters/srp/__init__.py +3 -3
  168. src/linters/srp/class_analyzer.py +11 -7
  169. src/linters/srp/config.py +12 -6
  170. src/linters/srp/heuristics.py +56 -22
  171. src/linters/srp/linter.py +47 -39
  172. src/linters/srp/python_analyzer.py +55 -20
  173. src/linters/srp/typescript_metrics_calculator.py +110 -50
  174. src/linters/stateless_class/__init__.py +25 -0
  175. src/linters/stateless_class/config.py +58 -0
  176. src/linters/stateless_class/linter.py +349 -0
  177. src/linters/stateless_class/python_analyzer.py +290 -0
  178. src/linters/stringly_typed/__init__.py +36 -0
  179. src/linters/stringly_typed/config.py +189 -0
  180. src/linters/stringly_typed/context_filter.py +451 -0
  181. src/linters/stringly_typed/function_call_violation_builder.py +135 -0
  182. src/linters/stringly_typed/ignore_checker.py +100 -0
  183. src/linters/stringly_typed/ignore_utils.py +51 -0
  184. src/linters/stringly_typed/linter.py +376 -0
  185. src/linters/stringly_typed/python/__init__.py +33 -0
  186. src/linters/stringly_typed/python/analyzer.py +348 -0
  187. src/linters/stringly_typed/python/call_tracker.py +175 -0
  188. src/linters/stringly_typed/python/comparison_tracker.py +257 -0
  189. src/linters/stringly_typed/python/condition_extractor.py +134 -0
  190. src/linters/stringly_typed/python/conditional_detector.py +179 -0
  191. src/linters/stringly_typed/python/constants.py +21 -0
  192. src/linters/stringly_typed/python/match_analyzer.py +94 -0
  193. src/linters/stringly_typed/python/validation_detector.py +189 -0
  194. src/linters/stringly_typed/python/variable_extractor.py +96 -0
  195. src/linters/stringly_typed/storage.py +620 -0
  196. src/linters/stringly_typed/storage_initializer.py +45 -0
  197. src/linters/stringly_typed/typescript/__init__.py +28 -0
  198. src/linters/stringly_typed/typescript/analyzer.py +157 -0
  199. src/linters/stringly_typed/typescript/call_tracker.py +335 -0
  200. src/linters/stringly_typed/typescript/comparison_tracker.py +378 -0
  201. src/linters/stringly_typed/violation_generator.py +419 -0
  202. src/orchestrator/core.py +264 -16
  203. src/orchestrator/language_detector.py +5 -3
  204. src/templates/thailint_config_template.yaml +354 -0
  205. src/utils/project_root.py +138 -16
  206. thailint-0.15.3.dist-info/METADATA +187 -0
  207. thailint-0.15.3.dist-info/RECORD +226 -0
  208. {thailint-0.2.0.dist-info → thailint-0.15.3.dist-info}/WHEEL +1 -1
  209. thailint-0.15.3.dist-info/entry_points.txt +4 -0
  210. src/cli.py +0 -1055
  211. thailint-0.2.0.dist-info/METADATA +0 -980
  212. thailint-0.2.0.dist-info/RECORD +0 -75
  213. thailint-0.2.0.dist-info/entry_points.txt +0 -4
  214. {thailint-0.2.0.dist-info → thailint-0.15.3.dist-info/licenses}/LICENSE +0 -0
@@ -0,0 +1,64 @@
1
+ """
2
+ Purpose: Python AST analysis for finding numeric literal nodes
3
+
4
+ Scope: Python magic number detection through AST traversal
5
+
6
+ Overview: Provides PythonMagicNumberAnalyzer class that traverses Python AST to find all numeric
7
+ literal nodes (integers and floats). Uses ast.NodeVisitor pattern to walk the syntax tree and
8
+ collect Constant nodes containing numeric values along with their parent nodes and line numbers.
9
+ Returns structured data about each numeric literal including the AST node, parent node, numeric
10
+ value, and source location. This analyzer handles Python-specific AST structure and provides
11
+ the foundation for magic number detection by identifying all candidates before context filtering.
12
+
13
+ Dependencies: ast module for AST parsing and node types, analyzers.ast_utils
14
+
15
+ Exports: PythonMagicNumberAnalyzer class
16
+
17
+ Interfaces: PythonMagicNumberAnalyzer.find_numeric_literals(tree) -> list[tuple],
18
+ returns list of (node, parent, value, line_number) tuples
19
+
20
+ Implementation: AST NodeVisitor pattern with parent tracking, filters for numeric Constant nodes
21
+ """
22
+
23
+ import ast
24
+ from typing import Any
25
+
26
+ from src.analyzers.ast_utils import build_parent_map
27
+
28
+
29
+ class PythonMagicNumberAnalyzer(ast.NodeVisitor):
30
+ """Analyzes Python AST to find numeric literals."""
31
+
32
+ def __init__(self) -> None:
33
+ """Initialize the analyzer."""
34
+ self.numeric_literals: list[tuple[ast.Constant, ast.AST | None, Any, int]] = []
35
+ self.parent_map: dict[ast.AST, ast.AST] = {}
36
+
37
+ def find_numeric_literals(
38
+ self, tree: ast.AST
39
+ ) -> list[tuple[ast.Constant, ast.AST | None, Any, int]]:
40
+ """Find all numeric literals in the AST.
41
+
42
+ Args:
43
+ tree: The AST to analyze
44
+
45
+ Returns:
46
+ List of tuples (node, parent, value, line_number)
47
+ """
48
+ self.numeric_literals = []
49
+ self.parent_map = build_parent_map(tree)
50
+ self.visit(tree)
51
+ return self.numeric_literals
52
+
53
+ def visit_Constant(self, node: ast.Constant) -> None:
54
+ """Visit a Constant node and check if it's a numeric literal.
55
+
56
+ Args:
57
+ node: The Constant node to check
58
+ """
59
+ if isinstance(node.value, (int, float)):
60
+ parent = self.parent_map.get(node)
61
+ line_number = node.lineno if hasattr(node, "lineno") else 0
62
+ self.numeric_literals.append((node, parent, node.value, line_number))
63
+
64
+ self.generic_visit(node)
@@ -0,0 +1,215 @@
1
+ """
2
+ Purpose: TypeScript/JavaScript magic number detection using Tree-sitter AST analysis
3
+
4
+ Scope: Tree-sitter based numeric literal detection for TypeScript and JavaScript code
5
+
6
+ Overview: Analyzes TypeScript and JavaScript code to detect numeric literals that should be
7
+ extracted to named constants. Uses Tree-sitter parser to traverse TypeScript AST and
8
+ identify numeric literal nodes with their line numbers and values. Detects acceptable
9
+ contexts such as enum definitions and UPPERCASE constant declarations to avoid false
10
+ positives. Supports both TypeScript and JavaScript files with shared detection logic.
11
+ Handles TypeScript-specific syntax including enums, const assertions, readonly properties,
12
+ arrow functions, async functions, and class methods.
13
+
14
+ Dependencies: TypeScriptBaseAnalyzer for tree-sitter parsing, tree-sitter Node type
15
+
16
+ Exports: TypeScriptMagicNumberAnalyzer class with find_numeric_literals and context detection
17
+
18
+ Interfaces: find_numeric_literals(root_node) -> list[tuple], is_enum_context(node),
19
+ is_constant_definition(node)
20
+
21
+ Implementation: Tree-sitter node traversal with visitor pattern, context-aware filtering
22
+ for acceptable numeric literal locations
23
+
24
+ Suppressions:
25
+ - srp: Analyzer implements tree-sitter traversal with context detection methods.
26
+ Methods support single responsibility of magic number detection in TypeScript.
27
+ """
28
+
29
+ from src.analyzers.typescript_base import (
30
+ TREE_SITTER_AVAILABLE,
31
+ Node,
32
+ TypeScriptBaseAnalyzer,
33
+ )
34
+
35
+
36
+ class TypeScriptMagicNumberAnalyzer(TypeScriptBaseAnalyzer): # thailint: ignore[srp]
37
+ """Analyzes TypeScript/JavaScript code for magic numbers using Tree-sitter.
38
+
39
+ Note: Method count (11) exceeds SRP limit (8) because refactoring for A-grade
40
+ complexity requires extracting helper methods. Class maintains single responsibility
41
+ of TypeScript magic number detection - all methods support this core purpose.
42
+ """
43
+
44
+ def find_numeric_literals(self, root_node: Node) -> list[tuple[Node, float | int, int]]:
45
+ """Find all numeric literal nodes in TypeScript/JavaScript AST.
46
+
47
+ Args:
48
+ root_node: Root tree-sitter node to search from
49
+
50
+ Returns:
51
+ List of (node, value, line_number) tuples for each numeric literal
52
+ """
53
+ if not TREE_SITTER_AVAILABLE or root_node is None:
54
+ return []
55
+
56
+ literals: list[tuple[Node, float | int, int]] = []
57
+ self._collect_numeric_literals(root_node, literals)
58
+ return literals
59
+
60
+ def _collect_numeric_literals(
61
+ self, node: Node, literals: list[tuple[Node, float | int, int]]
62
+ ) -> None:
63
+ """Recursively collect numeric literals from AST.
64
+
65
+ Args:
66
+ node: Current tree-sitter node
67
+ literals: List to accumulate found literals
68
+ """
69
+ if node.type == "number":
70
+ value = self._extract_numeric_value(node)
71
+ if value is not None:
72
+ line_number = node.start_point[0] + 1
73
+ literals.append((node, value, line_number))
74
+
75
+ for child in node.children:
76
+ self._collect_numeric_literals(child, literals)
77
+
78
+ def _extract_numeric_value(self, node: Node) -> float | int | None:
79
+ """Extract numeric value from number node.
80
+
81
+ Args:
82
+ node: Tree-sitter number node
83
+
84
+ Returns:
85
+ Numeric value (int or float) or None if parsing fails
86
+ """
87
+ text = self.extract_node_text(node)
88
+ try:
89
+ # Try int first
90
+ if "." not in text and "e" not in text.lower():
91
+ return int(text, 0) # Handles hex, octal, binary
92
+ # Otherwise float
93
+ return float(text)
94
+ except (ValueError, TypeError):
95
+ return None
96
+
97
+ def is_enum_context(self, node: Node) -> bool:
98
+ """Check if numeric literal is in enum definition.
99
+
100
+ Args:
101
+ node: Numeric literal node
102
+
103
+ Returns:
104
+ True if node is within enum_declaration
105
+ """
106
+ if not TREE_SITTER_AVAILABLE:
107
+ return False
108
+
109
+ current = node.parent
110
+ while current is not None:
111
+ if current.type == "enum_declaration":
112
+ return True
113
+ current = current.parent
114
+ return False
115
+
116
+ def is_constant_definition(self, node: Node, source_code: str) -> bool:
117
+ """Check if numeric literal is in UPPERCASE constant definition.
118
+
119
+ Args:
120
+ node: Numeric literal node
121
+ source_code: Full source code to extract variable names
122
+
123
+ Returns:
124
+ True if assigned to UPPERCASE constant variable
125
+ """
126
+ if not TREE_SITTER_AVAILABLE:
127
+ return False
128
+
129
+ # Find the declaration parent
130
+ parent = self._find_declaration_parent(node)
131
+ if parent is None:
132
+ return False
133
+
134
+ # Check if identifier is UPPERCASE constant
135
+ return self._has_uppercase_identifier(parent)
136
+
137
+ def _find_declaration_parent(self, node: Node) -> Node | None:
138
+ """Find the declaration parent node.
139
+
140
+ Args:
141
+ node: Starting node
142
+
143
+ Returns:
144
+ Declaration parent or None
145
+ """
146
+ parent = node.parent
147
+ if self._is_declaration_type(parent):
148
+ return parent
149
+
150
+ # Try grandparent for nested cases
151
+ if parent is not None:
152
+ grandparent = parent.parent
153
+ if self._is_declaration_type(grandparent):
154
+ return grandparent
155
+
156
+ return None
157
+
158
+ def _is_declaration_type(self, node: Node | None) -> bool:
159
+ """Check if node is a declaration type."""
160
+ if node is None:
161
+ return False
162
+ return node.type in ("variable_declarator", "lexical_declaration", "pair")
163
+
164
+ def _has_uppercase_identifier(self, parent_node: Node) -> bool:
165
+ """Check if declaration has UPPERCASE identifier.
166
+
167
+ Args:
168
+ parent_node: Declaration parent node
169
+
170
+ Returns:
171
+ True if identifier is UPPERCASE
172
+ """
173
+ identifier_node = self._find_identifier_in_declaration(parent_node)
174
+ if identifier_node is None:
175
+ return False
176
+
177
+ identifier_text = self.extract_node_text(identifier_node)
178
+ return self._is_uppercase_constant(identifier_text)
179
+
180
+ def _find_identifier_in_declaration(self, node: Node) -> Node | None:
181
+ """Find identifier node in variable declaration.
182
+
183
+ Args:
184
+ node: Variable declarator or lexical declaration node
185
+
186
+ Returns:
187
+ Identifier node or None
188
+ """
189
+ # Walk children looking for identifier
190
+ for child in node.children:
191
+ if child.type in ("identifier", "property_identifier"):
192
+ return child
193
+ # Recursively check children
194
+ result = self._find_identifier_in_declaration(child)
195
+ if result is not None:
196
+ return result
197
+ return None
198
+
199
+ def _is_uppercase_constant(self, name: str) -> bool:
200
+ """Check if identifier is UPPERCASE constant style.
201
+
202
+ Args:
203
+ name: Identifier name
204
+
205
+ Returns:
206
+ True if name is UPPERCASE with optional underscores
207
+ """
208
+ if not name:
209
+ return False
210
+ # Must be at least one letter and all letters must be uppercase
211
+ # Allow underscores and numbers
212
+ letters_only = "".join(c for c in name if c.isalpha())
213
+ if not letters_only:
214
+ return False
215
+ return letters_only.isupper()
@@ -0,0 +1,81 @@
1
+ """
2
+ Purpose: TypeScript-specific ignore directive checking for magic numbers linter
3
+
4
+ Scope: Ignore directive detection for TypeScript/JavaScript files
5
+
6
+ Overview: Provides ignore directive checking functionality specifically for TypeScript and JavaScript
7
+ files in the magic numbers linter. Handles both thailint-style and noqa-style ignore comments
8
+ using TypeScript comment syntax (// instead of #). Extracted from linter.py to reduce file
9
+ size and improve modularity.
10
+
11
+ Dependencies: IgnoreDirectiveParser from src.linter_config.ignore, Violation type, violation_utils
12
+
13
+ Exports: TypeScriptIgnoreChecker class
14
+
15
+ Interfaces: TypeScriptIgnoreChecker.should_ignore(violation, context) -> bool
16
+
17
+ Implementation: Comment parsing with TypeScript-specific syntax handling, uses shared utilities
18
+ """
19
+
20
+ from src.core.base import BaseLintContext
21
+ from src.core.types import Violation
22
+ from src.core.violation_utils import get_violation_line, has_typescript_noqa
23
+ from src.linter_config.ignore import get_ignore_parser
24
+
25
+
26
+ class TypeScriptIgnoreChecker:
27
+ """Checks for TypeScript-style ignore directives in magic numbers linter."""
28
+
29
+ def __init__(self) -> None:
30
+ """Initialize with standard ignore parser."""
31
+ self._ignore_parser = get_ignore_parser()
32
+
33
+ def should_ignore(self, violation: Violation, context: BaseLintContext) -> bool:
34
+ """Check if TypeScript violation should be ignored.
35
+
36
+ Args:
37
+ violation: Violation to check
38
+ context: Lint context
39
+
40
+ Returns:
41
+ True if should ignore
42
+ """
43
+ if self._ignore_parser.should_ignore_violation(violation, context.file_content or ""):
44
+ return True
45
+
46
+ return self._check_typescript_ignore(violation, context)
47
+
48
+ def _check_typescript_ignore(self, violation: Violation, context: BaseLintContext) -> bool:
49
+ """Check for TypeScript-style ignore directives.
50
+
51
+ Args:
52
+ violation: Violation to check
53
+ context: Lint context
54
+
55
+ Returns:
56
+ True if line has ignore directive
57
+ """
58
+ line_text = get_violation_line(violation, context)
59
+ if line_text is None:
60
+ return False
61
+
62
+ return self._has_typescript_ignore_directive(line_text)
63
+
64
+ def _has_typescript_ignore_directive(self, line_text: str) -> bool:
65
+ """Check if line has TypeScript-style ignore directive.
66
+
67
+ Args:
68
+ line_text: Line text to check
69
+
70
+ Returns:
71
+ True if has ignore directive
72
+ """
73
+ if "// thailint: ignore[magic-numbers]" in line_text:
74
+ return True
75
+
76
+ if "// thailint: ignore" in line_text:
77
+ after_ignore = line_text.split("// thailint: ignore")[1].split("//")[0]
78
+ if "[" not in after_ignore:
79
+ return True
80
+
81
+ return has_typescript_noqa(line_text)
@@ -0,0 +1,98 @@
1
+ """
2
+ Purpose: Builds Violation objects for magic number detection
3
+
4
+ Scope: Violation message construction for magic numbers linter
5
+
6
+ Overview: Provides ViolationBuilder class that creates Violation objects for magic number detections.
7
+ Generates helpful, descriptive messages suggesting constant extraction for numeric literals.
8
+ Constructs complete Violation instances with rule_id, file_path, line number, column, message,
9
+ and suggestions. Formats messages to mention the specific numeric value and encourage using
10
+ named constants for better code maintainability and readability. Provides consistent violation
11
+ structure across all magic number detections.
12
+
13
+ Dependencies: src.core.types for Violation dataclass, pathlib for Path handling, ast for node types
14
+
15
+ Exports: ViolationBuilder class
16
+
17
+ Interfaces: ViolationBuilder.create_violation(node, value, line, file_path) -> Violation,
18
+ builds complete Violation object with all required fields
19
+
20
+ Implementation: Message template with value interpolation, structured violation construction
21
+ """
22
+
23
+ import ast
24
+ from pathlib import Path
25
+
26
+ from src.core.types import Violation
27
+
28
+
29
+ class ViolationBuilder:
30
+ """Builds violations for magic number detections."""
31
+
32
+ def __init__(self, rule_id: str) -> None:
33
+ """Initialize the violation builder.
34
+
35
+ Args:
36
+ rule_id: The rule ID to use in violations
37
+ """
38
+ self.rule_id = rule_id
39
+
40
+ def create_violation(
41
+ self,
42
+ node: ast.Constant,
43
+ value: int | float,
44
+ line: int,
45
+ file_path: Path | None,
46
+ ) -> Violation:
47
+ """Create a violation for a magic number.
48
+
49
+ Args:
50
+ node: The AST node containing the magic number
51
+ value: The numeric value
52
+ line: Line number where the violation occurs
53
+ file_path: Path to the file
54
+
55
+ Returns:
56
+ Violation object with details about the magic number
57
+ """
58
+ message = f"Magic number {value} should be a named constant"
59
+
60
+ suggestion = f"Extract {value} to a named constant (e.g., CONSTANT_NAME = {value})"
61
+
62
+ return Violation(
63
+ rule_id=self.rule_id,
64
+ file_path=str(file_path) if file_path else "",
65
+ line=line,
66
+ column=node.col_offset if hasattr(node, "col_offset") else 0,
67
+ message=message,
68
+ suggestion=suggestion,
69
+ )
70
+
71
+ def create_typescript_violation(
72
+ self,
73
+ value: int | float,
74
+ line: int,
75
+ file_path: Path | None,
76
+ ) -> Violation:
77
+ """Create a violation for a TypeScript magic number.
78
+
79
+ Args:
80
+ value: The numeric value
81
+ line: Line number where the violation occurs
82
+ file_path: Path to the file
83
+
84
+ Returns:
85
+ Violation object with details about the magic number
86
+ """
87
+ message = f"Magic number {value} should be a named constant"
88
+
89
+ suggestion = f"Extract {value} to a named constant (e.g., const CONSTANT_NAME = {value})"
90
+
91
+ return Violation(
92
+ rule_id=self.rule_id,
93
+ file_path=str(file_path) if file_path else "",
94
+ line=line,
95
+ column=0, # Tree-sitter nodes don't have easy column access
96
+ message=message,
97
+ suggestion=suggestion,
98
+ )
@@ -0,0 +1,49 @@
1
+ """
2
+ Purpose: Package exports for method-should-be-property linter
3
+
4
+ Scope: Method property linter public API
5
+
6
+ Overview: Exports the MethodPropertyRule class and MethodPropertyConfig dataclass for use by
7
+ the orchestrator and external consumers. Provides a convenience lint() function for
8
+ standalone usage of the linter.
9
+
10
+ Dependencies: MethodPropertyRule from linter module, MethodPropertyConfig from config module
11
+
12
+ Exports: MethodPropertyRule, MethodPropertyConfig, lint function
13
+
14
+ Interfaces: lint(file_path, content, config) -> list[Violation] convenience function
15
+
16
+ Implementation: Simple re-exports from submodules with optional convenience wrapper
17
+ """
18
+
19
+ from .config import MethodPropertyConfig
20
+ from .linter import MethodPropertyRule
21
+
22
+ __all__ = ["MethodPropertyRule", "MethodPropertyConfig", "lint"]
23
+
24
+
25
+ def lint(
26
+ file_path: str,
27
+ content: str,
28
+ config: dict | None = None,
29
+ ) -> list:
30
+ """Lint a file for method-should-be-property violations.
31
+
32
+ Args:
33
+ file_path: Path to the file being linted
34
+ content: Content of the file
35
+ config: Optional configuration dictionary
36
+
37
+ Returns:
38
+ List of Violation objects
39
+ """
40
+ from unittest.mock import Mock
41
+
42
+ rule = MethodPropertyRule()
43
+ context = Mock()
44
+ context.file_path = file_path
45
+ context.file_content = content
46
+ context.language = "python"
47
+ context.config = config
48
+
49
+ return rule.check(context)
@@ -0,0 +1,138 @@
1
+ """
2
+ Purpose: Configuration schema for method-should-be-property linter
3
+
4
+ Scope: Method property linter configuration for Python files
5
+
6
+ Overview: Defines configuration schema for method-should-be-property linter. Provides
7
+ MethodPropertyConfig dataclass with enabled flag, max_body_statements threshold (default 3)
8
+ for determining when a method body is too complex to be a property candidate, and ignore
9
+ patterns list for excluding specific files or directories. Includes configurable action verb
10
+ exclusions (prefixes and names) with sensible defaults that can be extended or overridden.
11
+ Supports per-file and per-directory config overrides through from_dict class method.
12
+ Integrates with orchestrator's configuration system via .thailint.yaml.
13
+
14
+ Dependencies: dataclasses module for configuration structure, typing module for type hints
15
+
16
+ Exports: MethodPropertyConfig dataclass, DEFAULT_EXCLUDE_PREFIXES, DEFAULT_EXCLUDE_NAMES
17
+
18
+ Interfaces: from_dict(config, language) -> MethodPropertyConfig for configuration loading
19
+
20
+ Implementation: Dataclass with defaults matching Pythonic conventions and common use cases
21
+
22
+ Suppressions:
23
+ - dry: MethodPropertyConfig includes extensive exclusion lists that share patterns with
24
+ other config classes. Lists are maintained separately for clear documentation.
25
+ """
26
+
27
+ from dataclasses import dataclass, field
28
+ from typing import Any
29
+
30
+ # Default action verb prefixes - methods starting with these are excluded
31
+ # These represent actions/transformations, not property access
32
+ DEFAULT_EXCLUDE_PREFIXES: tuple[str, ...] = (
33
+ "to_", # Transformation: to_dict, to_json, to_string
34
+ "dump_", # Serialization: dump_to_json, dump_to_apigw
35
+ "generate_", # Factory: generate_report, generate_html
36
+ "create_", # Factory: create_instance, create_config
37
+ "build_", # Construction: build_query, build_html
38
+ "make_", # Factory: make_request, make_connection
39
+ "render_", # Output: render_template, render_html
40
+ "compute_", # Calculation: compute_hash, compute_total
41
+ "calculate_", # Calculation: calculate_sum, calculate_average
42
+ )
43
+
44
+ # Default action verb names - exact method names that are excluded
45
+ # These are lifecycle hooks, display actions, and resource operations
46
+ DEFAULT_EXCLUDE_NAMES: frozenset[str] = frozenset(
47
+ {
48
+ "finalize", # Lifecycle hook
49
+ "serialize", # Transformation
50
+ "dump", # Serialization
51
+ "validate", # Validation action
52
+ "show", # Display action
53
+ "display", # Display action
54
+ "print", # Output action
55
+ "refresh", # Update action
56
+ "reset", # State action
57
+ "clear", # State action
58
+ "close", # Resource action
59
+ "open", # Resource action
60
+ "save", # Persistence action
61
+ "load", # Persistence action
62
+ "execute", # Action
63
+ "run", # Action
64
+ }
65
+ )
66
+
67
+
68
+ def _load_list_config(
69
+ config: dict[str, Any], key: str, override_key: str, default: tuple[str, ...]
70
+ ) -> tuple[str, ...]:
71
+ """Load a list config with extend/override semantics."""
72
+ if override_key in config and isinstance(config[override_key], list):
73
+ return tuple(config[override_key])
74
+ if key in config and isinstance(config[key], list):
75
+ return default + tuple(config[key])
76
+ return default
77
+
78
+
79
+ def _load_set_config(
80
+ config: dict[str, Any], key: str, override_key: str, default: frozenset[str]
81
+ ) -> frozenset[str]:
82
+ """Load a set config with extend/override semantics."""
83
+ if override_key in config and isinstance(config[override_key], list):
84
+ return frozenset(config[override_key])
85
+ if key in config and isinstance(config[key], list):
86
+ return default | frozenset(config[key])
87
+ return default
88
+
89
+
90
+ @dataclass
91
+ class MethodPropertyConfig: # thailint: ignore[dry]
92
+ """Configuration for method-should-be-property linter."""
93
+
94
+ enabled: bool = True
95
+ max_body_statements: int = 3
96
+ ignore: list[str] = field(default_factory=list)
97
+ ignore_methods: list[str] = field(default_factory=list)
98
+
99
+ # Action verb exclusions (extend defaults or override)
100
+ exclude_prefixes: tuple[str, ...] = DEFAULT_EXCLUDE_PREFIXES
101
+ exclude_names: frozenset[str] = DEFAULT_EXCLUDE_NAMES
102
+
103
+ @classmethod
104
+ def from_dict(
105
+ cls, config: dict[str, Any] | None, language: str | None = None
106
+ ) -> "MethodPropertyConfig":
107
+ """Load configuration from dictionary.
108
+
109
+ Args:
110
+ config: Dictionary containing configuration values, or None
111
+ language: Programming language (unused, for interface compatibility)
112
+
113
+ Returns:
114
+ MethodPropertyConfig instance with values from dictionary
115
+ """
116
+ if config is None:
117
+ return cls()
118
+
119
+ ignore_patterns = config.get("ignore", [])
120
+ if not isinstance(ignore_patterns, list):
121
+ ignore_patterns = []
122
+
123
+ ignore_methods = config.get("ignore_methods", [])
124
+ if not isinstance(ignore_methods, list):
125
+ ignore_methods = []
126
+
127
+ return cls(
128
+ enabled=config.get("enabled", True),
129
+ max_body_statements=config.get("max_body_statements", 3),
130
+ ignore=ignore_patterns,
131
+ ignore_methods=ignore_methods,
132
+ exclude_prefixes=_load_list_config(
133
+ config, "exclude_prefixes", "exclude_prefixes_override", DEFAULT_EXCLUDE_PREFIXES
134
+ ),
135
+ exclude_names=_load_set_config(
136
+ config, "exclude_names", "exclude_names_override", DEFAULT_EXCLUDE_NAMES
137
+ ),
138
+ )