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.
- src/__init__.py +1 -0
- src/analyzers/__init__.py +4 -3
- src/analyzers/ast_utils.py +54 -0
- src/analyzers/rust_base.py +155 -0
- src/analyzers/rust_context.py +141 -0
- src/analyzers/typescript_base.py +4 -0
- src/cli/__init__.py +30 -0
- src/cli/__main__.py +22 -0
- src/cli/config.py +480 -0
- src/cli/config_merge.py +241 -0
- src/cli/linters/__init__.py +67 -0
- src/cli/linters/code_patterns.py +270 -0
- src/cli/linters/code_smells.py +342 -0
- src/cli/linters/documentation.py +83 -0
- src/cli/linters/performance.py +287 -0
- src/cli/linters/shared.py +331 -0
- src/cli/linters/structure.py +327 -0
- src/cli/linters/structure_quality.py +328 -0
- src/cli/main.py +120 -0
- src/cli/utils.py +395 -0
- src/cli_main.py +37 -0
- src/config.py +38 -25
- src/core/base.py +7 -2
- src/core/cli_utils.py +19 -2
- src/core/config_parser.py +5 -2
- src/core/constants.py +54 -0
- src/core/linter_utils.py +95 -6
- src/core/python_lint_rule.py +101 -0
- src/core/registry.py +1 -1
- src/core/rule_discovery.py +147 -84
- src/core/types.py +13 -0
- src/core/violation_builder.py +78 -15
- src/core/violation_utils.py +69 -0
- src/formatters/__init__.py +22 -0
- src/formatters/sarif.py +202 -0
- src/linter_config/directive_markers.py +109 -0
- src/linter_config/ignore.py +254 -395
- src/linter_config/loader.py +45 -12
- src/linter_config/pattern_utils.py +65 -0
- src/linter_config/rule_matcher.py +89 -0
- src/linters/collection_pipeline/__init__.py +90 -0
- src/linters/collection_pipeline/any_all_analyzer.py +281 -0
- src/linters/collection_pipeline/ast_utils.py +40 -0
- src/linters/collection_pipeline/config.py +75 -0
- src/linters/collection_pipeline/continue_analyzer.py +94 -0
- src/linters/collection_pipeline/detector.py +360 -0
- src/linters/collection_pipeline/filter_map_analyzer.py +402 -0
- src/linters/collection_pipeline/linter.py +420 -0
- src/linters/collection_pipeline/suggestion_builder.py +130 -0
- src/linters/cqs/__init__.py +54 -0
- src/linters/cqs/config.py +55 -0
- src/linters/cqs/function_analyzer.py +201 -0
- src/linters/cqs/input_detector.py +139 -0
- src/linters/cqs/linter.py +159 -0
- src/linters/cqs/output_detector.py +84 -0
- src/linters/cqs/python_analyzer.py +54 -0
- src/linters/cqs/types.py +82 -0
- src/linters/cqs/typescript_cqs_analyzer.py +61 -0
- src/linters/cqs/typescript_function_analyzer.py +192 -0
- src/linters/cqs/typescript_input_detector.py +203 -0
- src/linters/cqs/typescript_output_detector.py +117 -0
- src/linters/cqs/violation_builder.py +94 -0
- src/linters/dry/base_token_analyzer.py +16 -9
- src/linters/dry/block_filter.py +120 -20
- src/linters/dry/block_grouper.py +4 -0
- src/linters/dry/cache.py +104 -10
- src/linters/dry/cache_query.py +4 -0
- src/linters/dry/config.py +54 -11
- src/linters/dry/constant.py +92 -0
- src/linters/dry/constant_matcher.py +223 -0
- src/linters/dry/constant_violation_builder.py +98 -0
- src/linters/dry/duplicate_storage.py +5 -4
- src/linters/dry/file_analyzer.py +4 -2
- src/linters/dry/inline_ignore.py +7 -16
- src/linters/dry/linter.py +183 -48
- src/linters/dry/python_analyzer.py +60 -439
- src/linters/dry/python_constant_extractor.py +100 -0
- src/linters/dry/single_statement_detector.py +417 -0
- src/linters/dry/token_hasher.py +116 -112
- src/linters/dry/typescript_analyzer.py +68 -382
- src/linters/dry/typescript_constant_extractor.py +138 -0
- src/linters/dry/typescript_statement_detector.py +255 -0
- src/linters/dry/typescript_value_extractor.py +70 -0
- src/linters/dry/violation_builder.py +4 -0
- src/linters/dry/violation_filter.py +5 -4
- src/linters/dry/violation_generator.py +71 -14
- src/linters/file_header/atemporal_detector.py +68 -50
- src/linters/file_header/base_parser.py +93 -0
- src/linters/file_header/bash_parser.py +66 -0
- src/linters/file_header/config.py +90 -16
- src/linters/file_header/css_parser.py +70 -0
- src/linters/file_header/field_validator.py +36 -33
- src/linters/file_header/linter.py +140 -144
- src/linters/file_header/markdown_parser.py +130 -0
- src/linters/file_header/python_parser.py +14 -58
- src/linters/file_header/typescript_parser.py +73 -0
- src/linters/file_header/violation_builder.py +13 -12
- src/linters/file_placement/config_loader.py +3 -1
- src/linters/file_placement/directory_matcher.py +4 -0
- src/linters/file_placement/linter.py +66 -34
- src/linters/file_placement/pattern_matcher.py +41 -6
- src/linters/file_placement/pattern_validator.py +31 -12
- src/linters/file_placement/rule_checker.py +12 -7
- src/linters/lazy_ignores/__init__.py +43 -0
- src/linters/lazy_ignores/config.py +74 -0
- src/linters/lazy_ignores/directive_utils.py +164 -0
- src/linters/lazy_ignores/header_parser.py +177 -0
- src/linters/lazy_ignores/linter.py +158 -0
- src/linters/lazy_ignores/matcher.py +168 -0
- src/linters/lazy_ignores/python_analyzer.py +209 -0
- src/linters/lazy_ignores/rule_id_utils.py +180 -0
- src/linters/lazy_ignores/skip_detector.py +298 -0
- src/linters/lazy_ignores/types.py +71 -0
- src/linters/lazy_ignores/typescript_analyzer.py +146 -0
- src/linters/lazy_ignores/violation_builder.py +135 -0
- src/linters/lbyl/__init__.py +31 -0
- src/linters/lbyl/config.py +63 -0
- src/linters/lbyl/linter.py +67 -0
- src/linters/lbyl/pattern_detectors/__init__.py +53 -0
- src/linters/lbyl/pattern_detectors/base.py +63 -0
- src/linters/lbyl/pattern_detectors/dict_key_detector.py +107 -0
- src/linters/lbyl/pattern_detectors/division_check_detector.py +232 -0
- src/linters/lbyl/pattern_detectors/file_exists_detector.py +220 -0
- src/linters/lbyl/pattern_detectors/hasattr_detector.py +119 -0
- src/linters/lbyl/pattern_detectors/isinstance_detector.py +119 -0
- src/linters/lbyl/pattern_detectors/len_check_detector.py +173 -0
- src/linters/lbyl/pattern_detectors/none_check_detector.py +146 -0
- src/linters/lbyl/pattern_detectors/string_validator_detector.py +145 -0
- src/linters/lbyl/python_analyzer.py +215 -0
- src/linters/lbyl/violation_builder.py +354 -0
- src/linters/magic_numbers/context_analyzer.py +227 -225
- src/linters/magic_numbers/linter.py +28 -82
- src/linters/magic_numbers/python_analyzer.py +4 -16
- src/linters/magic_numbers/typescript_analyzer.py +9 -12
- src/linters/magic_numbers/typescript_ignore_checker.py +81 -0
- src/linters/method_property/__init__.py +49 -0
- src/linters/method_property/config.py +138 -0
- src/linters/method_property/linter.py +414 -0
- src/linters/method_property/python_analyzer.py +473 -0
- src/linters/method_property/violation_builder.py +119 -0
- src/linters/nesting/linter.py +24 -16
- src/linters/nesting/python_analyzer.py +4 -0
- src/linters/nesting/typescript_analyzer.py +6 -12
- src/linters/nesting/violation_builder.py +1 -0
- src/linters/performance/__init__.py +91 -0
- src/linters/performance/config.py +43 -0
- src/linters/performance/constants.py +49 -0
- src/linters/performance/linter.py +149 -0
- src/linters/performance/python_analyzer.py +365 -0
- src/linters/performance/regex_analyzer.py +312 -0
- src/linters/performance/regex_linter.py +139 -0
- src/linters/performance/typescript_analyzer.py +236 -0
- src/linters/performance/violation_builder.py +160 -0
- src/linters/print_statements/config.py +7 -12
- src/linters/print_statements/linter.py +26 -43
- src/linters/print_statements/python_analyzer.py +91 -93
- src/linters/print_statements/typescript_analyzer.py +15 -25
- src/linters/print_statements/violation_builder.py +12 -14
- src/linters/srp/class_analyzer.py +11 -7
- src/linters/srp/heuristics.py +56 -22
- src/linters/srp/linter.py +15 -16
- src/linters/srp/python_analyzer.py +55 -20
- src/linters/srp/typescript_metrics_calculator.py +110 -50
- src/linters/stateless_class/__init__.py +25 -0
- src/linters/stateless_class/config.py +58 -0
- src/linters/stateless_class/linter.py +349 -0
- src/linters/stateless_class/python_analyzer.py +290 -0
- src/linters/stringly_typed/__init__.py +36 -0
- src/linters/stringly_typed/config.py +189 -0
- src/linters/stringly_typed/context_filter.py +451 -0
- src/linters/stringly_typed/function_call_violation_builder.py +135 -0
- src/linters/stringly_typed/ignore_checker.py +100 -0
- src/linters/stringly_typed/ignore_utils.py +51 -0
- src/linters/stringly_typed/linter.py +376 -0
- src/linters/stringly_typed/python/__init__.py +33 -0
- src/linters/stringly_typed/python/analyzer.py +348 -0
- src/linters/stringly_typed/python/call_tracker.py +175 -0
- src/linters/stringly_typed/python/comparison_tracker.py +257 -0
- src/linters/stringly_typed/python/condition_extractor.py +134 -0
- src/linters/stringly_typed/python/conditional_detector.py +179 -0
- src/linters/stringly_typed/python/constants.py +21 -0
- src/linters/stringly_typed/python/match_analyzer.py +94 -0
- src/linters/stringly_typed/python/validation_detector.py +189 -0
- src/linters/stringly_typed/python/variable_extractor.py +96 -0
- src/linters/stringly_typed/storage.py +620 -0
- src/linters/stringly_typed/storage_initializer.py +45 -0
- src/linters/stringly_typed/typescript/__init__.py +28 -0
- src/linters/stringly_typed/typescript/analyzer.py +157 -0
- src/linters/stringly_typed/typescript/call_tracker.py +335 -0
- src/linters/stringly_typed/typescript/comparison_tracker.py +378 -0
- src/linters/stringly_typed/violation_generator.py +419 -0
- src/orchestrator/core.py +252 -14
- src/orchestrator/language_detector.py +5 -3
- src/templates/thailint_config_template.yaml +196 -0
- src/utils/project_root.py +3 -0
- thailint-0.15.3.dist-info/METADATA +187 -0
- thailint-0.15.3.dist-info/RECORD +226 -0
- thailint-0.15.3.dist-info/entry_points.txt +4 -0
- src/cli.py +0 -1665
- thailint-0.5.0.dist-info/METADATA +0 -1286
- thailint-0.5.0.dist-info/RECORD +0 -96
- thailint-0.5.0.dist-info/entry_points.txt +0 -4
- {thailint-0.5.0.dist-info → thailint-0.15.3.dist-info}/WHEEL +0 -0
- {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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|