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,255 @@
1
+ """
2
+ Purpose: Detects single-statement patterns in TypeScript/JavaScript code for DRY linter filtering
3
+
4
+ Scope: Tree-sitter AST analysis to identify single logical statements that should not be flagged
5
+
6
+ Overview: Provides sophisticated single-statement pattern detection to filter false positives in the
7
+ DRY linter for TypeScript/JavaScript code. Uses tree-sitter AST to identify when a code block
8
+ represents a single logical statement (decorators, call expressions, object literals, class fields,
9
+ JSX elements, interface definitions) that should not be flagged as duplicate code.
10
+
11
+ Dependencies: tree-sitter for TypeScript AST parsing
12
+
13
+ Exports: is_single_statement, should_include_block functions
14
+
15
+ Interfaces: is_single_statement(content, start_line, end_line) -> bool,
16
+ should_include_block(content, start_line, end_line) -> bool
17
+
18
+ Implementation: Tree-sitter AST walking with pattern matching for TypeScript constructs
19
+
20
+ Suppressions:
21
+ - type:ignore[assignment,misc]: Tree-sitter Node type alias (optional dependency fallback)
22
+ """
23
+
24
+ from collections.abc import Generator
25
+ from typing import Any
26
+
27
+ from src.analyzers.typescript_base import TREE_SITTER_AVAILABLE
28
+
29
+ if TREE_SITTER_AVAILABLE:
30
+ from tree_sitter import Node
31
+ else:
32
+ Node = Any # type: ignore[assignment,misc]
33
+
34
+
35
+ def is_single_statement(content: str, start_line: int, end_line: int) -> bool:
36
+ """Check if a line range is a single logical statement.
37
+
38
+ Args:
39
+ content: TypeScript source code
40
+ start_line: Starting line number (1-indexed)
41
+ end_line: Ending line number (1-indexed)
42
+
43
+ Returns:
44
+ True if this range represents a single logical statement/expression
45
+ """
46
+ if not TREE_SITTER_AVAILABLE:
47
+ return False
48
+
49
+ from src.analyzers.typescript_base import TypeScriptBaseAnalyzer
50
+
51
+ analyzer = TypeScriptBaseAnalyzer()
52
+ root = analyzer.parse_typescript(content)
53
+ if not root:
54
+ return False
55
+
56
+ return _check_overlapping_nodes(root, start_line, end_line)
57
+
58
+
59
+ def should_include_block(content: str, start_line: int, end_line: int) -> bool:
60
+ """Check if block should be included (not overlapping interface definitions).
61
+
62
+ Args:
63
+ content: File content
64
+ start_line: Block start line
65
+ end_line: Block end line
66
+
67
+ Returns:
68
+ False if block overlaps interface definition, True otherwise
69
+ """
70
+ interface_ranges = _find_interface_ranges(content)
71
+ return not _overlaps_interface(start_line, end_line, interface_ranges)
72
+
73
+
74
+ def _check_overlapping_nodes(root: Node, start_line: int, end_line: int) -> bool:
75
+ """Check if any AST node overlaps and matches single-statement pattern."""
76
+ ts_start = start_line - 1 # Convert to 0-indexed
77
+ ts_end = end_line - 1
78
+
79
+ return any(_node_overlaps_and_matches(node, ts_start, ts_end) for node in _walk_nodes(root))
80
+
81
+
82
+ def _walk_nodes(node: Node) -> Generator[Node, None, None]:
83
+ """Generator to walk all nodes in tree."""
84
+ yield node
85
+ for child in node.children:
86
+ yield from _walk_nodes(child)
87
+
88
+
89
+ def _node_overlaps_and_matches(node: Node, ts_start: int, ts_end: int) -> bool:
90
+ """Check if node overlaps with range and matches single-statement pattern."""
91
+ node_start = node.start_point[0]
92
+ node_end = node.end_point[0]
93
+
94
+ overlaps = not (node_end < ts_start or node_start > ts_end)
95
+ if not overlaps:
96
+ return False
97
+
98
+ return _is_single_statement_pattern(node, ts_start, ts_end)
99
+
100
+
101
+ def _is_single_statement_pattern(node: Node, ts_start: int, ts_end: int) -> bool:
102
+ """Check if an AST node represents a single-statement pattern to filter."""
103
+ node_start = node.start_point[0]
104
+ node_end = node.end_point[0]
105
+ contains = (node_start <= ts_start) and (node_end >= ts_end)
106
+
107
+ matchers = [
108
+ _matches_simple_container_pattern(node, contains),
109
+ _matches_call_expression_pattern(node, ts_start, ts_end, contains),
110
+ _matches_declaration_pattern(node, contains),
111
+ _matches_jsx_pattern(node, contains),
112
+ _matches_class_body_pattern(node, ts_start, ts_end),
113
+ ]
114
+ return any(matchers)
115
+
116
+
117
+ def _matches_simple_container_pattern(node: Node, contains: bool) -> bool:
118
+ """Check if node is a simple container pattern (decorator, object, etc.)."""
119
+ simple_types = (
120
+ "decorator",
121
+ "object",
122
+ "member_expression",
123
+ "as_expression",
124
+ "array_pattern",
125
+ )
126
+ return node.type in simple_types and contains
127
+
128
+
129
+ def _matches_call_expression_pattern(
130
+ node: Node, ts_start: int, ts_end: int, contains: bool
131
+ ) -> bool:
132
+ """Check if node is a call expression pattern."""
133
+ if node.type != "call_expression":
134
+ return False
135
+
136
+ node_start = node.start_point[0]
137
+ node_end = node.end_point[0]
138
+ is_multiline = node_start < node_end
139
+ if is_multiline and node_start <= ts_start <= node_end:
140
+ return True
141
+
142
+ return contains
143
+
144
+
145
+ def _matches_declaration_pattern(node: Node, contains: bool) -> bool:
146
+ """Check if node is a lexical declaration pattern."""
147
+ if node.type != "lexical_declaration" or not contains:
148
+ return False
149
+
150
+ if _contains_function_body(node):
151
+ return False
152
+
153
+ return True
154
+
155
+
156
+ def _matches_jsx_pattern(node: Node, contains: bool) -> bool:
157
+ """Check if node is a JSX element pattern."""
158
+ jsx_types = ("jsx_opening_element", "jsx_self_closing_element")
159
+ return node.type in jsx_types and contains
160
+
161
+
162
+ def _matches_class_body_pattern(node: Node, ts_start: int, ts_end: int) -> bool:
163
+ """Check if node is a class body field definition pattern."""
164
+ if node.type != "class_body":
165
+ return False
166
+
167
+ return _is_in_class_field_area(node, ts_start, ts_end)
168
+
169
+
170
+ def _contains_function_body(node: Node) -> bool:
171
+ """Check if node contains an arrow function or function expression."""
172
+ for child in node.children:
173
+ if child.type in ("arrow_function", "function", "function_expression"):
174
+ return True
175
+ if _contains_function_body(child):
176
+ return True
177
+ return False
178
+
179
+
180
+ def _find_first_method_line(class_body: Node) -> int | None:
181
+ """Find line number of first method in class body."""
182
+ for child in class_body.children:
183
+ if child.type in ("method_definition", "function_declaration"):
184
+ return child.start_point[0]
185
+ return None
186
+
187
+
188
+ def _is_in_class_field_area(class_body: Node, ts_start: int, ts_end: int) -> bool:
189
+ """Check if range is in class field definition area (before methods)."""
190
+ first_method_line = _find_first_method_line(class_body)
191
+ class_start = class_body.start_point[0]
192
+ class_end = class_body.end_point[0]
193
+
194
+ if first_method_line is None:
195
+ return class_start <= ts_start and class_end >= ts_end
196
+
197
+ return class_start <= ts_start and ts_end < first_method_line
198
+
199
+
200
+ def _find_interface_ranges(content: str) -> list[tuple[int, int]]:
201
+ """Find line ranges of interface/type definitions."""
202
+ ranges: list[tuple[int, int]] = []
203
+ lines = content.split("\n")
204
+ state = {"in_interface": False, "start_line": 0, "brace_count": 0}
205
+
206
+ for i, line in enumerate(lines, start=1):
207
+ stripped = line.strip()
208
+ _process_line_for_interface(stripped, i, state, ranges)
209
+
210
+ return ranges
211
+
212
+
213
+ def _process_line_for_interface(
214
+ stripped: str, line_num: int, state: dict[str, Any], ranges: list[tuple[int, int]]
215
+ ) -> None:
216
+ """Process single line for interface detection."""
217
+ if _is_interface_start(stripped):
218
+ _handle_interface_start(stripped, line_num, state, ranges)
219
+ return
220
+
221
+ if state["in_interface"]:
222
+ _handle_interface_continuation(stripped, line_num, state, ranges)
223
+
224
+
225
+ def _is_interface_start(stripped: str) -> bool:
226
+ """Check if line starts interface/type definition."""
227
+ return stripped.startswith(("interface ", "type ")) and "{" in stripped
228
+
229
+
230
+ def _handle_interface_start(
231
+ stripped: str, line_num: int, state: dict[str, Any], ranges: list[tuple[int, int]]
232
+ ) -> None:
233
+ """Handle start of interface definition."""
234
+ state["in_interface"] = True
235
+ state["start_line"] = line_num
236
+ state["brace_count"] = stripped.count("{") - stripped.count("}")
237
+
238
+ if state["brace_count"] == 0:
239
+ ranges.append((line_num, line_num))
240
+ state["in_interface"] = False
241
+
242
+
243
+ def _handle_interface_continuation(
244
+ stripped: str, line_num: int, state: dict[str, Any], ranges: list[tuple[int, int]]
245
+ ) -> None:
246
+ """Handle continuation of interface definition."""
247
+ state["brace_count"] += stripped.count("{") - stripped.count("}")
248
+ if state["brace_count"] == 0:
249
+ ranges.append((state["start_line"], line_num))
250
+ state["in_interface"] = False
251
+
252
+
253
+ def _overlaps_interface(start: int, end: int, interface_ranges: list[tuple[int, int]]) -> bool:
254
+ """Check if block overlaps with any interface range."""
255
+ return any(start <= if_end and end >= if_start for if_start, if_end in interface_ranges)
@@ -0,0 +1,70 @@
1
+ """
2
+ Purpose: Extract value representations from TypeScript AST nodes
3
+
4
+ Scope: Helper for TypeScript constant extraction to extract value strings
5
+
6
+ Overview: Provides utility methods to extract string representations from tree-sitter AST nodes
7
+ for TypeScript value types (numbers, strings, booleans, arrays, objects, call expressions).
8
+ Used by TypeScriptConstantExtractor to get value context for duplicate constant detection.
9
+
10
+ Dependencies: tree-sitter, tree-sitter-typescript, src.analyzers.typescript_base
11
+
12
+ Exports: TypeScriptValueExtractor class
13
+
14
+ Interfaces: TypeScriptValueExtractor.get_value_string(node, content) -> str | None
15
+
16
+ Implementation: Tree-sitter node traversal with type-specific string formatting
17
+
18
+ Suppressions:
19
+ - type:ignore[assignment,misc]: Tree-sitter Node type alias (optional dependency fallback)
20
+ """
21
+
22
+ from contextlib import suppress
23
+ from typing import Any
24
+
25
+ from src.analyzers.typescript_base import TREE_SITTER_AVAILABLE
26
+
27
+ if TREE_SITTER_AVAILABLE:
28
+ from tree_sitter import Node
29
+ else:
30
+ Node = Any # type: ignore[assignment,misc]
31
+
32
+
33
+ class TypeScriptValueExtractor:
34
+ """Extracts value representations from TypeScript AST nodes."""
35
+
36
+ # Types that return their literal text
37
+ LITERAL_TYPES = frozenset(("number", "string", "true", "false", "null", "identifier"))
38
+
39
+ # Types with fixed representations
40
+ FIXED_REPRESENTATIONS = {"array": "[...]", "object": "{...}"}
41
+
42
+ def get_node_text(self, node: Node, content: str) -> str:
43
+ """Get text content of a node."""
44
+ return content[node.start_byte : node.end_byte]
45
+
46
+ def get_value_string(self, node: Node, content: str) -> str | None:
47
+ """Get string representation of a value node."""
48
+ if node.type in self.LITERAL_TYPES:
49
+ return self.get_node_text(node, content)
50
+ with suppress(KeyError):
51
+ return self.FIXED_REPRESENTATIONS[node.type]
52
+ if node.type == "call_expression":
53
+ return self._get_call_string(node, content)
54
+ return None
55
+
56
+ def _get_call_string(self, node: Node, content: str) -> str:
57
+ """Get string representation of a call expression.
58
+
59
+ Args:
60
+ node: call_expression node
61
+ content: Original source content
62
+
63
+ Returns:
64
+ String like "functionName(...)"
65
+ """
66
+ for child in node.children:
67
+ if child.type == "identifier":
68
+ func_name = self.get_node_text(child, content)
69
+ return f"{func_name}(...)"
70
+ return "call(...)"
@@ -27,6 +27,10 @@ from .cache import CodeBlock
27
27
  class DRYViolationBuilder:
