thailint 0.2.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 (214) 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 +44 -27
  23. src/core/base.py +95 -5
  24. src/core/cli_utils.py +19 -2
  25. src/core/config_parser.py +36 -6
  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 +125 -22
  65. src/linters/dry/block_grouper.py +4 -0
  66. src/linters/dry/cache.py +142 -94
  67. src/linters/dry/cache_query.py +4 -0
  68. src/linters/dry/config.py +68 -21
  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 +20 -82
  73. src/linters/dry/file_analyzer.py +15 -50
  74. src/linters/dry/inline_ignore.py +7 -16
  75. src/linters/dry/linter.py +182 -54
  76. src/linters/dry/python_analyzer.py +108 -336
  77. src/linters/dry/python_constant_extractor.py +100 -0
  78. src/linters/dry/single_statement_detector.py +417 -0
  79. src/linters/dry/storage_initializer.py +9 -18
  80. src/linters/dry/token_hasher.py +129 -71
  81. src/linters/dry/typescript_analyzer.py +68 -380
  82. src/linters/dry/typescript_constant_extractor.py +138 -0
  83. src/linters/dry/typescript_statement_detector.py +255 -0
  84. src/linters/dry/typescript_value_extractor.py +70 -0
  85. src/linters/dry/violation_builder.py +4 -0
  86. src/linters/dry/violation_filter.py +9 -5
  87. src/linters/dry/violation_generator.py +71 -14
  88. src/linters/file_header/__init__.py +24 -0
  89. src/linters/file_header/atemporal_detector.py +105 -0
  90. src/linters/file_header/base_parser.py +93 -0
  91. src/linters/file_header/bash_parser.py +66 -0
  92. src/linters/file_header/config.py +140 -0
  93. src/linters/file_header/css_parser.py +70 -0
  94. src/linters/file_header/field_validator.py +72 -0
  95. src/linters/file_header/linter.py +309 -0
  96. src/linters/file_header/markdown_parser.py +130 -0
  97. src/linters/file_header/python_parser.py +42 -0
  98. src/linters/file_header/typescript_parser.py +73 -0
  99. src/linters/file_header/violation_builder.py +79 -0
  100. src/linters/file_placement/config_loader.py +3 -1
  101. src/linters/file_placement/directory_matcher.py +4 -0
  102. src/linters/file_placement/linter.py +74 -31
  103. src/linters/file_placement/pattern_matcher.py +41 -6
  104. src/linters/file_placement/pattern_validator.py +31 -12
  105. src/linters/file_placement/rule_checker.py +12 -7
  106. src/linters/lazy_ignores/__init__.py +43 -0
  107. src/linters/lazy_ignores/config.py +74 -0
  108. src/linters/lazy_ignores/directive_utils.py +164 -0
  109. src/linters/lazy_ignores/header_parser.py +177 -0
  110. src/linters/lazy_ignores/linter.py +158 -0
  111. src/linters/lazy_ignores/matcher.py +168 -0
  112. src/linters/lazy_ignores/python_analyzer.py +209 -0
  113. src/linters/lazy_ignores/rule_id_utils.py +180 -0
  114. src/linters/lazy_ignores/skip_detector.py +298 -0
  115. src/linters/lazy_ignores/types.py +71 -0
  116. src/linters/lazy_ignores/typescript_analyzer.py +146 -0
  117. src/linters/lazy_ignores/violation_builder.py +135 -0
  118. src/linters/lbyl/__init__.py +31 -0
  119. src/linters/lbyl/config.py +63 -0
  120. src/linters/lbyl/linter.py +67 -0
  121. src/linters/lbyl/pattern_detectors/__init__.py +53 -0
  122. src/linters/lbyl/pattern_detectors/base.py +63 -0
  123. src/linters/lbyl/pattern_detectors/dict_key_detector.py +107 -0
  124. src/linters/lbyl/pattern_detectors/division_check_detector.py +232 -0
  125. src/linters/lbyl/pattern_detectors/file_exists_detector.py +220 -0
  126. src/linters/lbyl/pattern_detectors/hasattr_detector.py +119 -0
  127. src/linters/lbyl/pattern_detectors/isinstance_detector.py +119 -0
  128. src/linters/lbyl/pattern_detectors/len_check_detector.py +173 -0
  129. src/linters/lbyl/pattern_detectors/none_check_detector.py +146 -0
  130. src/linters/lbyl/pattern_detectors/string_validator_detector.py +145 -0
  131. src/linters/lbyl/python_analyzer.py +215 -0
  132. src/linters/lbyl/violation_builder.py +354 -0
  133. src/linters/magic_numbers/__init__.py +48 -0
  134. src/linters/magic_numbers/config.py +82 -0
  135. src/linters/magic_numbers/context_analyzer.py +249 -0
  136. src/linters/magic_numbers/linter.py +462 -0
  137. src/linters/magic_numbers/python_analyzer.py +64 -0
  138. src/linters/magic_numbers/typescript_analyzer.py +215 -0
  139. src/linters/magic_numbers/typescript_ignore_checker.py +81 -0
  140. src/linters/magic_numbers/violation_builder.py +98 -0
  141. src/linters/method_property/__init__.py +49 -0
  142. src/linters/method_property/config.py +138 -0
  143. src/linters/method_property/linter.py +414 -0
  144. src/linters/method_property/python_analyzer.py +473 -0
  145. src/linters/method_property/violation_builder.py +119 -0
  146. src/linters/nesting/__init__.py +6 -2
  147. src/linters/nesting/config.py +6 -3
  148. src/linters/nesting/linter.py +31 -34
  149. src/linters/nesting/python_analyzer.py +4 -0
  150. src/linters/nesting/typescript_analyzer.py +6 -11
  151. src/linters/nesting/violation_builder.py +1 -0
  152. src/linters/performance/__init__.py +91 -0
  153. src/linters/performance/config.py +43 -0
  154. src/linters/performance/constants.py +49 -0
  155. src/linters/performance/linter.py +149 -0
  156. src/linters/performance/python_analyzer.py +365 -0
  157. src/linters/performance/regex_analyzer.py +312 -0
  158. src/linters/performance/regex_linter.py +139 -0
  159. src/linters/performance/typescript_analyzer.py +236 -0
  160. src/linters/performance/violation_builder.py +160 -0
  161. src/linters/print_statements/__init__.py +53 -0
  162. src/linters/print_statements/config.py +78 -0
  163. src/linters/print_statements/linter.py +413 -0
  164. src/linters/print_statements/python_analyzer.py +153 -0
  165. src/linters/print_statements/typescript_analyzer.py +125 -0
  166. src/linters/print_statements/violation_builder.py +96 -0
  167. src/linters/srp/__init__.py +3 -3
  168. src/linters/srp/class_analyzer.py +11 -7
  169. src/linters/srp/config.py +12 -6
  170. src/linters/srp/heuristics.py +56 -22
  171. src/linters/srp/linter.py +47 -39
  172. src/linters/srp/python_analyzer.py +55 -20
  173. src/linters/srp/typescript_metrics_calculator.py +110 -50
  174. src/linters/stateless_class/__init__.py +25 -0
  175. src/linters/stateless_class/config.py +58 -0
  176. src/linters/stateless_class/linter.py +349 -0
  177. src/linters/stateless_class/python_analyzer.py +290 -0
  178. src/linters/stringly_typed/__init__.py +36 -0
  179. src/linters/stringly_typed/config.py +189 -0
  180. src/linters/stringly_typed/context_filter.py +451 -0
  181. src/linters/stringly_typed/function_call_violation_builder.py +135 -0
  182. src/linters/stringly_typed/ignore_checker.py +100 -0
  183. src/linters/stringly_typed/ignore_utils.py +51 -0
  184. src/linters/stringly_typed/linter.py +376 -0
  185. src/linters/stringly_typed/python/__init__.py +33 -0
  186. src/linters/stringly_typed/python/analyzer.py +348 -0
  187. src/linters/stringly_typed/python/call_tracker.py +175 -0
  188. src/linters/stringly_typed/python/comparison_tracker.py +257 -0
  189. src/linters/stringly_typed/python/condition_extractor.py +134 -0
  190. src/linters/stringly_typed/python/conditional_detector.py +179 -0
  191. src/linters/stringly_typed/python/constants.py +21 -0
  192. src/linters/stringly_typed/python/match_analyzer.py +94 -0
  193. src/linters/stringly_typed/python/validation_detector.py +189 -0
  194. src/linters/stringly_typed/python/variable_extractor.py +96 -0
  195. src/linters/stringly_typed/storage.py +620 -0
  196. src/linters/stringly_typed/storage_initializer.py +45 -0
  197. src/linters/stringly_typed/typescript/__init__.py +28 -0
  198. src/linters/stringly_typed/typescript/analyzer.py +157 -0
  199. src/linters/stringly_typed/typescript/call_tracker.py +335 -0
  200. src/linters/stringly_typed/typescript/comparison_tracker.py +378 -0
  201. src/linters/stringly_typed/violation_generator.py +419 -0
  202. src/orchestrator/core.py +264 -16
  203. src/orchestrator/language_detector.py +5 -3
  204. src/templates/thailint_config_template.yaml +354 -0
  205. src/utils/project_root.py +138 -16
  206. thailint-0.15.3.dist-info/METADATA +187 -0
  207. thailint-0.15.3.dist-info/RECORD +226 -0
  208. {thailint-0.2.0.dist-info → thailint-0.15.3.dist-info}/WHEEL +1 -1
  209. thailint-0.15.3.dist-info/entry_points.txt +4 -0
  210. src/cli.py +0 -1055
  211. thailint-0.2.0.dist-info/METADATA +0 -980
  212. thailint-0.2.0.dist-info/RECORD +0 -75
  213. thailint-0.2.0.dist-info/entry_points.txt +0 -4
  214. {thailint-0.2.0.dist-info → thailint-0.15.3.dist-info/licenses}/LICENSE +0 -0
