thailint 0.1.6__py3-none-any.whl → 0.2.1__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 (68) hide show
  1. src/__init__.py +7 -2
  2. src/analyzers/__init__.py +23 -0
  3. src/analyzers/typescript_base.py +148 -0
  4. src/api.py +1 -1
  5. src/cli.py +524 -141
  6. src/config.py +6 -31
  7. src/core/base.py +12 -0
  8. src/core/cli_utils.py +206 -0
  9. src/core/config_parser.py +99 -0
  10. src/core/linter_utils.py +168 -0
  11. src/core/registry.py +17 -92
  12. src/core/rule_discovery.py +132 -0
  13. src/core/violation_builder.py +122 -0
  14. src/linter_config/ignore.py +112 -40
  15. src/linter_config/loader.py +3 -13
  16. src/linters/dry/__init__.py +23 -0
  17. src/linters/dry/base_token_analyzer.py +76 -0
  18. src/linters/dry/block_filter.py +262 -0
  19. src/linters/dry/block_grouper.py +59 -0
  20. src/linters/dry/cache.py +218 -0
  21. src/linters/dry/cache_query.py +61 -0
  22. src/linters/dry/config.py +130 -0
  23. src/linters/dry/config_loader.py +44 -0
  24. src/linters/dry/deduplicator.py +120 -0
  25. src/linters/dry/duplicate_storage.py +126 -0
  26. src/linters/dry/file_analyzer.py +127 -0
  27. src/linters/dry/inline_ignore.py +140 -0
  28. src/linters/dry/linter.py +170 -0
  29. src/linters/dry/python_analyzer.py +517 -0
  30. src/linters/dry/storage_initializer.py +51 -0
  31. src/linters/dry/token_hasher.py +115 -0
  32. src/linters/dry/typescript_analyzer.py +590 -0
  33. src/linters/dry/violation_builder.py +74 -0
  34. src/linters/dry/violation_filter.py +91 -0
  35. src/linters/dry/violation_generator.py +174 -0
  36. src/linters/file_placement/config_loader.py +86 -0
  37. src/linters/file_placement/directory_matcher.py +80 -0
  38. src/linters/file_placement/linter.py +252 -472
  39. src/linters/file_placement/path_resolver.py +61 -0
  40. src/linters/file_placement/pattern_matcher.py +55 -0
  41. src/linters/file_placement/pattern_validator.py +106 -0
  42. src/linters/file_placement/rule_checker.py +229 -0
  43. src/linters/file_placement/violation_factory.py +177 -0
  44. src/linters/nesting/config.py +13 -3
  45. src/linters/nesting/linter.py +76 -152
  46. src/linters/nesting/typescript_analyzer.py +38 -102
  47. src/linters/nesting/typescript_function_extractor.py +130 -0
  48. src/linters/nesting/violation_builder.py +139 -0
  49. src/linters/srp/__init__.py +99 -0
  50. src/linters/srp/class_analyzer.py +113 -0
  51. src/linters/srp/config.py +76 -0
  52. src/linters/srp/heuristics.py +89 -0
  53. src/linters/srp/linter.py +225 -0
  54. src/linters/srp/metrics_evaluator.py +47 -0
  55. src/linters/srp/python_analyzer.py +72 -0
  56. src/linters/srp/typescript_analyzer.py +75 -0
  57. src/linters/srp/typescript_metrics_calculator.py +90 -0
  58. src/linters/srp/violation_builder.py +117 -0
  59. src/orchestrator/core.py +42 -7
  60. src/utils/__init__.py +4 -0
  61. src/utils/project_root.py +84 -0
  62. {thailint-0.1.6.dist-info → thailint-0.2.1.dist-info}/METADATA +414 -63
  63. thailint-0.2.1.dist-info/RECORD +75 -0
  64. src/.ai/layout.yaml +0 -48
  65. thailint-0.1.6.dist-info/RECORD +0 -28
  66. {thailint-0.1.6.dist-info → thailint-0.2.1.dist-info}/LICENSE +0 -0
  67. {thailint-0.1.6.dist-info → thailint-0.2.1.dist-info}/WHEEL +0 -0
  68. {thailint-0.1.6.dist-info → thailint-0.2.1.dist-info}/entry_points.txt +0 -0