28
28
  """Builds violation messages for duplicate code."""
29
29
 
30
+ def __init__(self) -> None:
31
+ """Initialize the DRY violation builder."""
32
+ pass # Stateless builder for duplicate code violations
33
+
30
34
  def build_violation(
31
35
  self, block: CodeBlock, all_duplicates: list[CodeBlock], rule_id: str
32
36
  ) -> Violation:
@@ -25,6 +25,10 @@ DEFAULT_FALLBACK_LINE_COUNT = 5
25
25
  class ViolationFilter:
26
26
  """Filters overlapping violations."""
27
27
 
28
+ def __init__(self) -> None:
29
+ """Initialize the violation filter."""
30
+ pass # Stateless filter for overlapping violations
31
+
28
32
  def filter_overlapping(self, sorted_violations: list[Violation]) -> list[Violation]:
29
33
  """Filter overlapping violations, keeping first occurrence.
30
34
 
@@ -50,10 +54,7 @@ class ViolationFilter:
50
54
  Returns:
51
55
  True if violation overlaps with any kept violation
52
56
  """
53
- for kept in kept_violations:
54
- if self._overlaps(violation, kept):
55
- return True
56
- return False
57
+ return any(self._overlaps(violation, kept) for kept in kept_violations)
57
58
 
58
59
  def _overlaps(self, v1: Violation, v2: Violation) -> bool:
