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,139 @@
1
+ """
2
+ Purpose: Regex compilation in loop linter rule implementation
3
+
4
+ Scope: RegexInLoopRule class implementing MultiLanguageLintRule interface
5
+
6
+ Overview: Implements regex-in-loop linter rule following MultiLanguageLintRule interface.
7
+ Orchestrates configuration loading, Python analysis, and violation building through
8
+ focused helper classes. Detects repeated regex compilation patterns using re.method()
9
+ calls in loops instead of pre-compiled patterns. Supports configurable enabled flag
10
+ and ignore directives. Main rule class acts as coordinator for regex detection workflow.
11
+
12
+ Dependencies: MultiLanguageLintRule, BaseLintContext, PythonRegexInLoopAnalyzer,
13
+ PerformanceViolationBuilder
14
+
15
+ Exports: RegexInLoopRule class
16
+
17
+ Interfaces: RegexInLoopRule.check(context) -> list[Violation], properties for rule metadata
18
+
19
+ Implementation: Composition pattern with analyzer classes, AST-based analysis
20
+
21
+ """
22
+
23
+ from typing import Any
24
+
25
+ from src.core.base import BaseLintContext, MultiLanguageLintRule
26
+ from src.core.linter_utils import load_linter_config, with_parsed_python
27
+ from src.core.types import Violation
28
+ from src.linter_config.ignore import get_ignore_parser
29
+
30
+ from .config import PerformanceConfig
31
+ from .regex_analyzer import PythonRegexInLoopAnalyzer
32
+ from .violation_builder import PerformanceViolationBuilder
33
+
34
+
35
+ class RegexInLoopRule(MultiLanguageLintRule):
36
+ """Detects regex compilation in loops."""
37
+
38
+ def __init__(self) -> None:
39
+ """Initialize the regex in loop rule."""
40
+ self._ignore_parser = get_ignore_parser()
41
+ self._violation_builder = PerformanceViolationBuilder(self.rule_id)
42
+ self._python_analyzer = PythonRegexInLoopAnalyzer()
43
+
44
+ @property
45
+ def rule_id(self) -> str:
46
+ """Unique identifier for this rule."""
47
+ return "performance.regex-in-loop"
48
+
49
+ @property
50
+ def rule_name(self) -> str:
51
+ """Human-readable name for this rule."""
52
+ return "Regex Compilation in Loop"
53
+
54
+ @property
55
+ def description(self) -> str:
56
+ """Description of what this rule checks."""
57
+ return "re.method() in loops recompiles pattern each iteration; use re.compile() instead"
58
+
59
+ def _load_config(self, context: BaseLintContext) -> PerformanceConfig:
60
+ """Load configuration from context.
61
+
62
+ Args:
63
+ context: Lint context
64
+
65
+ Returns:
66
+ PerformanceConfig instance
67
+ """
68
+ return load_linter_config(context, "performance", PerformanceConfig)
69
+
70
+ def _check_python(self, context: BaseLintContext, config: PerformanceConfig) -> list[Violation]:
71
+ """Check Python code for regex compilation in loops.
72
+
73
+ Args:
74
+ context: Lint context with Python file information
75
+ config: Performance configuration
76
+
77
+ Returns:
78
+ List of violations found in Python code
79
+ """
80
+ return with_parsed_python(
81
+ context,
82
+ self._violation_builder,
83
+ lambda tree: self._analyze_python_regex(tree, context),
84
+ )
85
+
86
+ def _analyze_python_regex(self, tree: Any, context: BaseLintContext) -> list[Violation]:
87
+ """Analyze parsed Python AST for regex in loops."""
88
+ violations_raw = self._python_analyzer.find_violations(tree)
89
+ return self._build_violations(violations_raw, context)
90
+
91
+ def _check_typescript(
92
+ self, context: BaseLintContext, config: PerformanceConfig
93
+ ) -> list[Violation]:
94
+ """Check TypeScript code for regex compilation in loops.
95
+
96
+ Args:
97
+ context: Lint context with TypeScript file information
98
+ config: Performance configuration
99
+
100
+ Returns:
101
+ Empty list - TypeScript regex handling is different from Python
102
+ """
103
+ # TypeScript uses RegExp objects differently, not implemented
104
+ return []
105
+
106
+ def _build_violations(self, raw_violations: list, context: BaseLintContext) -> list[Violation]:
107
+ """Build Violation objects from analyzer results.
108
+
109
+ Args:
110
+ raw_violations: List of RegexInLoopViolation dataclass instances
111
+ context: Lint context
112
+
113
+ Returns:
114
+ List of Violation objects
115
+ """
116
+ violations = []
117
+ for v in raw_violations:
118
+ violation = self._violation_builder.create_regex_in_loop_violation(
119
+ method_name=v.method_name,
120
+ line_number=v.line_number,
121
+ column=v.column,
122
+ loop_type=v.loop_type,
123
+ context=context,
124
+ )
125
+ if not self._should_ignore(violation, context):
126
+ violations.append(violation)
127
+ return violations
128
+
129
+ def _should_ignore(self, violation: Violation, context: BaseLintContext) -> bool:
130
+ """Check if violation should be ignored based on inline directives.
131
+
132
+ Args:
133
+ violation: Violation to check
134
+ context: Lint context with file content
135
+
136
+ Returns:
137
+ True if violation should be ignored
138
+ """
139
+ return self._ignore_parser.should_ignore_violation(violation, context.file_content or "")
@@ -0,0 +1,236 @@
1
+ """
2
+ Purpose: TypeScript tree-sitter based string concatenation in loop detector
3
+
4
+ Scope: Detect O(n²) string building patterns using += in TypeScript loops
5
+
6
+ Overview: Analyzes TypeScript code to detect string concatenation inside loops using tree-sitter.
7
+ Implements heuristic-based detection using variable naming patterns and initialization values.
8
+ Detects `result += item` patterns inside for/while/do loops that indicate O(n²) complexity.
9
+ Provides suggestions for using join() or array-based building instead.
10
+
11
+ Dependencies: TypeScriptBaseAnalyzer, tree-sitter, tree-sitter-typescript, constants module
12
+
13
+ Exports: TypeScriptStringConcatAnalyzer class with find_violations method
14
+
15
+ Interfaces: find_violations(root_node) -> list[dict] with violation info
16
+
17
+ Implementation: Tree-sitter traversal detecting augmented assignments in loop contexts
18
+
19
+ Suppressions:
20
+ - srp.violation: Class uses many small methods to achieve A-grade cyclomatic complexity.
21
+ This is an intentional tradeoff - low complexity is prioritized over strict SRP adherence.
22
+ """
23
+
24
+ from dataclasses import dataclass
25
+
26
+ from src.analyzers.typescript_base import (
27
+ TREE_SITTER_AVAILABLE,
28
+ Node,
29
+ TypeScriptBaseAnalyzer,
30
+ )
31
+
32
+ from .constants import LOOP_NODE_TYPES_TS, STRING_VARIABLE_PATTERNS
33
+
34
+
35
+ @dataclass
36
+ class StringConcatViolation:
37
+ """Represents a string concatenation violation found in code."""
38
+
39
+ variable_name: str
40
+ line_number: int
41
+ column: int
42
+ loop_type: str # 'for', 'for_in', 'while', 'do'
43
+
44
+
45
+ # thailint: ignore-next-line[srp.violation] Uses small focused methods to reduce complexity
46
+ class TypeScriptStringConcatAnalyzer(TypeScriptBaseAnalyzer):
47
+ """Detects string concatenation in loops for TypeScript code."""
48
+
49
+ def __init__(self) -> None:
50
+ """Initialize the analyzer."""
51
+ super().__init__()
52
+ self._string_variables: set[str] = set()
53
+
54
+ def find_violations(self, root_node: Node) -> list[StringConcatViolation]:
55
+ """Find all string concatenation in loop violations.
56
+
57
+ Args:
58
+ root_node: Tree-sitter AST root node
59
+
60
+ Returns:
61
+ List of violations found
62
+ """
63
+ if not TREE_SITTER_AVAILABLE or root_node is None:
64
+ return []
65
+
66
+ violations: list[StringConcatViolation] = []
67
+ self._string_variables = set()
68
+
69
+ # First pass: identify variables initialized as strings
70
+ self._identify_string_variables(root_node)
71
+
72
+ # Second pass: find += in loops
73
+ self._find_concat_in_loops(root_node, violations, None)
74
+
75
+ return violations
76
+
77
+ def _identify_string_variables(self, node: Node) -> None:
78
+ """Identify variables that are initialized as strings.
79
+
80
+ Args:
81
+ node: AST node to analyze
82
+ """
83
+ self._check_variable_declarator(node)
84
+ for child in node.children:
85
+ self._identify_string_variables(child)
86
+
87
+ def _check_variable_declarator(self, node: Node) -> None:
88
+ """Check if a variable_declarator node initializes a string variable."""
89
+ if node.type != "variable_declarator":
90
+ return
91
+ name_node = self.find_child_by_type(node, "identifier")
92
+ value_node = self._find_string_value(node)
93
+ if name_node and value_node:
94
+ self._string_variables.add(self.extract_node_text(name_node))
95
+
96
+ def _find_string_value(self, node: Node) -> Node | None:
97
+ """Find a string or template_string child node."""
98
+ for child in node.children:
99
+ if child.type in ("string", "template_string"):
100
+ return child
101
+ return None
102
+
103
+ def _find_concat_in_loops(
104
+ self, node: Node, violations: list[StringConcatViolation], loop_type: str | None
105
+ ) -> None:
106
+ """Recursively find string concatenation in loops.
107
+
108
+ Args:
109
+ node: Current AST node
110
+ violations: List to append violations to
111
+ loop_type: Type of enclosing loop, None if not in loop
112
+ """
113
+ # Track loop entry
114
+ current_loop = loop_type
115
+ if node.type in LOOP_NODE_TYPES_TS:
116
+ current_loop = node.type.replace("_statement", "").replace("_", "_")
117
+
118
+ # Check for augmented assignment (+=)
119
+ if node.type == "augmented_assignment_expression" and current_loop:
120
+ self._check_augmented_assignment(node, violations, current_loop)
121
+
122
+ # Recurse into children
123
+ for child in node.children:
124
+ self._find_concat_in_loops(child, violations, current_loop)
125
+
126
+ def _check_augmented_assignment(
127
+ self, node: Node, violations: list[StringConcatViolation], loop_type: str
128
+ ) -> None:
129
+ """Check if an augmented assignment is string concatenation.
130
+
131
+ Args:
132
+ node: Augmented assignment node
133
+ violations: List to append violations to
134
+ loop_type: Type of enclosing loop
135
+ """
136
+ if not self._is_plus_equals(node):
137
+ return
138
+ var_name = self._get_var_name(node)
139
+ if not var_name:
140
+ return
141
+ value_node = self._get_value_node(node)
142
+ if self._is_likely_string_variable(var_name, value_node):
143
+ self._create_violation(node, var_name, loop_type, violations)
144
+
145
+ def _is_plus_equals(self, node: Node) -> bool:
146
+ """Check if node has a += operator."""
147
+ return any(child.type == "+=" for child in node.children)
148
+
149
+ def _get_var_name(self, node: Node) -> str | None:
150
+ """Get the variable name from an augmented assignment."""
151
+ for child in node.children:
152
+ if child.type == "identifier":
153
+ return self.extract_node_text(child)
154
+ return None
155
+
156
+ def _get_value_node(self, node: Node) -> Node | None:
157
+ """Get the value node from an augmented assignment."""
158
+ children = node.children
159
+ operator_idx = self._find_plus_equals_index(children)
160
+ if operator_idx < 0:
161
+ return None
162
+ return self._find_value_after_operator(children, operator_idx)
163
+
164
+ def _find_plus_equals_index(self, children: list[Node]) -> int:
165
+ """Find the index of the += operator in children."""
166
+ for i, child in enumerate(children):
167
+ if child.type == "+=":
168
+ return i
169
+ return -1
170
+
171
+ def _find_value_after_operator(self, children: list[Node], operator_idx: int) -> Node | None:
172
+ """Find the first non-identifier value after the operator."""
173
+ for child in children[operator_idx + 1 :]:
174
+ if child.type != "identifier":
175
+ return child
176
+ return None
177
+
178
+ def _create_violation(
179
+ self, node: Node, var_name: str, loop_type: str, violations: list[StringConcatViolation]
180
+ ) -> None:
181
+ """Create and append a string concat violation."""
182
+ violations.append(
183
+ StringConcatViolation(
184
+ variable_name=var_name,
185
+ line_number=node.start_point[0] + 1,
186
+ column=node.start_point[1],
187
+ loop_type=loop_type,
188
+ )
189
+ )
190
+
191
+ def _is_likely_string_variable(self, var_name: str, value_node: Node | None) -> bool:
192
+ """Determine if a variable is likely a string being concatenated.
193
+
194
+ Args:
195
+ var_name: Variable name
196
+ value_node: Value being added (may be None)
197
+
198
+ Returns:
199
+ True if this is likely string concatenation
200
+ """
201
+ return self._is_known_string_var(var_name) or self._is_string_value_node(value_node)
202
+
203
+ def _is_known_string_var(self, var_name: str) -> bool:
204
+ """Check if variable is known or named like a string."""
205
+ return var_name in self._string_variables or var_name.lower() in STRING_VARIABLE_PATTERNS
206
+
207
+ def _is_string_value_node(self, value_node: Node | None) -> bool:
208
+ """Check if value node is a string or contains a string."""
209
+ if not value_node:
210
+ return False
211
+ if value_node.type in ("string", "template_string"):
212
+ return True
213
+ if value_node.type == "binary_expression":
214
+ return any(child.type in ("string", "template_string") for child in value_node.children)
215
+ return False
216
+
217
+ def deduplicate_violations(
218
+ self, violations: list[StringConcatViolation]
219
+ ) -> list[StringConcatViolation]:
220
+ """Deduplicate violations to report one per variable.
221
+
222
+ Args:
223
+ violations: List of all violations found
224
+
225
+ Returns:
226
+ Deduplicated list with one violation per variable
227
+ """
228
+ seen: set[str] = set()
229
+ result: list[StringConcatViolation] = []
230
+
231
+ for v in violations:
232
+ if v.variable_name not in seen:
233
+ seen.add(v.variable_name)
234
+ result.append(v)
235
+
236
+ return result
@@ -0,0 +1,160 @@
1
+ """
2
+ Purpose: Violation creation for performance linter rules
3
+
4
+ Scope: Builds Violation objects for string-concat-loop and regex-in-loop rules
5
+
6
+ Overview: Provides violation building functionality for the performance linter. Creates
7
+ violations for string concatenation in loops with contextual error messages,
8
+ variable names, and actionable refactoring suggestions (use join(), list comprehension).
9
+ Handles syntax errors gracefully. Isolates violation construction from analysis logic.
10
+
11
+ Dependencies: BaseLintContext, Violation, Severity, PerformanceConfig, src.core.violation_builder
12
+
13
+ Exports: PerformanceViolationBuilder
14
+
15
+ Interfaces: create_string_concat_violation, create_syntax_error_violation
16
+
17
+ Implementation: Formats messages with variable names, provides targeted refactoring suggestions,
18
+ extends BaseViolationBuilder for consistent violation construction
19
+
20
+ Suppressions:
21
+ - too-many-arguments,too-many-positional-arguments: Violation builder methods inherently
22
+ require multiple parameters (variable_name, line, column, loop_type, context)
23
+ """
24
+
25
+ from src.core.base import BaseLintContext
26
+ from src.core.types import Severity, Violation
27
+ from src.core.violation_builder import BaseViolationBuilder
28
+
29
+
30
+ class PerformanceViolationBuilder(BaseViolationBuilder):
31
+ """Builds violations for performance issues."""
32
+
33
+ def __init__(self, rule_id: str):
34
+ """Initialize violation builder.
35
+
36
+ Args:
37
+ rule_id: Rule identifier for violations
38
+ """
39
+ self.rule_id = rule_id
40
+
41
+ def create_syntax_error_violation(
42
+ self, error: SyntaxError, context: BaseLintContext
43
+ ) -> Violation:
44
+ """Create violation for syntax error.
45
+
46
+ Args:
47
+ error: SyntaxError exception
48
+ context: Lint context
49
+
50
+ Returns:
51
+ Syntax error violation
52
+ """
53
+ return self.build_from_params(
54
+ rule_id=self.rule_id,
55
+ file_path=str(context.file_path or ""),
56
+ line=error.lineno or 0,
57
+ column=error.offset or 0,
58
+ message=f"Syntax error: {error.msg}",
59
+ severity=Severity.ERROR,
60
+ suggestion="Fix syntax errors before checking for performance issues",
61
+ )
62
+
63
+ def create_string_concat_violation( # pylint: disable=too-many-arguments,too-many-positional-arguments
64
+ self,
65
+ variable_name: str,
66
+ line_number: int,
67
+ column: int,
68
+ loop_type: str,
69
+ context: BaseLintContext,
70
+ ) -> Violation:
71
+ """Create violation for string concatenation in loop.
72
+
73
+ Args:
74
+ variable_name: Name of the variable being concatenated
75
+ line_number: Line number of the violation
76
+ column: Column number of the violation
77
+ loop_type: Type of loop ('for' or 'while')
78
+ context: Lint context
79
+
80
+ Returns:
81
+ String concat in loop violation
82
+ """
83
+ return self.build_from_params(
84
+ rule_id=self.rule_id,
85
+ file_path=str(context.file_path or ""),
86
+ line=line_number,
87
+ column=column,
88
+ message=(
89
+ f"String concatenation in {loop_type} loop: '{variable_name} +=' "
90
+ f"creates O(n²) complexity"
91
+ ),
92
+ severity=Severity.ERROR,
93
+ suggestion=self._generate_suggestion(variable_name),
94
+ )
95
+
96
+ def _generate_suggestion(self, variable_name: str) -> str:
97
+ """Generate refactoring suggestion for string concatenation.
98
+
99
+ Args:
100
+ variable_name: Variable being concatenated
101
+
102
+ Returns:
103
+ Suggestion string with refactoring advice
104
+ """
105
+ return (
106
+ f"Use ''.join() with a list comprehension or generator instead of "
107
+ f"repeatedly concatenating to '{variable_name}'. "
108
+ f"Example: {variable_name} = ''.join(items) or "
109
+ f"{variable_name} = ''.join(str(x) for x in items)"
110
+ )
111
+
112
+ def create_regex_in_loop_violation( # pylint: disable=too-many-arguments,too-many-positional-arguments
113
+ self,
114
+ method_name: str,
115
+ line_number: int,
116
+ column: int,
117
+ loop_type: str,
118
+ context: BaseLintContext,
119
+ ) -> Violation:
120
+ """Create violation for regex function call in loop.
121
+
122
+ Args:
123
+ method_name: Name of the regex method (e.g., 're.match', 'search')
124
+ line_number: Line number of the violation
125
+ column: Column number of the violation
126
+ loop_type: Type of loop ('for' or 'while')
127
+ context: Lint context
128
+
129
+ Returns:
130
+ Regex in loop violation
131
+ """
132
+ return self.build_from_params(
133
+ rule_id=self.rule_id,
134
+ file_path=str(context.file_path or ""),
135
+ line=line_number,
136
+ column=column,
137
+ message=(
138
+ f"Regex compilation in {loop_type} loop: '{method_name}()' "
139
+ f"recompiles pattern on each iteration"
140
+ ),
141
+ severity=Severity.ERROR,
142
+ suggestion=self._generate_regex_suggestion(method_name),
143
+ )
144
+
145
+ def _generate_regex_suggestion(self, method_name: str) -> str:
146
+ """Generate refactoring suggestion for regex in loop.
147
+
148
+ Args:
149
+ method_name: Regex method being called
150
+
151
+ Returns:
152
+ Suggestion string with refactoring advice
153
+ """
154
+ # Extract just the method name if prefixed with 're.'
155
+ base_method = method_name.split(".")[-1]
156
+ return (
157
+ f"Compile the pattern once outside the loop using re.compile(), "
158
+ f"then use pattern.{base_method}() inside the loop. "
159
+ f"Example: pattern = re.compile(r'...'); pattern.{base_method}(text)"
160
+ )
@@ -1,16 +1,7 @@
1
1
  """
2
- File: src/linters/print_statements/config.py
3
-
4
2
  Purpose: Configuration schema for print statements linter
5
3
 
6
- Exports: PrintStatementConfig dataclass
7
-
8
- Depends: dataclasses, typing
9
-
10
- Implements: PrintStatementConfig(enabled, ignore, allow_in_scripts, console_methods),
11
- from_dict class method for loading configuration from dictionary
12
-
13
- Related: src/linters/magic_numbers/config.py, src/core/types.py
4
+ Scope: Print statements linter configuration for all supported languages
14
5
 
15
6
  Overview: Defines configuration schema for print statements linter. Provides PrintStatementConfig
16
7
  dataclass with enabled flag, ignore patterns list, allow_in_scripts setting (default True to
@@ -19,9 +10,13 @@ Overview: Defines configuration schema for print statements linter. Provides Pri
19
10
  per-directory config overrides through from_dict class method. Integrates with orchestrator's
20
11
  configuration system to allow users to customize detection via .thailint.yaml configuration.
21
12
 
22
- Usage: config = PrintStatementConfig.from_dict(yaml_config, language="python")
13
+ Dependencies: dataclasses module for configuration structure, typing module for type hints
14
+
15
+ Exports: PrintStatementConfig dataclass
16
+
17
+ Interfaces: from_dict(config, language) -> PrintStatementConfig for configuration loading from dictionary
23
18
 
24
- Notes: Dataclass with defaults matching common use cases, language-specific override support
19
+ Implementation: Dataclass with defaults matching common use cases and language-specific override support
25
20
  """
26
21
 
27
22
  from dataclasses import dataclass, field