@@ -0,0 +1,139 @@
1
+ """
2
+ Purpose: Violation creation for nesting depth linter
3
+
4
+ Scope: Builds Violation objects for nesting depth violations
5
+
6
+ Overview: Provides violation building functionality for the nesting depth linter. Creates
7
+ violations for Python and TypeScript functions with excessive nesting, generates contextual
8
+ error messages with actual vs maximum depth, and provides actionable refactoring suggestions
9
+ (early returns, guard clauses, extract method). Handles syntax errors gracefully. Isolates
10
+ violation construction from analysis and checking logic.
11
+
12
+ Dependencies: ast, BaseLintContext, Violation, Severity, NestingConfig, src.core.violation_builder
13
+
14
+ Exports: NestingViolationBuilder
15
+
16
+ Interfaces: create_nesting_violation, create_typescript_nesting_violation, create_syntax_error_violation
17
+
18
+ Implementation: Formats messages with depth information, provides targeted refactoring suggestions,
19
+ extends BaseViolationBuilder for consistent violation construction
20
+ """
21
+
22
+ import ast
23
+ from typing import Any
24
+
25
+ from src.core.base import BaseLintContext
26
+ from src.core.types import Severity, Violation
27
+ from src.core.violation_builder import BaseViolationBuilder
28
+
29
+ from .config import NestingConfig
30
+
31
+
32
+ class NestingViolationBuilder(BaseViolationBuilder):
33
+ """Builds violations for nesting depth issues."""
34
+
35
+ def __init__(self, rule_id: str):
36
+ """Initialize violation builder.
37
+
38
+ Args:
39
+ rule_id: Rule identifier for violations
40
+ """
41
+ self.rule_id = rule_id
42
+
43
+ def create_syntax_error_violation(
44
+ self, error: SyntaxError, context: BaseLintContext
45
+ ) -> Violation:
46
+ """Create violation for syntax error.
47
+
48
+ Args:
49
+ error: SyntaxError exception
50
+ context: Lint context
51
+
52
+ Returns:
53
+ Syntax error violation
54
+ """
55
+ return self.build_from_params(
56
+ rule_id=self.rule_id,
57
+ file_path=str(context.file_path or ""),
58
+ line=error.lineno or 0,
59
+ column=error.offset or 0,
60
+ message=f"Syntax error: {error.msg}",
61
+ severity=Severity.ERROR,
62
+ suggestion="Fix syntax errors before checking nesting depth",
63
+ )
64
+
65
+ def create_nesting_violation(
66
+ self,
67
+ func: ast.FunctionDef | ast.AsyncFunctionDef,
68
+ max_depth: int,
69
+ config: NestingConfig,
70
+ context: BaseLintContext,
71
+ ) -> Violation:
72
+ """Create violation for excessive nesting in Python function.
73
+
74
+ Args:
75
+ func: Python function AST node
76
+ max_depth: Actual max nesting depth found
77
+ config: Nesting configuration
78
+ context: Lint context
79
+
80
+ Returns:
81
+ Nesting depth violation
82
+ """
83
+ return self.build_from_params(
84
+ rule_id=self.rule_id,
85
+ file_path=str(context.file_path or ""),
86
+ line=func.lineno,
87
+ column=func.col_offset,
88
+ message=f"Function '{func.name}' has excessive nesting depth ({max_depth})",
89
+ severity=Severity.ERROR,
90
+ suggestion=self._generate_suggestion(max_depth, config.max_nesting_depth),
91
+ )
92
+
93
+ def create_typescript_nesting_violation(
94
+ self,
95
+ func_info: tuple[Any, str],
96
+ max_depth: int,
97
+ config: NestingConfig,
98
+ context: BaseLintContext,
99
+ ) -> Violation:
100
+ """Create violation for excessive nesting in TypeScript function.
101
+
102
+ Args:
103
+ func_info: Tuple of (func_node, func_name)
104
+ max_depth: Actual max nesting depth found
105
+ config: Nesting configuration
106
+ context: Lint context
107
+
108
+ Returns:
109
+ Nesting depth violation
110
+ """
111
+ func_node, func_name = func_info
112
+ line = func_node.start_point[0] + 1 # Convert to 1-indexed
113
+ column = func_node.start_point[1]
114
+
115
+ return self.build_from_params(
116
+ rule_id=self.rule_id,
117
+ file_path=str(context.file_path or ""),
118
+ line=line,
119
+ column=column,
120
+ message=f"Function '{func_name}' has excessive nesting depth ({max_depth})",
121
+ severity=Severity.ERROR,
122
+ suggestion=self._generate_suggestion(max_depth, config.max_nesting_depth),
123
+ )
124
+
125
+ def _generate_suggestion(self, actual_depth: int, max_depth: int) -> str:
126
+ """Generate refactoring suggestion based on depth.
127
+
128
+ Args:
129
+ actual_depth: Actual nesting depth found
130
+ max_depth: Maximum allowed depth
131
+
132
+ Returns:
133
+ Suggestion string with refactoring advice
134
+ """
135
+ return (
136
+ f"Maximum nesting depth of {actual_depth} exceeds limit of {max_depth}. "
137
+ "Consider extracting nested logic to separate functions, using early returns, "
138
+ "or applying guard clauses to reduce nesting."
139
+ )
@@ -0,0 +1,99 @@
1
+ """
2
+ Purpose: SRP linter package initialization
3
+
4
+ Scope: Exports for Single Responsibility Principle linter module
5
+
6
+ Overview: Initializes the SRP linter package and exposes the main rule class for external use.
7
+ Exports SRPRule as the primary interface for the SRP linter, allowing the orchestrator to
8
+ discover and instantiate the rule. Also exports configuration and analyzer classes for
9
+ advanced use cases. Provides a convenience lint() function for direct usage without
10
+ orchestrator setup. This module serves as the entry point for the SRP linter functionality
11
+ within the thai-lint framework, enabling detection of classes with too many responsibilities.
12
+
13
+ Dependencies: SRPRule, SRPConfig, PythonSRPAnalyzer, TypeScriptSRPAnalyzer
14
+
15
+ Exports: SRPRule (primary), SRPConfig, PythonSRPAnalyzer, TypeScriptSRPAnalyzer, lint
16
+
17
+ Interfaces: Standard Python package initialization with __all__ for explicit exports, lint() convenience function
18
+
19
+ Implementation: Simple re-export pattern for package interface, convenience function wraps orchestrator
20
+ """
21
+
22
+ from pathlib import Path
23
+ from typing import Any
24
+
25
+ from .config import SRPConfig
26
+ from .linter import SRPRule
27
+ from .python_analyzer import PythonSRPAnalyzer
28
+ from .typescript_analyzer import TypeScriptSRPAnalyzer
29
+
30
+ __all__ = [
31
+ "SRPRule",
32
+ "SRPConfig",
33
+ "PythonSRPAnalyzer",
34
+ "TypeScriptSRPAnalyzer",
35
+ "lint",
36
+ ]
37
+
38
+
39
+ def lint(
40
+ path: Path | str,
41
+ config: dict[str, Any] | None = None,
42
+ max_methods: int = 7,
43
+ max_loc: int = 200,
44
+ ) -> list:
45
+ """Lint a file or directory for SRP violations.
46
+
47
+ Args:
48
+ path: Path to file or directory to lint
49
+ config: Configuration dict (optional, uses defaults if not provided)
50
+ max_methods: Maximum allowed methods per class (default: 7)
51
+ max_loc: Maximum allowed lines of code per class (default: 200)
52
+
53
+ Returns:
54
+ List of violations found
55
+
56
+ Example:
57
+ >>> from src.linters.srp import lint
58
+ >>> violations = lint('src/my_module.py', max_methods=5)
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_srp_orchestrator(project_root, config, max_methods, max_loc)
66
+ violations = _execute_srp_lint(orchestrator, path_obj)
67
+
68
+ return [v for v in violations if "srp" in v.rule_id]
69
+
70
+
71
+ def _setup_srp_orchestrator(
72
+ project_root: Path,
73
+ config: dict[str, Any] | None,
74
+ max_methods: int,
75
+ max_loc: int,
76
+ ) -> Any:
77
+ """Set up orchestrator with SRP config."""
78
+ from src.orchestrator.core import Orchestrator
79
+
80
+ orchestrator = Orchestrator(project_root=project_root)
81
+
82
+ if config:
83
+ orchestrator.config["srp"] = config
84
+ else:
85
+ orchestrator.config["srp"] = {
86
+ "max_methods": max_methods,
87
+ "max_loc": max_loc,
88
+ }
89
+
90
+ return orchestrator
91
+
92
+
93
+ def _execute_srp_lint(orchestrator: Any, path_obj: Path) -> list:
94
+ """Execute linting on file or directory."""
95
+ if path_obj.is_file():
96
+ return orchestrator.lint_file(path_obj)
97
+ if path_obj.is_dir():
98
+ return orchestrator.lint_directory(path_obj)
99
+ return []
@@ -0,0 +1,113 @@
1
+ """
2
+ Purpose: Class analysis coordination for SRP linter
3
+
4
+ Scope: Coordinates Python and TypeScript class analysis
5
+
6
+ Overview: Provides unified class analysis interface for the SRP linter. Delegates to language-
7
+ specific analyzers (PythonSRPAnalyzer, TypeScriptSRPAnalyzer) based on language type.
8
+ Handles syntax error gracefully and extracts class metrics for SRP evaluation. Isolates
9
+ language-specific analysis logic from rule checking and violation building.
10
+
11
+ Dependencies: ast, PythonSRPAnalyzer, TypeScriptSRPAnalyzer, BaseLintContext, SRPConfig
12
+
13
+ Exports: ClassAnalyzer
14
+
15
+ Interfaces: analyze_python(context, config) -> list[dict], analyze_typescript(context, config) -> list[dict]
16
+
17
+ Implementation: Delegates to language-specific analyzers, returns normalized metrics dicts
18
+ """
19
+
20
+ import ast
21
+ from typing import Any
22
+
23
+ from src.core.base import BaseLintContext
24
+ from src.core.types import Severity, Violation
25
+
26
+ from .config import SRPConfig
27
+ from .python_analyzer import PythonSRPAnalyzer
28
+ from .typescript_analyzer import TypeScriptSRPAnalyzer
29
+
30
+
31
+ class ClassAnalyzer:
32
+ """Coordinates class analysis for Python and TypeScript."""
33
+
34
+ def analyze_python(
35
+ self, context: BaseLintContext, config: SRPConfig
36
+ ) -> list[dict[str, Any]] | list[Violation]:
37
+ """Analyze Python classes and return metrics or syntax errors.
38
+
39
+ Args:
40
+ context: Lint context with file information
41
+ config: SRP configuration
42
+
43
+ Returns:
44
+ List of class metrics dicts, or list of syntax error violations
45
+ """
46
+ tree = self._parse_python_safely(context)
47
+ if isinstance(tree, list): # Syntax error violations
48
+ return tree
49
+
50
+ analyzer = PythonSRPAnalyzer()
51
+ classes = analyzer.find_all_classes(tree)
52
+ return [
53
+ analyzer.analyze_class(class_node, context.file_content or "", config)
54
+ for class_node in classes
55
+ ]
56
+
57
+ def analyze_typescript(
58
+ self, context: BaseLintContext, config: SRPConfig
59
+ ) -> list[dict[str, Any]]:
60
+ """Analyze TypeScript classes and return metrics.
61
+
62
+ Args:
63
+ context: Lint context with file information
64
+ config: SRP configuration
65
+
66
+ Returns:
67
+ List of class metrics dicts
68
+ """
69
+ analyzer = TypeScriptSRPAnalyzer()
70
+ root_node = analyzer.parse_typescript(context.file_content or "")
71
+ if not root_node:
72
+ return []
73
+
74
+ classes = analyzer.find_all_classes(root_node)
75
+ return [
76
+ analyzer.analyze_class(class_node, context.file_content or "", config)
77
+ for class_node in classes
78
+ ]
79
+
80
+ def _parse_python_safely(self, context: BaseLintContext) -> ast.AST | list[Violation]:
81
+ """Parse Python code and return AST or syntax error violations.
82
+
83
+ Args:
84
+ context: Lint context with file information
85
+
86
+ Returns:
87
+ AST if successful, list of syntax error violations otherwise
88
+ """
89
+ try:
90
+ return ast.parse(context.file_content or "")
91
+ except SyntaxError as exc:
92
+ return [self._create_syntax_error_violation(exc, context)]
93
+
94
+ def _create_syntax_error_violation(
95
+ self, exc: SyntaxError, context: BaseLintContext
96
+ ) -> Violation:
97
+ """Create syntax error violation.
98
+
99
+ Args:
100
+ exc: SyntaxError exception
101
+ context: Lint context
102
+
103
+ Returns:
104
+ Syntax error violation
105
+ """
106
+ return Violation(
107
+ rule_id="srp.syntax-error",
108
+ file_path=str(context.file_path or ""),
109
+ line=exc.lineno or 1,
110
+ column=exc.offset or 0,
111
+ message=f"Syntax error: {exc.msg}",
112
+ severity=Severity.ERROR,
113
+ )
@@ -0,0 +1,76 @@
1
+ """
2
+ Purpose: Configuration schema for Single Responsibility Principle linter
3
+
4
+ Scope: SRPConfig dataclass with max_methods, max_loc, and keyword settings
5
+
6
+ Overview: Defines configuration schema for SRP linter. Provides SRPConfig dataclass with
7
+ max_methods field (default 7), max_loc field (default 200), and check_keywords flag
8
+ (default True) with configurable responsibility keywords. Supports per-file and
9
+ per-directory config overrides. Validates that thresholds are positive integers.
10
+ Integrates with the orchestrator's configuration system to allow users to customize
11
+ SRP thresholds via .thailint.yaml configuration files. Keywords list identifies
12
+ generic class names that often indicate SRP violations (Manager, Handler, etc.).
13
+
14
+ Dependencies: dataclasses, typing
15
+
16
+ Exports: SRPConfig dataclass
17
+
18
+ Interfaces: SRPConfig(max_methods, max_loc, check_keywords, keywords), from_dict class method
19
+
20
+ Implementation: Dataclass with validation and defaults, heuristic-based SRP detection thresholds
21
+ """
22
+
23
+ from dataclasses import dataclass, field
24
+ from typing import Any
25
+
26
+
27
+ @dataclass
28
+ class SRPConfig:
29
+ """Configuration for SRP linter."""
30
+
31
+ max_methods: int = 7 # Maximum methods per class
32
+ max_loc: int = 200 # Maximum lines of code per class
33
+ enabled: bool = True
34
+ check_keywords: bool = True
35
+ keywords: list[str] = field(
36
+ default_factory=lambda: ["Manager", "Handler", "Processor", "Utility", "Helper"]
37
+ )
38
+ ignore: list[str] = field(default_factory=list) # Path patterns to ignore
39
+
40
+ def __post_init__(self) -> None:
41
+ """Validate configuration values."""
42
+ if self.max_methods <= 0:
43
+ raise ValueError(f"max_methods must be positive, got {self.max_methods}")
44
+ if self.max_loc <= 0:
45
+ raise ValueError(f"max_loc must be positive, got {self.max_loc}")
46
+
47
+ @classmethod
48
+ def from_dict(cls, config: dict[str, Any], language: str | None = None) -> "SRPConfig":
49
+ """Load configuration from dictionary with language-specific overrides.
50
+
51
+ Args:
52
+ config: Dictionary containing configuration values
53
+ language: Programming language (python, typescript, javascript) for language-specific thresholds
54
+
55
+ Returns:
56
+ SRPConfig instance with values from dictionary
57
+ """
58
+ # Get language-specific config if available
59
+ if language and language in config:
60
+ lang_config = config[language]
61
+ max_methods = lang_config.get("max_methods", config.get("max_methods", 7))
62
+ max_loc = lang_config.get("max_loc", config.get("max_loc", 200))
63
+ else:
64
+ max_methods = config.get("max_methods", 7)
65
+ max_loc = config.get("max_loc", 200)
66
+
67
+ return cls(
68
+ max_methods=max_methods,
69
+ max_loc=max_loc,
70
+ enabled=config.get("enabled", True),
71
+ check_keywords=config.get("check_keywords", True),
72
+ keywords=config.get(
73
+ "keywords", ["Manager", "Handler", "Processor", "Utility", "Helper"]
74
+ ),
75
+ ignore=config.get("ignore", []),
76
+ )
@@ -0,0 +1,89 @@
1
+ """
2
+ Purpose: SRP detection heuristics for analyzing code complexity and responsibility
3
+
4
+ Scope: Helper functions for method counting, LOC calculation, and keyword detection
5
+
6
+ Overview: Provides heuristic-based analysis functions for detecting Single Responsibility
7
+ Principle violations. Implements method counting that excludes property decorators and
8
+ special methods. Provides LOC calculation that filters out blank lines and comments.
9
+ Includes keyword detection for identifying generic class names that often indicate SRP
10
+ violations (Manager, Handler, etc.). Supports both Python AST and TypeScript tree-sitter
11
+ nodes. These heuristics enable practical SRP detection without requiring perfect semantic
12
+ analysis, focusing on measurable code metrics that correlate with responsibility scope.
13
+
14
+ Dependencies: ast module for Python AST analysis, typing for type hints
15
+
16
+ Exports: count_methods, count_loc, has_responsibility_keyword, has_property_decorator
17
+
18
+ Interfaces: Functions accepting AST nodes and returning metrics (int, bool)
19
+
20
+ Implementation: AST walking with filtering logic, heuristic-based thresholds
21
+ """
22
+
23
+ import ast
24
+
25
+
26
+ def count_methods(class_node: ast.ClassDef) -> int:
27
+ """Count methods in a class (excludes properties and special methods).
28
+
29
+ Args:
30
+ class_node: AST node representing a class definition
31
+
32
+ Returns:
33
+ Number of methods in the class
34
+ """
35
+ methods = 0
36
+ for node in class_node.body:
37
+ if not isinstance(node, (ast.FunctionDef, ast.AsyncFunctionDef)):
38
+ continue
39
+ # Don't count @property decorators as methods
40
+ if not has_property_decorator(node):
41
+ methods += 1
42
+ return methods
43
+
44
+
45
+ def count_loc(class_node: ast.ClassDef, source: str) -> int:
46
+ """Count lines of code in a class (excludes blank lines and comments).
47
+
48
+ Args:
49
+ class_node: AST node representing a class definition
50
+ source: Full source code of the file
51
+
52
+ Returns:
53
+ Number of code lines in the class
54
+ """
55
+ start_line = class_node.lineno
56
+ end_line = class_node.end_lineno or start_line
57
+ lines = source.split("\n")[start_line - 1 : end_line]
58
+
59
+ # Filter out blank lines and comments
60
+ code_lines = [line for line in lines if line.strip() and not line.strip().startswith("#")]
61
+ return len(code_lines)
62
+
63
+
64
+ def has_responsibility_keyword(class_name: str, keywords: list[str]) -> bool:
65
+ """Check if class name contains responsibility keywords.
66
+
67
+ Args:
68
+ class_name: Name of the class to check
69
+ keywords: List of keywords indicating potential SRP violations
70
+
71
+ Returns:
72
+ True if class name contains any responsibility keyword
73
+ """
74
+ return any(keyword in class_name for keyword in keywords)
75
+
76
+
77
+ def has_property_decorator(func_node: ast.FunctionDef | ast.AsyncFunctionDef) -> bool:
78
+ """Check if function has @property decorator.
79
+
80
+ Args:
81
+ func_node: AST node representing a function definition
82
+
83
+ Returns:
84
+ True if function has @property decorator
85
+ """
86
+ for decorator in func_node.decorator_list:
87
+ if isinstance(decorator, ast.Name) and decorator.id == "property":
88
+ return True
89
+ return False