59
60
  """Check if two violations overlap.
@@ -10,14 +10,16 @@ Overview: Handles violation generation for duplicate code blocks. Queries storag
10
10
 
11
11
  Dependencies: DuplicateStorage, ViolationDeduplicator, DRYViolationBuilder, Violation, DRYConfig
12
12
 
13
- Exports: ViolationGenerator class
13
+ Exports: ViolationGenerator class, IgnoreContext dataclass
14
14
 
15
15
  Interfaces: ViolationGenerator.generate_violations(storage, rule_id, config) -> list[Violation]
16
16
 
17
17
  Implementation: Queries storage, deduplicates blocks, builds violations, filters by ignore patterns
18
18
  """
19
19
 
20
+ from dataclasses import dataclass
20
21
  from pathlib import Path
22
+ from typing import TYPE_CHECKING
21
23
 
22
24
  from src.core.types import Violation
23
25
  from src.orchestrator.language_detector import detect_language
@@ -28,6 +30,18 @@ from .duplicate_storage import DuplicateStorage
28
30
  from .inline_ignore import InlineIgnoreParser
29
31
  from .violation_builder import DRYViolationBuilder
30
32
 
33
+ if TYPE_CHECKING:
34
+ from src.linter_config.ignore import IgnoreDirectiveParser
35
+
36
+
37
+ @dataclass
38
+ class IgnoreContext:
39
+ """Context for ignore directive filtering."""
40
+
41
+ inline_ignore: InlineIgnoreParser
42
+ shared_parser: "IgnoreDirectiveParser | None" = None
43
+ file_contents: dict[str, str] | None = None
44
+
31
45
 
