thailint 0.13.0__py3-none-any.whl → 0.15.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 (32) hide show
  1. src/cli/linters/__init__.py +6 -0
  2. src/cli/linters/code_patterns.py +75 -333
  3. src/cli/linters/code_smells.py +47 -168
  4. src/cli/linters/documentation.py +21 -98
  5. src/cli/linters/performance.py +274 -0
  6. src/cli/linters/shared.py +232 -6
  7. src/cli/linters/structure.py +23 -21
  8. src/cli/linters/structure_quality.py +25 -21
  9. src/core/linter_utils.py +91 -6
  10. src/linters/file_header/atemporal_detector.py +54 -40
  11. src/linters/file_header/config.py +14 -0
  12. src/linters/lazy_ignores/python_analyzer.py +5 -1
  13. src/linters/lazy_ignores/types.py +2 -0
  14. src/linters/method_property/config.py +0 -1
  15. src/linters/method_property/linter.py +0 -6
  16. src/linters/nesting/linter.py +11 -6
  17. src/linters/nesting/violation_builder.py +1 -0
  18. src/linters/performance/__init__.py +91 -0
  19. src/linters/performance/config.py +43 -0
  20. src/linters/performance/constants.py +49 -0
  21. src/linters/performance/linter.py +149 -0
  22. src/linters/performance/python_analyzer.py +365 -0
  23. src/linters/performance/regex_analyzer.py +312 -0
  24. src/linters/performance/regex_linter.py +139 -0
  25. src/linters/performance/typescript_analyzer.py +236 -0
  26. src/linters/performance/violation_builder.py +160 -0
  27. src/templates/thailint_config_template.yaml +30 -0
  28. {thailint-0.13.0.dist-info → thailint-0.15.0.dist-info}/METADATA +3 -2
  29. {thailint-0.13.0.dist-info → thailint-0.15.0.dist-info}/RECORD +32 -22
  30. {thailint-0.13.0.dist-info → thailint-0.15.0.dist-info}/WHEEL +0 -0
  31. {thailint-0.13.0.dist-info → thailint-0.15.0.dist-info}/entry_points.txt +0 -0
  32. {thailint-0.13.0.dist-info → thailint-0.15.0.dist-info}/licenses/LICENSE +0 -0
@@ -111,6 +111,20 @@ class FileHeaderConfig:
111
111
  defaults = cls()
112
112
  required_fields = config_dict.get("required_fields", {})
113
113
 