@@ -16,15 +16,15 @@ 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
- from src.core.base import BaseLintContext, BaseLintRule
25
- from src.core.linter_utils import has_file_content, load_linter_config
24
+ from src.core.base import BaseLintContext, MultiLanguageLintRule
25
+ from src.core.linter_utils import load_linter_config, with_parsed_python
26
26
  from src.core.types import Violation
27
- from src.linter_config.ignore import IgnoreDirectiveParser
27
+ from src.linter_config.ignore import get_ignore_parser
28
28
 
29
29
  from .config import NestingConfig
30
30
  from .python_analyzer import PythonNestingAnalyzer
@@ -32,13 +32,16 @@ from .typescript_analyzer import TypeScriptNestingAnalyzer
32
32
  from .violation_builder import NestingViolationBuilder
33
33
 
34
34
 
35
- class NestingDepthRule(BaseLintRule):
35
+ class NestingDepthRule(MultiLanguageLintRule):
36
36
  """Detects excessive nesting depth in functions."""
37
37
 
38
38
  def __init__(self) -> None:
39
39
  """Initialize the nesting depth rule."""
40
- self._ignore_parser = IgnoreDirectiveParser()
40
+ self._ignore_parser = get_ignore_parser()
41
41
  self._violation_builder = NestingViolationBuilder(self.rule_id)