32
46
  class ViolationGenerator:
33
47
  """Generates violations from duplicate code blocks."""
@@ -42,7 +56,7 @@ class ViolationGenerator:
42
56
  storage: DuplicateStorage,
43
57
  rule_id: str,
44
58
  config: DRYConfig,
45
- inline_ignore: InlineIgnoreParser,
59
+ ignore_ctx: IgnoreContext,
46
60
  ) -> list[Violation]:
47
61
  """Generate violations from storage.
48
62
 
@@ -50,19 +64,42 @@ class ViolationGenerator:
50
64
  storage: Duplicate storage instance
51
65
  rule_id: Rule identifier for violations
52
66
  config: DRY configuration with ignore patterns
53
- inline_ignore: Parser with inline ignore directives
67
+ ignore_ctx: Context containing ignore parsers and file contents
54
68
 
55
69
  Returns:
56
70
  List of violations filtered by ignore patterns and inline directives
57
71
  """
58
- duplicate_hashes = storage.get_duplicate_hashes()
59
- violations = []
72
+ raw_violations = self._collect_violations(storage, rule_id, config)
73
+ deduplicated = self._deduplicator.deduplicate_violations(raw_violations)
74
+ pattern_filtered = self._filter_ignored(deduplicated, config.ignore_patterns)
75
+ inline_filtered = self._filter_inline_ignored(pattern_filtered, ignore_ctx.inline_ignore)
76
+
77
+ # Apply shared ignore directive filtering for block and line directives
78
+ if ignore_ctx.shared_parser and ignore_ctx.file_contents:
79
+ return self._filter_shared_ignored(
80
+ inline_filtered, ignore_ctx.shared_parser, ignore_ctx.file_contents
81
+ )
82
+
83
+ return inline_filtered
60
84
 