114
+ # Handle both list format (applies to all languages) and dict format (language-specific)
115
+ if isinstance(required_fields, list):
116
+ # Simple list format: apply same fields to all languages
117
+ return cls(
118
+ required_fields_python=required_fields,
119
+ required_fields_typescript=required_fields,
120
+ required_fields_bash=required_fields,
121
+ required_fields_markdown=required_fields,
122
+ required_fields_css=required_fields,
123
+ enforce_atemporal=config_dict.get("enforce_atemporal", True),
124
+ ignore=config_dict.get("ignore", defaults.ignore),
125
+ )
126
+
127
+ # Dict format: language-specific fields
114
128
  return cls(
115
129
  required_fields_python=required_fields.get("python", defaults.required_fields_python),
116
130
  required_fields_typescript=required_fields.get(
@@ -1,7 +1,7 @@
1
1
  """
2
2
  Purpose: Detect Python linting ignore directives in source code
3
3
 
4
- Scope: noqa, type:ignore, pylint:disable, nosec pattern detection
4
+ Scope: noqa, type:ignore, pylint:disable, nosec, dry:ignore-block pattern detection
5
5
 
6
6
  Overview: Provides PythonIgnoreDetector class that scans Python source code for common
7
7
  linting ignore patterns. Detects bare patterns (e.g., # noqa) and rule-specific
@@ -122,6 +122,10 @@ class PythonIgnoreDetector:
122
122
  r"#\s*thailint:\s*ignore-start(?:\[([^\]]+)\])?",
123
123
  re.IGNORECASE,
124
124
  ),
125
+ IgnoreType.DRY_IGNORE_BLOCK: re.compile(
126
+ r"#\s*dry:\s*ignore-block\b",
127
+ re.IGNORECASE,
128
+ ),
125
129
  }
126
130
 
127
131
  def find_ignores(self, code: str, file_path: Path | None = None) -> list[IgnoreDirective]:
@@ -39,6 +39,8 @@ class IgnoreType(Enum):
39
39
  THAILINT_IGNORE_FILE = "thailint:ignore-file"
40
40
  THAILINT_IGNORE_NEXT = "thailint:ignore-next-line"
41
41
  THAILINT_IGNORE_BLOCK = "thailint:ignore-start"
42
+ # DRY ignore patterns
43
+ DRY_IGNORE_BLOCK = "dry:ignore-block"
42
44
  # Test skip patterns
43
45
  PYTEST_SKIP = "pytest:skip"
44
46
  PYTEST_SKIPIF = "pytest:skipif"
@@ -100,7 +100,6 @@ class MethodPropertyConfig: # thailint: ignore[dry]
100
100
  exclude_prefixes: tuple[str, ...] = DEFAULT_EXCLUDE_PREFIXES
101
101
  exclude_names: frozenset[str] = DEFAULT_EXCLUDE_NAMES
102
102
 
103
- # dry: ignore-block
104
103
  @classmethod
105
104
  def from_dict(
106
105
  cls, config: dict[str, Any] | None, language: str | None = None
@@ -75,7 +75,6 @@ class MethodPropertyRule(MultiLanguageLintRule): # thailint: ignore[srp,dry]
75
75
 
76
76
  return MethodPropertyConfig()
77
77
 
78
- # dry: ignore-block
79
78
  def _try_load_test_config(self, context: BaseLintContext) -> MethodPropertyConfig | None:
80
79
  """Try to load test-style configuration.
81
80
 
@@ -95,7 +94,6 @@ class MethodPropertyRule(MultiLanguageLintRule): # thailint: ignore[srp,dry]
95
94
  linter_config = config_attr.get("method-property", config_attr)
96
95
  return MethodPropertyConfig.from_dict(linter_config)
97
96
 
98
- # dry: ignore-block
99
97
  def _is_file_ignored(self, context: BaseLintContext, config: MethodPropertyConfig) -> bool:
100
98
  """Check if file matches ignore patterns.
101
99
 
@@ -115,7 +113,6 @@ class MethodPropertyRule(MultiLanguageLintRule): # thailint: ignore[srp,dry]
115
113
  file_path = Path(context.file_path)
116
114
  return any(self._matches_pattern(file_path, pattern) for pattern in config.ignore)
117
115
 
118
- # dry: ignore-block
119
116
  def _matches_pattern(self, file_path: Path, pattern: str) -> bool:
120
117
  """Check if file path matches a glob pattern.
121
118
 
@@ -132,7 +129,6 @@ class MethodPropertyRule(MultiLanguageLintRule): # thailint: ignore[srp,dry]
132
129
  return True
133
130
  return False
134
131
 
135
- # dry: ignore-block
136
132
  def _is_test_file(self, file_path: object) -> bool:
137
133
  """Check if file is a test file.
138
134
 
@@ -204,7 +200,6 @@ class MethodPropertyRule(MultiLanguageLintRule): # thailint: ignore[srp,dry]
204
200
  return candidates
205
201
  return [c for c in candidates if c.method_name not in config.ignore_methods]
206
202
 
207
- # dry: ignore-block
208
203
  def _parse_python_code(self, code: str | None) -> ast.AST | None:
209
204
  """Parse Python code into AST.
210
205
 
@@ -285,7 +280,6 @@ class MethodPropertyRule(MultiLanguageLintRule): # thailint: ignore[srp,dry]
285
280
  return True
286
281
  return False
287
282
 
288
- # dry: ignore-block
289
283
  def _has_inline_ignore(self, violation: Violation, context: BaseLintContext) -> bool:
290
284
  """Check for inline ignore directive on method line.
291
285
 
@@ -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
 
@@ -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 "")