42
+ # Singleton analyzers for performance (avoid recreating per-file)
43
+ self._python_analyzer = PythonNestingAnalyzer()
44
+ self._typescript_analyzer = TypeScriptNestingAnalyzer()
42
45
 
43
46
  @property
44
47
  def rule_id(self) -> str:
@@ -55,27 +58,16 @@ class NestingDepthRule(BaseLintRule):
55
58
  """Description of what this rule checks."""
56
59
  return "Functions should not have excessive nesting depth for better readability"
57
60
 
58
- def check(self, context: BaseLintContext) -> list[Violation]:
59
- """Check for excessive nesting depth violations.
61
+ def _load_config(self, context: BaseLintContext) -> NestingConfig:
62
+ """Load configuration from context.
60
63
 
61
64
  Args:
62
- context: Lint context with file information
65
+ context: Lint context
63
66
 
64
67
  Returns:
65
- List of violations found
68
+ NestingConfig instance
66
69
  """
67
- if not has_file_content(context):
68
- return []
69
-
70
- config = load_linter_config(context, "nesting", NestingConfig)
71
- if not config.enabled:
72
- return []
73
-
74
- if context.language == "python":
75
- return self._check_python(context, config)
76
- if context.language in ("typescript", "javascript"):
77
- return self._check_typescript(context, config)
78
- return []
70
+ return load_linter_config(context, "nesting", NestingConfig)
79
71
 
80
72
  def _process_python_functions(
81
73
  self, functions: list, analyzer: Any, config: NestingConfig, context: BaseLintContext
@@ -114,14 +106,18 @@ class NestingDepthRule(BaseLintRule):
114
106
  Returns:
115
107
  List of violations found in Python code
116
108
  """