61
- for hash_value in duplicate_hashes:
85
+ def _collect_violations(
86
+ self, storage: DuplicateStorage, rule_id: str, config: DRYConfig
87
+ ) -> list[Violation]:
88
+ """Collect raw violations from storage duplicate hashes.
89
+
90
+ Args:
91
+ storage: Duplicate storage instance
92
+ rule_id: Rule identifier for violations
93
+ config: DRY configuration
94
+
95
+ Returns:
96
+ List of raw violations before filtering
97
+ """
98
+ violations = []
99
+ for hash_value in storage.duplicate_hashes:
62
100
  blocks = storage.get_blocks_for_hash(hash_value)
63
101
  dedup_blocks = self._deduplicator.deduplicate_blocks(blocks)
64
102
 
65
- # Check min_occurrences threshold (language-aware)
66
103
  if not self._meets_min_occurrences(dedup_blocks, config):
67
104
  continue
68
105
 
@@ -70,9 +107,7 @@ class ViolationGenerator:
70
107
  violation = self._violation_builder.build_violation(block, dedup_blocks, rule_id)
71
108
  violations.append(violation)
72
109
 
73
- deduplicated = self._deduplicator.deduplicate_violations(violations)
74
- pattern_filtered = self._filter_ignored(deduplicated, config.ignore_patterns)
75
- return self._filter_inline_ignored(pattern_filtered, inline_ignore)
110
+ return violations
76
111
 
77
112
  def _meets_min_occurrences(self, blocks: list, config: DRYConfig) -> bool:
78
113
  """Check if blocks meet minimum occurrence threshold for the language.
@@ -128,10 +163,7 @@ class ViolationGenerator:
128
163
  True if file should be ignored
129
164
  """
130
165
  path_str = str(Path(file_path))
131
- for pattern in ignore_patterns:
132
- if pattern in path_str:
133
- return True
134
- return False
166
+ return any(pattern in path_str for pattern in ignore_patterns)
135
167
 
