thailint 0.12.0__py3-none-any.whl → 0.14.0__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/analyzers/__init__.py +4 -3
- src/analyzers/ast_utils.py +54 -0
- src/analyzers/typescript_base.py +4 -0
- src/cli/__init__.py +3 -0
- src/cli/config.py +12 -12
- src/cli/config_merge.py +241 -0
- src/cli/linters/__init__.py +9 -0
- src/cli/linters/code_patterns.py +107 -257
- src/cli/linters/code_smells.py +48 -165
- src/cli/linters/documentation.py +21 -95
- src/cli/linters/performance.py +274 -0
- src/cli/linters/shared.py +232 -6
- src/cli/linters/structure.py +26 -21
- src/cli/linters/structure_quality.py +28 -21
- src/cli_main.py +3 -0
- src/config.py +2 -1
- src/core/base.py +3 -2
- src/core/cli_utils.py +3 -1
- src/core/config_parser.py +5 -2
- src/core/constants.py +54 -0
- src/core/linter_utils.py +95 -6
- src/core/rule_discovery.py +5 -1
- src/core/violation_builder.py +3 -0
- src/linter_config/directive_markers.py +109 -0
- src/linter_config/ignore.py +225 -383
- src/linter_config/pattern_utils.py +65 -0
- src/linter_config/rule_matcher.py +89 -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 +12 -0
- src/linters/collection_pipeline/continue_analyzer.py +2 -8
- src/linters/collection_pipeline/detector.py +262 -32
- src/linters/collection_pipeline/filter_map_analyzer.py +402 -0
- src/linters/collection_pipeline/linter.py +18 -35
- src/linters/collection_pipeline/suggestion_builder.py +68 -1
- src/linters/dry/base_token_analyzer.py +16 -9
- src/linters/dry/block_filter.py +7 -4
- src/linters/dry/cache.py +7 -2
- src/linters/dry/config.py +7 -1
- src/linters/dry/constant_matcher.py +34 -25
- src/linters/dry/file_analyzer.py +4 -2
- src/linters/dry/inline_ignore.py +7 -16
- src/linters/dry/linter.py +48 -25
- src/linters/dry/python_analyzer.py +18 -10
- src/linters/dry/python_constant_extractor.py +51 -52
- src/linters/dry/single_statement_detector.py +14 -12
- src/linters/dry/token_hasher.py +115 -115
- src/linters/dry/typescript_analyzer.py +11 -6
- src/linters/dry/typescript_constant_extractor.py +4 -0
- src/linters/dry/typescript_statement_detector.py +208 -208
- src/linters/dry/typescript_value_extractor.py +3 -0
- src/linters/dry/violation_filter.py +1 -4
- src/linters/dry/violation_generator.py +1 -4
- src/linters/file_header/atemporal_detector.py +58 -40
- src/linters/file_header/base_parser.py +4 -0
- src/linters/file_header/bash_parser.py +4 -0
- src/linters/file_header/config.py +14 -0
- src/linters/file_header/field_validator.py +5 -8
- src/linters/file_header/linter.py +19 -12
- src/linters/file_header/markdown_parser.py +6 -0
- src/linters/file_placement/config_loader.py +3 -1
- src/linters/file_placement/linter.py +22 -8
- src/linters/file_placement/pattern_matcher.py +21 -4
- src/linters/file_placement/pattern_validator.py +21 -7
- src/linters/file_placement/rule_checker.py +2 -2
- src/linters/lazy_ignores/__init__.py +43 -0
- src/linters/lazy_ignores/config.py +66 -0
- src/linters/lazy_ignores/directive_utils.py +121 -0
- src/linters/lazy_ignores/header_parser.py +177 -0
- src/linters/lazy_ignores/linter.py +158 -0
- src/linters/lazy_ignores/matcher.py +135 -0
- src/linters/lazy_ignores/python_analyzer.py +205 -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 +69 -0
- src/linters/lazy_ignores/typescript_analyzer.py +146 -0
- src/linters/lazy_ignores/violation_builder.py +131 -0
- src/linters/lbyl/__init__.py +29 -0
- src/linters/lbyl/config.py +63 -0
- src/linters/lbyl/pattern_detectors/__init__.py +25 -0
- src/linters/lbyl/pattern_detectors/base.py +46 -0
- src/linters/magic_numbers/context_analyzer.py +227 -229
- src/linters/magic_numbers/linter.py +20 -15
- src/linters/magic_numbers/python_analyzer.py +4 -16
- src/linters/magic_numbers/typescript_analyzer.py +9 -16
- src/linters/method_property/config.py +4 -1
- src/linters/method_property/linter.py +5 -10
- src/linters/method_property/python_analyzer.py +5 -4
- src/linters/method_property/violation_builder.py +3 -0
- src/linters/nesting/linter.py +11 -6
- src/linters/nesting/typescript_analyzer.py +6 -12
- src/linters/nesting/typescript_function_extractor.py +0 -4
- 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/linter.py +6 -4
- src/linters/print_statements/python_analyzer.py +85 -81
- src/linters/print_statements/typescript_analyzer.py +6 -15
- src/linters/srp/heuristics.py +4 -4
- src/linters/srp/linter.py +12 -12
- src/linters/srp/violation_builder.py +0 -4
- src/linters/stateless_class/linter.py +30 -36
- src/linters/stateless_class/python_analyzer.py +11 -20
- src/linters/stringly_typed/config.py +4 -5
- src/linters/stringly_typed/context_filter.py +410 -410
- src/linters/stringly_typed/function_call_violation_builder.py +93 -95
- src/linters/stringly_typed/linter.py +48 -16
- src/linters/stringly_typed/python/analyzer.py +5 -1
- src/linters/stringly_typed/python/call_tracker.py +8 -5
- src/linters/stringly_typed/python/comparison_tracker.py +10 -5
- src/linters/stringly_typed/python/condition_extractor.py +3 -0
- src/linters/stringly_typed/python/conditional_detector.py +4 -1
- src/linters/stringly_typed/python/match_analyzer.py +8 -2
- src/linters/stringly_typed/python/validation_detector.py +3 -0
- src/linters/stringly_typed/storage.py +14 -14
- src/linters/stringly_typed/typescript/call_tracker.py +9 -3
- src/linters/stringly_typed/typescript/comparison_tracker.py +9 -3
- src/linters/stringly_typed/violation_generator.py +288 -259
- src/orchestrator/core.py +13 -4
- src/templates/thailint_config_template.yaml +196 -0
- src/utils/project_root.py +3 -0
- thailint-0.14.0.dist-info/METADATA +185 -0
- thailint-0.14.0.dist-info/RECORD +199 -0
- thailint-0.12.0.dist-info/METADATA +0 -1667
- thailint-0.12.0.dist-info/RECORD +0 -164
- {thailint-0.12.0.dist-info → thailint-0.14.0.dist-info}/WHEEL +0 -0
- {thailint-0.12.0.dist-info → thailint-0.14.0.dist-info}/entry_points.txt +0 -0
- {thailint-0.12.0.dist-info → thailint-0.14.0.dist-info}/licenses/LICENSE +0 -0
|
@@ -18,6 +18,10 @@ Exports: MethodPropertyConfig dataclass, DEFAULT_EXCLUDE_PREFIXES, DEFAULT_EXCLU
|
|
|
18
18
|
Interfaces: from_dict(config, language) -> MethodPropertyConfig for configuration loading
|
|
19
19
|
|
|
20
20
|
Implementation: Dataclass with defaults matching Pythonic conventions and common use cases
|
|
21
|
+
|
|
22
|
+
Suppressions:
|
|
23
|
+
- dry: MethodPropertyConfig includes extensive exclusion lists that share patterns with
|
|
24
|
+
other config classes. Lists are maintained separately for clear documentation.
|
|
21
25
|
"""
|
|
22
26
|
|
|
23
27
|
from dataclasses import dataclass, field
|
|
@@ -96,7 +100,6 @@ class MethodPropertyConfig: # thailint: ignore[dry]
|
|
|
96
100
|
exclude_prefixes: tuple[str, ...] = DEFAULT_EXCLUDE_PREFIXES
|
|
97
101
|
exclude_names: frozenset[str] = DEFAULT_EXCLUDE_NAMES
|
|
98
102
|
|
|
99
|
-
# dry: ignore-block
|
|
100
103
|
@classmethod
|
|
101
104
|
def from_dict(
|
|
102
105
|
cls, config: dict[str, Any] | None, language: str | None = None
|
|
@@ -21,6 +21,10 @@ Interfaces: check(context) -> list[Violation] for rule validation, standard rule
|
|
|
21
21
|
|
|
22
22
|
Implementation: Composition pattern with helper classes (analyzer, violation builder),
|
|
23
23
|
AST-based analysis for Python with comprehensive exclusion rules
|
|
24
|
+
|
|
25
|
+
Suppressions:
|
|
26
|
+
- srp,dry: Rule class coordinates analyzer, config, and violation building. Method count
|
|
27
|
+
exceeds limit due to comprehensive ignore directive support.
|
|
24
28
|
"""
|
|
25
29
|
|
|
26
30
|
import ast
|
|
@@ -71,7 +75,6 @@ class MethodPropertyRule(MultiLanguageLintRule): # thailint: ignore[srp,dry]
|
|
|
71
75
|
|
|
72
76
|
return MethodPropertyConfig()
|
|
73
77
|
|
|
74
|
-
# dry: ignore-block
|
|
75
78
|
def _try_load_test_config(self, context: BaseLintContext) -> MethodPropertyConfig | None:
|
|
76
79
|
"""Try to load test-style configuration.
|
|
77
80
|
|
|
@@ -91,7 +94,6 @@ class MethodPropertyRule(MultiLanguageLintRule): # thailint: ignore[srp,dry]
|
|
|
91
94
|
linter_config = config_attr.get("method-property", config_attr)
|
|
92
95
|
return MethodPropertyConfig.from_dict(linter_config)
|
|
93
96
|
|
|
94
|
-
# dry: ignore-block
|
|
95
97
|
def _is_file_ignored(self, context: BaseLintContext, config: MethodPropertyConfig) -> bool:
|
|
96
98
|
"""Check if file matches ignore patterns.
|
|
97
99
|
|
|
@@ -109,12 +111,8 @@ class MethodPropertyRule(MultiLanguageLintRule): # thailint: ignore[srp,dry]
|
|
|
109
111
|
return False
|
|
110
112
|
|
|
111
113
|
file_path = Path(context.file_path)
|
|
112
|
-
for pattern in config.ignore
|
|
113
|
-
if self._matches_pattern(file_path, pattern):
|
|
114
|
-
return True
|
|
115
|
-
return False
|
|
114
|
+
return any(self._matches_pattern(file_path, pattern) for pattern in config.ignore)
|
|
116
115
|
|
|
117
|
-
# dry: ignore-block
|
|
118
116
|
def _matches_pattern(self, file_path: Path, pattern: str) -> bool:
|
|
119
117
|
"""Check if file path matches a glob pattern.
|
|
120
118
|
|
|
@@ -131,7 +129,6 @@ class MethodPropertyRule(MultiLanguageLintRule): # thailint: ignore[srp,dry]
|
|
|
131
129
|
return True
|
|
132
130
|
return False
|
|
133
131
|
|
|
134
|
-
# dry: ignore-block
|
|
135
132
|
def _is_test_file(self, file_path: object) -> bool:
|
|
136
133
|
"""Check if file is a test file.
|
|
137
134
|
|
|
@@ -203,7 +200,6 @@ class MethodPropertyRule(MultiLanguageLintRule): # thailint: ignore[srp,dry]
|
|
|
203
200
|
return candidates
|
|
204
201
|
return [c for c in candidates if c.method_name not in config.ignore_methods]
|
|
205
202
|
|
|
206
|
-
# dry: ignore-block
|
|
207
203
|
def _parse_python_code(self, code: str | None) -> ast.AST | None:
|
|
208
204
|
"""Parse Python code into AST.
|
|
209
205
|
|
|
@@ -284,7 +280,6 @@ class MethodPropertyRule(MultiLanguageLintRule): # thailint: ignore[srp,dry]
|
|
|
284
280
|
return True
|
|
285
281
|
return False
|
|
286
282
|
|
|
287
|
-
# dry: ignore-block
|
|
288
283
|
def _has_inline_ignore(self, violation: Violation, context: BaseLintContext) -> bool:
|
|
289
284
|
"""Check for inline ignore directive on method line.
|
|
290
285
|
|
|
@@ -18,6 +18,10 @@ Exports: PythonMethodAnalyzer class, PropertyCandidate dataclass
|
|
|
18
18
|
Interfaces: find_property_candidates(tree) -> list[PropertyCandidate]
|
|
19
19
|
|
|
20
20
|
Implementation: AST walk pattern with comprehensive method body analysis and exclusion checks
|
|
21
|
+
|
|
22
|
+
Suppressions:
|
|
23
|
+
- srp: Analyzer class implements comprehensive exclusion rules requiring many helper methods.
|
|
24
|
+
All methods support single responsibility of property candidate detection.
|
|
21
25
|
"""
|
|
22
26
|
|
|
23
27
|
import ast
|
|
@@ -391,10 +395,7 @@ class PythonMethodAnalyzer: # thailint: ignore[srp]
|
|
|
391
395
|
Returns:
|
|
392
396
|
True if assigning to self.*
|
|
393
397
|
"""
|
|
394
|
-
for target in targets
|
|
395
|
-
if self._is_self_target(target):
|
|
396
|
-
return True
|
|
397
|
-
return False
|
|
398
|
+
return any(self._is_self_target(target) for target in targets)
|
|
398
399
|
|
|
399
400
|
def _is_self_target(self, target: ast.expr) -> bool:
|
|
400
401
|
"""Check if target is a self attribute (self.* or self._*).
|
|
@@ -16,6 +16,9 @@ Exports: ViolationBuilder class
|
|
|
16
16
|
Interfaces: create_violation(method_name, line, column, file_path, is_get_prefix, class_name)
|
|
17
17
|
|
|
18
18
|
Implementation: Builder pattern with message templates suggesting @property decorator conversion
|
|
19
|
+
|
|
20
|
+
Suppressions:
|
|
21
|
+
- too-many-arguments,too-many-positional-arguments: Violation creation with related params
|
|
19
22
|
"""
|
|
20
23
|
|
|
21
24
|
from pathlib import Path
|
src/linters/nesting/linter.py
CHANGED
|
@@ -16,13 +16,13 @@ Exports: NestingDepthRule class
|
|
|
16
16
|
Interfaces: NestingDepthRule.check(context) -> list[Violation], properties for rule metadata
|
|
17
17
|
|
|
18
18
|
Implementation: Composition pattern with helper classes, AST-based analysis with configurable limits
|
|
19
|
+
|
|
19
20
|
"""
|
|
20
21
|
|
|
21
|
-
import ast
|
|
22
22
|
from typing import Any
|
|
23
23
|
|
|
24
24
|
from src.core.base import BaseLintContext, MultiLanguageLintRule
|
|
25
|
-
from src.core.linter_utils import load_linter_config
|
|
25
|
+
from src.core.linter_utils import load_linter_config, with_parsed_python
|
|
26
26
|
from src.core.types import Violation
|
|
27
27
|
from src.linter_config.ignore import get_ignore_parser
|
|
28
28
|
|
|
@@ -106,11 +106,16 @@ class NestingDepthRule(MultiLanguageLintRule):
|
|
|
106
106
|
Returns:
|
|
107
107
|
List of violations found in Python code
|
|
108
108
|
"""
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
109
|
+
return with_parsed_python(
|
|
110
|
+
context,
|
|
111
|
+
self._violation_builder,
|
|
112
|
+
lambda tree: self._analyze_python_tree(tree, config, context),
|
|
113
|
+
)
|
|
113
114
|
|
|
115
|
+
def _analyze_python_tree(
|
|
116
|
+
self, tree: Any, config: NestingConfig, context: BaseLintContext
|
|
117
|
+
) -> list[Violation]:
|
|
118
|
+
"""Analyze parsed Python AST for nesting violations."""
|
|
114
119
|
functions = self._python_analyzer.find_all_functions(tree)
|
|
115
120
|
return self._process_python_functions(functions, self._python_analyzer, config, context)
|
|
116
121
|
|
|
@@ -16,20 +16,14 @@ Exports: TypeScriptNestingAnalyzer class with calculate_max_depth methods
|
|
|
16
16
|
Interfaces: calculate_max_depth(func_node) -> tuple[int, int], find_all_functions(root_node)
|
|
17
17
|
|
|
18
18
|
Implementation: Inherits tree-sitter parsing from base, visitor pattern with depth tracking
|
|
19
|
-
"""
|
|
20
|
-
|
|
21
|
-
from typing import Any
|
|
22
19
|
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
# dry: ignore-block - tree-sitter import pattern (common across TypeScript analyzers)
|
|
26
|
-
try:
|
|
27
|
-
from tree_sitter import Node
|
|
20
|
+
"""
|
|
28
21
|
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
22
|
+
from src.analyzers.typescript_base import (
|
|
23
|
+
TREE_SITTER_AVAILABLE,
|
|
24
|
+
Node,
|
|
25
|
+
TypeScriptBaseAnalyzer,
|
|
26
|
+
)
|
|
33
27
|
|
|
34
28
|
from .typescript_function_extractor import TypeScriptFunctionExtractor
|
|
35
29
|
|
|
@@ -27,10 +27,6 @@ from src.analyzers.typescript_base import TypeScriptBaseAnalyzer
|
|
|
27
27
|
class TypeScriptFunctionExtractor(TypeScriptBaseAnalyzer):
|
|
28
28
|
"""Extracts function information from TypeScript AST nodes."""
|
|
29
29
|
|
|
30
|
-
def __init__(self) -> None: # pylint: disable=useless-parent-delegation
|
|
31
|
-
"""Initialize the TypeScript function extractor."""
|
|
32
|
-
super().__init__() # Sets self.tree_sitter_available from base class
|
|
33
|
-
|
|
34
30
|
def collect_all_functions(self, root_node: Any) -> list[tuple[Any, str]]:
|
|
35
31
|
"""Collect all function nodes from TypeScript AST.
|
|
36
32
|
|
|
@@ -17,6 +17,7 @@ Interfaces: create_nesting_violation, create_typescript_nesting_violation, creat
|
|
|
17
17
|
|
|
18
18
|
Implementation: Formats messages with depth information, provides targeted refactoring suggestions,
|
|
19
19
|
extends BaseViolationBuilder for consistent violation construction
|
|
20
|
+
|
|
20
21
|
"""
|
|
21
22
|
|
|
22
23
|
import ast
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Purpose: Performance linter package initialization
|
|
3
|
+
|
|
4
|
+
Scope: Exports for performance linter module
|
|
5
|
+
|
|
6
|
+
Overview: Initializes the performance linter package and exposes the main rule classes for
|
|
7
|
+
external use. Exports StringConcatLoopRule as the primary interface for the performance
|
|
8
|
+
linter, allowing the orchestrator to discover and instantiate the rule. Also exports
|
|
9
|
+
configuration and analyzer classes for advanced use cases. Provides a convenience lint()
|
|
10
|
+
function for direct usage without orchestrator setup. This module serves as the entry
|
|
11
|
+
point for the performance linter functionality within the thai-lint framework.
|
|
12
|
+
|
|
13
|
+
Dependencies: StringConcatLoopRule, PerformanceConfig, analyzers
|
|
14
|
+
|
|
15
|
+
Exports: StringConcatLoopRule (primary), PerformanceConfig, analyzers, lint
|
|
16
|
+
|
|
17
|
+
Interfaces: Standard Python package initialization with __all__ for explicit exports
|
|
18
|
+
|
|
19
|
+
Implementation: Simple re-export pattern for package interface, convenience function
|
|
20
|
+
"""
|
|
21
|
+
|
|
22
|
+
from pathlib import Path
|
|
23
|
+
from typing import Any
|
|
24
|
+
|
|
25
|
+
from .config import PerformanceConfig
|
|
26
|
+
from .linter import StringConcatLoopRule
|
|
27
|
+
from .python_analyzer import PythonStringConcatAnalyzer
|
|
28
|
+
from .regex_analyzer import PythonRegexInLoopAnalyzer
|
|
29
|
+
from .regex_linter import RegexInLoopRule
|
|
30
|
+
from .typescript_analyzer import TypeScriptStringConcatAnalyzer
|
|
31
|
+
|
|
32
|
+
__all__ = [
|
|
33
|
+
"StringConcatLoopRule",
|
|
34
|
+
"RegexInLoopRule",
|
|
35
|
+
"PerformanceConfig",
|
|
36
|
+
"PythonStringConcatAnalyzer",
|
|
37
|
+
"PythonRegexInLoopAnalyzer",
|
|
38
|
+
"TypeScriptStringConcatAnalyzer",
|
|
39
|
+
"lint",
|
|
40
|
+
]
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def lint(
|
|
44
|
+
path: Path | str,
|
|
45
|
+
config: dict[str, Any] | None = None,
|
|
46
|
+
) -> list:
|
|
47
|
+
"""Lint a file or directory for performance issues.
|
|
48
|
+
|
|
49
|
+
Args:
|
|
50
|
+
path: Path to file or directory to lint
|
|
51
|
+
config: Configuration dict (optional, uses defaults if not provided)
|
|
52
|
+
|
|
53
|
+
Returns:
|
|
54
|
+
List of violations found
|
|
55
|
+
|
|
56
|
+
Example:
|
|
57
|
+
>>> from src.linters.performance import lint
|
|
58
|
+
>>> violations = lint('src/my_module.py')
|
|
59
|
+
>>> for v in violations:
|
|
60
|
+
... print(f"{v.file_path}:{v.line} - {v.message}")
|
|
61
|
+
"""
|
|
62
|
+
path_obj = Path(path) if isinstance(path, str) else path
|
|
63
|
+
project_root = path_obj if path_obj.is_dir() else path_obj.parent
|
|
64
|
+
|
|
65
|
+
orchestrator = _setup_performance_orchestrator(project_root, config)
|
|
66
|
+
violations = _execute_performance_lint(orchestrator, path_obj)
|
|
67
|
+
|
|
68
|
+
return [v for v in violations if "performance" in v.rule_id]
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
def _setup_performance_orchestrator(project_root: Path, config: dict[str, Any] | None) -> Any:
|
|
72
|
+
"""Set up orchestrator with performance config."""
|
|
73
|
+
from src.orchestrator.core import Orchestrator
|
|
74
|
+
|
|
75
|
+
orchestrator = Orchestrator(project_root=project_root)
|
|
76
|
+
|
|
77
|
+
if config:
|
|
78
|
+
orchestrator.config["performance"] = config
|
|
79
|
+
else:
|
|
80
|
+
orchestrator.config["performance"] = {"enabled": True}
|
|
81
|
+
|
|
82
|
+
return orchestrator
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
def _execute_performance_lint(orchestrator: Any, path_obj: Path) -> list:
|
|
86
|
+
"""Execute linting on file or directory."""
|
|
87
|
+
if path_obj.is_file():
|
|
88
|
+
return orchestrator.lint_file(path_obj)
|
|
89
|
+
if path_obj.is_dir():
|
|
90
|
+
return orchestrator.lint_directory(path_obj)
|
|
91
|
+
return []
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Purpose: Configuration schema for performance linter rules
|
|
3
|
+
|
|
4
|
+
Scope: PerformanceConfig dataclass with settings for string-concat-loop and regex-in-loop
|
|
5
|
+
|
|
6
|
+
Overview: Defines configuration schema for performance linter rules. Provides PerformanceConfig
|
|
7
|
+
dataclass with enabled flag and optional rule-specific settings. Supports loading from
|
|
8
|
+
YAML/JSON configuration files. Integrates with the orchestrator's configuration system
|
|
9
|
+
to allow users to customize performance rule settings via .thailint.yaml files.
|
|
10
|
+
|
|
11
|
+
Dependencies: dataclasses, typing
|
|
12
|
+
|
|
13
|
+
Exports: PerformanceConfig dataclass
|
|
14
|
+
|
|
15
|
+
Interfaces: PerformanceConfig(enabled: bool = True), from_dict class method for loading config
|
|
16
|
+
|
|
17
|
+
Implementation: Dataclass with validation and defaults, simple enabled flag (extensible)
|
|
18
|
+
"""
|
|
19
|
+
|
|
20
|
+
from dataclasses import dataclass
|
|
21
|
+
from typing import Any
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
@dataclass
|
|
25
|
+
class PerformanceConfig:
|
|
26
|
+
"""Configuration for performance linter rules."""
|
|
27
|
+
|
|
28
|
+
enabled: bool = True
|
|
29
|
+
|
|
30
|
+
@classmethod
|
|
31
|
+
def from_dict(cls, config: dict[str, Any], language: str | None = None) -> "PerformanceConfig":
|
|
32
|
+
"""Load configuration from dictionary.
|
|
33
|
+
|
|
34
|
+
Args:
|
|
35
|
+
config: Dictionary containing configuration values
|
|
36
|
+
language: Programming language (reserved for language-specific config)
|
|
37
|
+
|
|
38
|
+
Returns:
|
|
39
|
+
PerformanceConfig instance with values from dictionary
|
|
40
|
+
"""
|
|
41
|
+
return cls(
|
|
42
|
+
enabled=config.get("enabled", True),
|
|
43
|
+
)
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Purpose: Shared constants for performance linter rules
|
|
3
|
+
|
|
4
|
+
Scope: Common patterns and constants used across Python and TypeScript analyzers
|
|
5
|
+
|
|
6
|
+
Overview: Provides shared constants for the performance linter, including variable
|
|
7
|
+
name patterns that suggest string building. Used by both PythonStringConcatAnalyzer
|
|
8
|
+
and TypeScriptStringConcatAnalyzer to detect likely string concatenation.
|
|
9
|
+
|
|
10
|
+
Dependencies: None
|
|
11
|
+
|
|
12
|
+
Exports: STRING_VARIABLE_PATTERNS, LOOP_NODE_TYPES_TS
|
|
13
|
+
|
|
14
|
+
Interfaces: Frozen sets of patterns
|
|
15
|
+
|
|
16
|
+
Implementation: Constants shared between analyzers
|
|
17
|
+
"""
|
|
18
|
+
|
|
19
|
+
# Variable names that suggest string building
|
|
20
|
+
STRING_VARIABLE_PATTERNS = frozenset(
|
|
21
|
+
{
|
|
22
|
+
"result",
|
|
23
|
+
"output",
|
|
24
|
+
"message",
|
|
25
|
+
"msg",
|
|
26
|
+
"text",
|
|
27
|
+
"html",
|
|
28
|
+
"content",
|
|
29
|
+
"body",
|
|
30
|
+
"buffer",
|
|
31
|
+
"response",
|
|
32
|
+
"data",
|
|
33
|
+
"line",
|
|
34
|
+
"lines",
|
|
35
|
+
"string",
|
|
36
|
+
"str",
|
|
37
|
+
"s",
|
|
38
|
+
}
|
|
39
|
+
)
|
|
40
|
+
|
|
41
|
+
# Tree-sitter node types for loops (TypeScript)
|
|
42
|
+
LOOP_NODE_TYPES_TS = frozenset(
|
|
43
|
+
{
|
|
44
|
+
"for_statement",
|
|
45
|
+
"for_in_statement",
|
|
46
|
+
"while_statement",
|
|
47
|
+
"do_statement",
|
|
48
|
+
}
|
|
49
|
+
)
|
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Purpose: Main string concatenation in loop linter rule implementation
|
|
3
|
+
|
|
4
|
+
Scope: StringConcatLoopRule class implementing MultiLanguageLintRule interface
|
|
5
|
+
|
|
6
|
+
Overview: Implements string-concat-loop linter rule following MultiLanguageLintRule interface.
|
|
7
|
+
Orchestrates configuration loading, Python/TypeScript analysis, and violation building
|
|
8
|
+
through focused helper classes. Detects O(n²) string building patterns using += in loops.
|
|
9
|
+
Supports configurable enabled flag and ignore directives. Main rule class acts as
|
|
10
|
+
coordinator for string concatenation detection workflow.
|
|
11
|
+
|
|
12
|
+
Dependencies: MultiLanguageLintRule, BaseLintContext, PythonStringConcatAnalyzer,
|
|
13
|
+
TypeScriptStringConcatAnalyzer, PerformanceViolationBuilder
|
|
14
|
+
|
|
15
|
+
Exports: StringConcatLoopRule class
|
|
16
|
+
|
|
17
|
+
Interfaces: StringConcatLoopRule.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 .python_analyzer import PythonStringConcatAnalyzer
|
|
32
|
+
from .typescript_analyzer import TypeScriptStringConcatAnalyzer
|
|
33
|
+
from .violation_builder import PerformanceViolationBuilder
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
class StringConcatLoopRule(MultiLanguageLintRule):
|
|
37
|
+
"""Detects O(n²) string concatenation in loops."""
|
|
38
|
+
|
|
39
|
+
def __init__(self) -> None:
|
|
40
|
+
"""Initialize the string concat loop rule."""
|
|
41
|
+
self._ignore_parser = get_ignore_parser()
|
|
42
|
+
self._violation_builder = PerformanceViolationBuilder(self.rule_id)
|
|
43
|
+
# Singleton analyzers for performance
|
|
44
|
+
self._python_analyzer = PythonStringConcatAnalyzer()
|
|
45
|
+
self._typescript_analyzer = TypeScriptStringConcatAnalyzer()
|
|
46
|
+
|
|
47
|
+
@property
|
|
48
|
+
def rule_id(self) -> str:
|
|
49
|
+
"""Unique identifier for this rule."""
|
|
50
|
+
return "performance.string-concat-loop"
|
|
51
|
+
|
|
52
|
+
@property
|
|
53
|
+
def rule_name(self) -> str:
|
|
54
|
+
"""Human-readable name for this rule."""
|
|
55
|
+
return "String Concatenation in Loop"
|
|
56
|
+
|
|
57
|
+
@property
|
|
58
|
+
def description(self) -> str:
|
|
59
|
+
"""Description of what this rule checks."""
|
|
60
|
+
return "String += in loops creates O(n²) complexity; use join() instead"
|
|
61
|
+
|
|
62
|
+
def _load_config(self, context: BaseLintContext) -> PerformanceConfig:
|
|
63
|
+
"""Load configuration from context.
|
|
64
|
+
|
|
65
|
+
Args:
|
|
66
|
+
context: Lint context
|
|
67
|
+
|
|
68
|
+
Returns:
|
|
69
|
+
PerformanceConfig instance
|
|
70
|
+
"""
|
|
71
|
+
return load_linter_config(context, "performance", PerformanceConfig)
|
|
72
|
+
|
|
73
|
+
def _check_python(self, context: BaseLintContext, config: PerformanceConfig) -> list[Violation]:
|
|
74
|
+
"""Check Python code for string concatenation in loops.
|
|
75
|
+
|
|
76
|
+
Args:
|
|
77
|
+
context: Lint context with Python file information
|
|
78
|
+
config: Performance configuration
|
|
79
|
+
|
|
80
|
+
Returns:
|
|
81
|
+
List of violations found in Python code
|
|
82
|
+
"""
|
|
83
|
+
return with_parsed_python(
|
|
84
|
+
context,
|
|
85
|
+
self._violation_builder,
|
|
86
|
+
lambda tree: self._analyze_python_string_concat(tree, context),
|
|
87
|
+
)
|
|
88
|
+
|
|
89
|
+
def _analyze_python_string_concat(self, tree: Any, context: BaseLintContext) -> list[Violation]:
|
|
90
|
+
"""Analyze parsed Python AST for string concatenation in loops."""
|
|
91
|
+
violations_raw = self._python_analyzer.find_violations(tree)
|
|
92
|
+
violations_deduped = self._python_analyzer.deduplicate_violations(violations_raw)
|
|
93
|
+
return self._build_violations(violations_deduped, context)
|
|
94
|
+
|
|
95
|
+
def _check_typescript(
|
|
96
|
+
self, context: BaseLintContext, config: PerformanceConfig
|
|
97
|
+
) -> list[Violation]:
|
|
98
|
+
"""Check TypeScript code for string concatenation in loops.
|
|
99
|
+
|
|
100
|
+
Args:
|
|
101
|
+
context: Lint context with TypeScript file information
|
|
102
|
+
config: Performance configuration
|
|
103
|
+
|
|
104
|
+
Returns:
|
|
105
|
+
List of violations found in TypeScript code
|
|
106
|
+
"""
|
|
107
|
+
root_node = self._typescript_analyzer.parse_typescript(context.file_content or "")
|
|
108
|
+
if root_node is None:
|
|
109
|
+
return []
|
|
110
|
+
|
|
111
|
+
violations_raw = self._typescript_analyzer.find_violations(root_node)
|
|
112
|
+
violations_deduped = self._typescript_analyzer.deduplicate_violations(violations_raw)
|
|
113
|
+
|
|
114
|
+
return self._build_violations(violations_deduped, context)
|
|
115
|
+
|
|
116
|
+
def _build_violations(self, raw_violations: list, context: BaseLintContext) -> list[Violation]:
|
|
117
|
+
"""Build Violation objects from analyzer results.
|
|
118
|
+
|
|
119
|
+
Args:
|
|
120
|
+
raw_violations: List of StringConcatViolation dataclass instances
|
|
121
|
+
context: Lint context
|
|
122
|
+
|
|
123
|
+
Returns:
|
|
124
|
+
List of Violation objects
|
|
125
|
+
"""
|
|
126
|
+
violations = []
|
|
127
|
+
for v in raw_violations:
|
|
128
|
+
violation = self._violation_builder.create_string_concat_violation(
|
|
129
|
+
variable_name=v.variable_name,
|
|
130
|
+
line_number=v.line_number,
|
|
131
|
+
column=v.column,
|
|
132
|
+
loop_type=v.loop_type,
|
|
133
|
+
context=context,
|
|
134
|
+
)
|
|
135
|
+
if not self._should_ignore(violation, context):
|
|
136
|
+
violations.append(violation)
|
|
137
|
+
return violations
|
|
138
|
+
|
|
139
|
+
def _should_ignore(self, violation: Violation, context: BaseLintContext) -> bool:
|
|
140
|
+
"""Check if violation should be ignored based on inline directives.
|
|
141
|
+
|
|
142
|
+
Args:
|
|
143
|
+
violation: Violation to check
|
|
144
|
+
context: Lint context with file content
|
|
145
|
+
|
|
146
|
+
Returns:
|
|
147
|
+
True if violation should be ignored
|
|
148
|
+
"""
|
|
149
|
+
return self._ignore_parser.should_ignore_violation(violation, context.file_content or "")
|