117
- try:
118
- tree = ast.parse(context.file_content or "")
119
- except SyntaxError as e:
120
- return [self._violation_builder.create_syntax_error_violation(e, context)]
121
-
122
- analyzer = PythonNestingAnalyzer()
123
- functions = analyzer.find_all_functions(tree)
124
- return self._process_python_functions(functions, analyzer, config, context)
109
+ return with_parsed_python(
110
+ context,
111
+ self._violation_builder,
112
+ lambda tree: self._analyze_python_tree(tree, config, context),
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."""
119
+ functions = self._python_analyzer.find_all_functions(tree)
120
+ return self._process_python_functions(functions, self._python_analyzer, config, context)
125
121
 
126
122
  def _process_typescript_functions(
127
123
  self, functions: list, analyzer: Any, config: NestingConfig, context: BaseLintContext
@@ -160,13 +156,14 @@ class NestingDepthRule(BaseLintRule):
160
156
  Returns:
161
157
  List of violations found in TypeScript code
162
158
  """
163
- analyzer = TypeScriptNestingAnalyzer()
164
- root_node = analyzer.parse_typescript(context.file_content or "")
159
+ root_node = self._typescript_analyzer.parse_typescript(context.file_content or "")
165
160
  if root_node is None:
166
161
  return []
167
162
 
168
- functions = analyzer.find_all_functions(root_node)
169
- return self._process_typescript_functions(functions, analyzer, config, context)
163
+ functions = self._typescript_analyzer.find_all_functions(root_node)
164
+ return self._process_typescript_functions(
165
+ functions, self._typescript_analyzer, config, context
166
+ )
170
167
 
171
168
  def _should_ignore(self, violation: Violation, context: BaseLintContext) -> bool:
172
169
  """Check if violation should be ignored based on inline directives.
@@ -25,6 +25,10 @@ import ast
25
25
  class PythonNestingAnalyzer:
26
26
  """Calculates maximum nesting depth in Python functions."""
27
27
 
28
+ def __init__(self) -> None:
29
+ """Initialize the Python nesting analyzer."""
30
+ pass # Stateless analyzer for nesting depth calculation
31
+
28
32
  def calculate_max_depth(
29
33
  self, func_node: ast.FunctionDef | ast.AsyncFunctionDef
30
34
  ) -> tuple[int, int]:
@@ -16,19 +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
- try:
26
- from tree_sitter import Node
20
+ """
27
21
 
28
- TREE_SITTER_AVAILABLE = True
29
- except ImportError:
30
- TREE_SITTER_AVAILABLE = False
31
- Node = Any # type: ignore
22
+ from src.analyzers.typescript_base import (
23
+ TREE_SITTER_AVAILABLE,
24
+ Node,
25
+ TypeScriptBaseAnalyzer,
26
+ )
32
27
 
33
28
  from .typescript_function_extractor import TypeScriptFunctionExtractor
34
29
 
@@ -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 "")