136
168
  def _filter_inline_ignored(
137
169
  self, violations: list[Violation], inline_ignore: InlineIgnoreParser
@@ -172,3 +204,28 @@ class ViolationGenerator:
172
204
  return int(message[start:end])
173
205
  except (ValueError, IndexError):
174
206
  return 1
207
+
208
+ def _filter_shared_ignored(
209
+ self,
210
+ violations: list[Violation],
211
+ ignore_parser: "IgnoreDirectiveParser",
212
+ file_contents: dict[str, str],
213
+ ) -> list[Violation]:
214
+ """Filter violations using the shared ignore directive parser.
215
+
216
+ This enables standard # thailint: ignore-start/end directives for DRY linter.
217
+
218
+ Args:
219
+ violations: List of violations to filter
220
+ ignore_parser: Shared ignore directive parser
221
+ file_contents: Cached file contents for ignore checking
222
+
223
+ Returns:
224
+ Filtered list of violations
225
+ """
226
+ filtered = []
227
+ for violation in violations:
228
+ file_content = file_contents.get(violation.file_path, "")
229
+ if not ignore_parser.should_ignore_violation(violation, file_content):
230
+ filtered.append(violation)
231
+ return filtered
@@ -1,64 +1,82 @@
1
1
  """
2
- File: src/linters/file_header/atemporal_detector.py
3
2
  Purpose: Detects temporal language patterns in file headers
4
- Exports: AtemporalDetector class
5
- Depends: re module for regex matching
6
- Implements: Regex-based pattern matching with configurable patterns
7
- Related: linter.py for detector usage, violation_builder.py for violation creation
8
3
 
9
- Overview:
10
- Implements pattern-based detection of temporal language that violates atemporal
4
+ Scope: File header validation for atemporal language compliance
5
+
6
+ Overview: Implements pattern-based detection of temporal language that violates atemporal
11
7
  documentation requirements. Detects dates, temporal qualifiers, state change language,
12
8
  and future references using regex patterns. Provides violation details for each pattern match.
9
+ Uses four pattern categories (dates, temporal qualifiers, state changes, future references)
10
+ to identify violations and returns detailed information for each match.
11
+
12
+ Dependencies: re module for regex-based pattern matching
13
+
14
+ Exports: AtemporalDetector class with detect_violations method
13
15
 
14
- Usage:
15
- detector = AtemporalDetector()
16
- violations = detector.detect_violations(header_text)
16
+ Interfaces: detect_violations(text) -> list[tuple[str, str, int]] returns pattern matches with line numbers
17
17
 
18
- Notes: Four pattern categories - dates, temporal qualifiers, state changes, future references
18
+ Implementation: Regex-based pattern matching with pre-compiled patterns organized by category
19
+
20
+ Suppressions:
21
+ - nesting: detect_violations iterates over pattern categories and their patterns.
22
+ Natural grouping by category requires nested loops.
19
23
  """
20
24
 
21
25
  import re
26
+ from re import Pattern
27
+
28
+
29
+ def _compile_patterns(patterns: list[tuple[str, str]]) -> list[tuple[Pattern[str], str]]:
30
+ """Compile regex patterns for efficient reuse."""
31
+ return [(re.compile(pattern, re.IGNORECASE), desc) for pattern, desc in patterns]
22
32
 
23
33
 
24
34
  class AtemporalDetector:
25
35
  """Detects temporal language patterns in text."""
26
36
 
27
- # Date patterns
28
- DATE_PATTERNS = [
29
- (r"\d{4}-\d{2}-\d{2}", "ISO date format (YYYY-MM-DD)"),
30
- (
31
- r"(?:January|February|March|April|May|June|July|August|September|October|November|December)\s+\d{4}",
32
- "Month Year format",
33
- ),
34
- (r"(?:Created|Updated|Modified):\s*\d{4}", "Date metadata"),
35
- ]
36
-
37
- # Temporal qualifiers
38
- TEMPORAL_QUALIFIERS = [
39
- (r"\bcurrently\b", 'temporal qualifier "currently"'),
40
- (r"\bnow\b", 'temporal qualifier "now"'),
41
- (r"\brecently\b", 'temporal qualifier "recently"'),
42
- (r"\bsoon\b", 'temporal qualifier "soon"'),
43
- (r"\bfor now\b", 'temporal qualifier "for now"'),
44
- ]
45
-
46
- # State change language
47
- STATE_CHANGE = [
48
- (r"\breplaces?\b", 'state change "replaces"'),
49
- (r"\bmigrated from\b", 'state change "migrated from"'),
50
- (r"\bformerly\b", 'state change "formerly"'),
51
- (r"\bold implementation\b", 'state change "old"'),
52
- (r"\bnew implementation\b", 'state change "new"'),
53
- ]
54
-
55
- # Future references
56
- FUTURE_REFS = [
57
- (r"\bwill be\b", 'future reference "will be"'),
58
- (r"\bplanned\b", 'future reference "planned"'),
59
- (r"\bto be added\b", 'future reference "to be added"'),
60
- (r"\bcoming soon\b", 'future reference "coming soon"'),
61
- ]
37
+ # Pre-compiled date patterns
38
+ DATE_PATTERNS = _compile_patterns(
39
+ [
40
+ (r"\d{4}-\d{2}-\d{2}", "ISO date format (YYYY-MM-DD)"),
41
+ (
42
+ r"(?:January|February|March|April|May|June|July|August|September|October|November|December)\s+\d{4}",
43
+ "Month Year format",
44
+ ),
45
+ (r"(?:Created|Updated|Modified):\s*\d{4}", "Date metadata"),
46
+ ]
47
+ )
48
+
49
+ # Pre-compiled temporal qualifiers
50
+ TEMPORAL_QUALIFIERS = _compile_patterns(
51
+ [
52
+ (r"\bcurrently\b", 'temporal qualifier "currently"'),
53
+ (r"\bnow\b", 'temporal qualifier "now"'),
54
+ (r"\brecently\b", 'temporal qualifier "recently"'),
55
+ (r"\bsoon\b", 'temporal qualifier "soon"'),
56
+ (r"\bfor now\b", 'temporal qualifier "for now"'),
57
+ ]
58
+ )
59
+
60
+ # Pre-compiled state change language
61
+ STATE_CHANGE = _compile_patterns(
62
+ [
63
+ (r"\breplaces?\b", 'state change "replaces"'),
64
+ (r"\bmigrated from\b", 'state change "migrated from"'),
65
+ (r"\bformerly\b", 'state change "formerly"'),
66
+ (r"\bold implementation\b", 'state change "old"'),
67
+ (r"\bnew implementation\b", 'state change "new"'),
68
+ ]
69
+ )
70
+
71
+ # Pre-compiled future references
72
+ FUTURE_REFS = _compile_patterns(
73
+ [
74
+ (r"\bwill be\b", 'future reference "will be"'),
75
+ (r"\bplanned\b", 'future reference "planned"'),
76
+ (r"\bto be added\b", 'future reference "to be added"'),
77
+ (r"\bcoming soon\b", 'future reference "coming soon"'),
78
+ ]
79
+ )
62
80
 
63
81
  def detect_violations( # thailint: ignore[nesting]
64
82
  self, text: str
@@ -73,15 +91,15 @@ class AtemporalDetector:
73
91
  """
74
92
  violations = []
75
93
 
76
- # Check all pattern categories
94
+ # Check all pattern categories (patterns are pre-compiled)
77
95
  all_patterns = (
78
96
  self.DATE_PATTERNS + self.TEMPORAL_QUALIFIERS + self.STATE_CHANGE + self.FUTURE_REFS
79
97
  )
80
98
 
81
99
  lines = text.split("\n")
82
100
  for line_num, line in enumerate(lines, start=1):
83
- for pattern, description in all_patterns:
84
- if re.search(pattern, line, re.IGNORECASE):
85
- violations.append((pattern, description, line_num))
101
+ for compiled_pattern, description in all_patterns:
102
+ if compiled_pattern.search(line):
103
+ violations.append((compiled_pattern.pattern, description, line_num))
86
104
 
87
105
  return violations