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.
Files changed (135) hide show
  1. src/analyzers/__init__.py +4 -3
  2. src/analyzers/ast_utils.py +54 -0
  3. src/analyzers/typescript_base.py +4 -0
  4. src/cli/__init__.py +3 -0
  5. src/cli/config.py +12 -12
  6. src/cli/config_merge.py +241 -0
  7. src/cli/linters/__init__.py +9 -0
  8. src/cli/linters/code_patterns.py +107 -257
  9. src/cli/linters/code_smells.py +48 -165
  10. src/cli/linters/documentation.py +21 -95
  11. src/cli/linters/performance.py +274 -0
  12. src/cli/linters/shared.py +232 -6
  13. src/cli/linters/structure.py +26 -21
  14. src/cli/linters/structure_quality.py +28 -21
  15. src/cli_main.py +3 -0
  16. src/config.py +2 -1
  17. src/core/base.py +3 -2
  18. src/core/cli_utils.py +3 -1
  19. src/core/config_parser.py +5 -2
  20. src/core/constants.py +54 -0
  21. src/core/linter_utils.py +95 -6
  22. src/core/rule_discovery.py +5 -1
  23. src/core/violation_builder.py +3 -0
  24. src/linter_config/directive_markers.py +109 -0
  25. src/linter_config/ignore.py +225 -383
  26. src/linter_config/pattern_utils.py +65 -0
  27. src/linter_config/rule_matcher.py +89 -0
  28. src/linters/collection_pipeline/any_all_analyzer.py +281 -0
  29. src/linters/collection_pipeline/ast_utils.py +40 -0
  30. src/linters/collection_pipeline/config.py +12 -0
  31. src/linters/collection_pipeline/continue_analyzer.py +2 -8
  32. src/linters/collection_pipeline/detector.py +262 -32
  33. src/linters/collection_pipeline/filter_map_analyzer.py +402 -0
  34. src/linters/collection_pipeline/linter.py +18 -35
  35. src/linters/collection_pipeline/suggestion_builder.py +68 -1
  36. src/linters/dry/base_token_analyzer.py +16 -9
  37. src/linters/dry/block_filter.py +7 -4
  38. src/linters/dry/cache.py +7 -2
  39. src/linters/dry/config.py +7 -1
  40. src/linters/dry/constant_matcher.py +34 -25
  41. src/linters/dry/file_analyzer.py +4 -2
  42. src/linters/dry/inline_ignore.py +7 -16
  43. src/linters/dry/linter.py +48 -25
  44. src/linters/dry/python_analyzer.py +18 -10
  45. src/linters/dry/python_constant_extractor.py +51 -52
  46. src/linters/dry/single_statement_detector.py +14 -12
  47. src/linters/dry/token_hasher.py +115 -115
  48. src/linters/dry/typescript_analyzer.py +11 -6
  49. src/linters/dry/typescript_constant_extractor.py +4 -0
  50. src/linters/dry/typescript_statement_detector.py +208 -208
  51. src/linters/dry/typescript_value_extractor.py +3 -0
  52. src/linters/dry/violation_filter.py +1 -4
  53. src/linters/dry/violation_generator.py +1 -4
  54. src/linters/file_header/atemporal_detector.py +58 -40
  55. src/linters/file_header/base_parser.py +4 -0
  56. src/linters/file_header/bash_parser.py +4 -0
  57. src/linters/file_header/config.py +14 -0
  58. src/linters/file_header/field_validator.py +5 -8
  59. src/linters/file_header/linter.py +19 -12
  60. src/linters/file_header/markdown_parser.py +6 -0
  61. src/linters/file_placement/config_loader.py +3 -1
  62. src/linters/file_placement/linter.py +22 -8
  63. src/linters/file_placement/pattern_matcher.py +21 -4
  64. src/linters/file_placement/pattern_validator.py +21 -7
  65. src/linters/file_placement/rule_checker.py +2 -2
  66. src/linters/lazy_ignores/__init__.py +43 -0
  67. src/linters/lazy_ignores/config.py +66 -0
  68. src/linters/lazy_ignores/directive_utils.py +121 -0
  69. src/linters/lazy_ignores/header_parser.py +177 -0
  70. src/linters/lazy_ignores/linter.py +158 -0
  71. src/linters/lazy_ignores/matcher.py +135 -0
  72. src/linters/lazy_ignores/python_analyzer.py +205 -0
  73. src/linters/lazy_ignores/rule_id_utils.py +180 -0
  74. src/linters/lazy_ignores/skip_detector.py +298 -0
  75. src/linters/lazy_ignores/types.py +69 -0
  76. src/linters/lazy_ignores/typescript_analyzer.py +146 -0
  77. src/linters/lazy_ignores/violation_builder.py +131 -0
  78. src/linters/lbyl/__init__.py +29 -0
  79. src/linters/lbyl/config.py +63 -0
  80. src/linters/lbyl/pattern_detectors/__init__.py +25 -0
  81. src/linters/lbyl/pattern_detectors/base.py +46 -0
  82. src/linters/magic_numbers/context_analyzer.py +227 -229
  83. src/linters/magic_numbers/linter.py +20 -15
  84. src/linters/magic_numbers/python_analyzer.py +4 -16
  85. src/linters/magic_numbers/typescript_analyzer.py +9 -16
  86. src/linters/method_property/config.py +4 -1
  87. src/linters/method_property/linter.py +5 -10
  88. src/linters/method_property/python_analyzer.py +5 -4
  89. src/linters/method_property/violation_builder.py +3 -0
  90. src/linters/nesting/linter.py +11 -6
  91. src/linters/nesting/typescript_analyzer.py +6 -12
  92. src/linters/nesting/typescript_function_extractor.py +0 -4
  93. src/linters/nesting/violation_builder.py +1 -0
  94. src/linters/performance/__init__.py +91 -0
  95. src/linters/performance/config.py +43 -0
  96. src/linters/performance/constants.py +49 -0
  97. src/linters/performance/linter.py +149 -0
  98. src/linters/performance/python_analyzer.py +365 -0
  99. src/linters/performance/regex_analyzer.py +312 -0
  100. src/linters/performance/regex_linter.py +139 -0
  101. src/linters/performance/typescript_analyzer.py +236 -0
  102. src/linters/performance/violation_builder.py +160 -0
  103. src/linters/print_statements/linter.py +6 -4
  104. src/linters/print_statements/python_analyzer.py +85 -81
  105. src/linters/print_statements/typescript_analyzer.py +6 -15
  106. src/linters/srp/heuristics.py +4 -4
  107. src/linters/srp/linter.py +12 -12
  108. src/linters/srp/violation_builder.py +0 -4
  109. src/linters/stateless_class/linter.py +30 -36
  110. src/linters/stateless_class/python_analyzer.py +11 -20
  111. src/linters/stringly_typed/config.py +4 -5
  112. src/linters/stringly_typed/context_filter.py +410 -410
  113. src/linters/stringly_typed/function_call_violation_builder.py +93 -95
  114. src/linters/stringly_typed/linter.py +48 -16
  115. src/linters/stringly_typed/python/analyzer.py +5 -1
  116. src/linters/stringly_typed/python/call_tracker.py +8 -5
  117. src/linters/stringly_typed/python/comparison_tracker.py +10 -5
  118. src/linters/stringly_typed/python/condition_extractor.py +3 -0
  119. src/linters/stringly_typed/python/conditional_detector.py +4 -1
  120. src/linters/stringly_typed/python/match_analyzer.py +8 -2
  121. src/linters/stringly_typed/python/validation_detector.py +3 -0
  122. src/linters/stringly_typed/storage.py +14 -14
  123. src/linters/stringly_typed/typescript/call_tracker.py +9 -3
  124. src/linters/stringly_typed/typescript/comparison_tracker.py +9 -3
  125. src/linters/stringly_typed/violation_generator.py +288 -259
  126. src/orchestrator/core.py +13 -4
  127. src/templates/thailint_config_template.yaml +196 -0
  128. src/utils/project_root.py +3 -0
  129. thailint-0.14.0.dist-info/METADATA +185 -0
  130. thailint-0.14.0.dist-info/RECORD +199 -0
  131. thailint-0.12.0.dist-info/METADATA +0 -1667
  132. thailint-0.12.0.dist-info/RECORD +0 -164
  133. {thailint-0.12.0.dist-info → thailint-0.14.0.dist-info}/WHEEL +0 -0
  134. {thailint-0.12.0.dist-info → thailint-0.14.0.dist-info}/entry_points.txt +0 -0
  135. {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
@@ -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
- try:
110
- tree = ast.parse(context.file_content or "")
111
- except SyntaxError as e:
112
- return [self._violation_builder.create_syntax_error_violation(e, context)]
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
- from src.analyzers.typescript_base import TypeScriptBaseAnalyzer
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
- TREE_SITTER_AVAILABLE = True
30
- except ImportError:
31
- TREE_SITTER_AVAILABLE = False
32
- Node = Any # type: ignore
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 "")