thailint 0.2.0__py3-none-any.whl → 0.5.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- src/cli.py +646 -36
- src/config.py +6 -2
- src/core/base.py +90 -5
- src/core/config_parser.py +31 -4
- src/linters/dry/block_filter.py +5 -2
- src/linters/dry/cache.py +46 -92
- src/linters/dry/config.py +17 -13
- src/linters/dry/duplicate_storage.py +17 -80
- src/linters/dry/file_analyzer.py +11 -48
- src/linters/dry/linter.py +5 -12
- src/linters/dry/python_analyzer.py +188 -37
- src/linters/dry/storage_initializer.py +9 -18
- src/linters/dry/token_hasher.py +63 -9
- src/linters/dry/typescript_analyzer.py +7 -5
- src/linters/dry/violation_filter.py +4 -1
- src/linters/file_header/__init__.py +24 -0
- src/linters/file_header/atemporal_detector.py +87 -0
- src/linters/file_header/config.py +66 -0
- src/linters/file_header/field_validator.py +69 -0
- src/linters/file_header/linter.py +313 -0
- src/linters/file_header/python_parser.py +86 -0
- src/linters/file_header/violation_builder.py +78 -0
- src/linters/file_placement/linter.py +15 -4
- src/linters/magic_numbers/__init__.py +48 -0
- src/linters/magic_numbers/config.py +82 -0
- src/linters/magic_numbers/context_analyzer.py +247 -0
- src/linters/magic_numbers/linter.py +516 -0
- src/linters/magic_numbers/python_analyzer.py +76 -0
- src/linters/magic_numbers/typescript_analyzer.py +218 -0
- src/linters/magic_numbers/violation_builder.py +98 -0
- src/linters/nesting/__init__.py +6 -2
- src/linters/nesting/config.py +6 -3
- src/linters/nesting/linter.py +8 -19
- src/linters/nesting/typescript_analyzer.py +1 -0
- src/linters/print_statements/__init__.py +53 -0
- src/linters/print_statements/config.py +83 -0
- src/linters/print_statements/linter.py +430 -0
- src/linters/print_statements/python_analyzer.py +155 -0
- src/linters/print_statements/typescript_analyzer.py +135 -0
- src/linters/print_statements/violation_builder.py +98 -0
- src/linters/srp/__init__.py +3 -3
- src/linters/srp/config.py +12 -6
- src/linters/srp/linter.py +33 -24
- src/orchestrator/core.py +12 -2
- src/templates/thailint_config_template.yaml +158 -0
- src/utils/project_root.py +135 -16
- {thailint-0.2.0.dist-info → thailint-0.5.0.dist-info}/METADATA +387 -81
- thailint-0.5.0.dist-info/RECORD +96 -0
- {thailint-0.2.0.dist-info → thailint-0.5.0.dist-info}/WHEEL +1 -1
- thailint-0.2.0.dist-info/RECORD +0 -75
- {thailint-0.2.0.dist-info → thailint-0.5.0.dist-info}/entry_points.txt +0 -0
- {thailint-0.2.0.dist-info → thailint-0.5.0.dist-info/licenses}/LICENSE +0 -0
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
"""
|
|
2
|
+
File: src/linters/print_statements/typescript_analyzer.py
|
|
3
|
+
|
|
4
|
+
Purpose: TypeScript/JavaScript console.* call detection using Tree-sitter AST analysis
|
|
5
|
+
|
|
6
|
+
Exports: TypeScriptPrintStatementAnalyzer class
|
|
7
|
+
|
|
8
|
+
Depends: TypeScriptBaseAnalyzer for tree-sitter parsing, tree-sitter Node type
|
|
9
|
+
|
|
10
|
+
Implements: find_console_calls(root_node, methods) -> list[tuple]
|
|
11
|
+
|
|
12
|
+
Related: src/linters/magic_numbers/typescript_analyzer.py, src/analyzers/typescript_base.py
|
|
13
|
+
|
|
14
|
+
Overview: Analyzes TypeScript and JavaScript code to detect console.* method calls that should
|
|
15
|
+
be replaced with proper logging. Uses Tree-sitter parser to traverse TypeScript/JavaScript
|
|
16
|
+
AST and identify call expressions where the callee is console.log, console.warn, console.error,
|
|
17
|
+
console.debug, or console.info (configurable). Returns structured data with the node, method
|
|
18
|
+
name, and line number for each detected console call. Supports both TypeScript and JavaScript
|
|
19
|
+
files with shared detection logic.
|
|
20
|
+
|
|
21
|
+
Usage: analyzer = TypeScriptPrintStatementAnalyzer()
|
|
22
|
+
root = analyzer.parse_typescript(code)
|
|
23
|
+
calls = analyzer.find_console_calls(root, {"log", "warn", "error"})
|
|
24
|
+
|
|
25
|
+
Notes: Tree-sitter node traversal with call_expression and member_expression pattern matching
|
|
26
|
+
"""
|
|
27
|
+
|
|
28
|
+
import logging
|
|
29
|
+
from typing import Any
|
|
30
|
+
|
|
31
|
+
from src.analyzers.typescript_base import TypeScriptBaseAnalyzer
|
|
32
|
+
|
|
33
|
+
logger = logging.getLogger(__name__)
|
|
34
|
+
|
|
35
|
+
# dry: ignore-block - tree-sitter import pattern (common across TypeScript analyzers)
|
|
36
|
+
try:
|
|
37
|
+
from tree_sitter import Node
|
|
38
|
+
|
|
39
|
+
TREE_SITTER_AVAILABLE = True
|
|
40
|
+
except ImportError:
|
|
41
|
+
TREE_SITTER_AVAILABLE = False
|
|
42
|
+
Node = Any # type: ignore
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
class TypeScriptPrintStatementAnalyzer(TypeScriptBaseAnalyzer):
|
|
46
|
+
"""Analyzes TypeScript/JavaScript code for console.* calls using Tree-sitter."""
|
|
47
|
+
|
|
48
|
+
def find_console_calls(self, root_node: Node, methods: set[str]) -> list[tuple[Node, str, int]]:
|
|
49
|
+
"""Find all console.* calls matching the specified methods.
|
|
50
|
+
|
|
51
|
+
Args:
|
|
52
|
+
root_node: Root tree-sitter node to search from
|
|
53
|
+
methods: Set of console method names to detect (e.g., {"log", "warn"})
|
|
54
|
+
|
|
55
|
+
Returns:
|
|
56
|
+
List of (node, method_name, line_number) tuples for each console call
|
|
57
|
+
"""
|
|
58
|
+
logger.debug(
|
|
59
|
+
"find_console_calls: TREE_SITTER_AVAILABLE=%s, root_node=%s",
|
|
60
|
+
TREE_SITTER_AVAILABLE,
|
|
61
|
+
root_node is not None,
|
|
62
|
+
)
|
|
63
|
+
if not TREE_SITTER_AVAILABLE or root_node is None:
|
|
64
|
+
logger.debug("Early return: tree-sitter not available or root_node is None")
|
|
65
|
+
return []
|
|
66
|
+
|
|
67
|
+
calls: list[tuple[Node, str, int]] = []
|
|
68
|
+
self._collect_console_calls(root_node, methods, calls)
|
|
69
|
+
logger.debug("find_console_calls: found %d calls", len(calls))
|
|
70
|
+
return calls
|
|
71
|
+
|
|
72
|
+
def _collect_console_calls(
|
|
73
|
+
self, node: Node, methods: set[str], calls: list[tuple[Node, str, int]]
|
|
74
|
+
) -> None:
|
|
75
|
+
"""Recursively collect console.* calls from AST.
|
|
76
|
+
|
|
77
|
+
Args:
|
|
78
|
+
node: Current tree-sitter node
|
|
79
|
+
methods: Set of console method names to detect
|
|
80
|
+
calls: List to accumulate found calls
|
|
81
|
+
"""
|
|
82
|
+
if node.type == "call_expression":
|
|
83
|
+
method_name = self._extract_console_method(node, methods)
|
|
84
|
+
if method_name is not None:
|
|
85
|
+
line_number = node.start_point[0] + 1
|
|
86
|
+
calls.append((node, method_name, line_number))
|
|
87
|
+
|
|
88
|
+
for child in node.children:
|
|
89
|
+
self._collect_console_calls(child, methods, calls)
|
|
90
|
+
|
|
91
|
+
def _extract_console_method(self, node: Node, methods: set[str]) -> str | None:
|
|
92
|
+
"""Extract console method name if this is a console.* call.
|
|
93
|
+
|
|
94
|
+
Args:
|
|
95
|
+
node: Tree-sitter call_expression node
|
|
96
|
+
methods: Set of console method names to detect
|
|
97
|
+
|
|
98
|
+
Returns:
|
|
99
|
+
Method name if this is a matching console call, None otherwise
|
|
100
|
+
"""
|
|
101
|
+
func_node = self.find_child_by_type(node, "member_expression")
|
|
102
|
+
if func_node is None:
|
|
103
|
+
return None
|
|
104
|
+
if not self._is_console_object(func_node):
|
|
105
|
+
return None
|
|
106
|
+
return self._get_matching_method(func_node, methods)
|
|
107
|
+
|
|
108
|
+
def _is_console_object(self, func_node: Node) -> bool:
|
|
109
|
+
"""Check if the member expression is on 'console' object."""
|
|
110
|
+
object_node = self._find_object_node(func_node)
|
|
111
|
+
if object_node is None:
|
|
112
|
+
return False
|
|
113
|
+
return self.extract_node_text(object_node) == "console"
|
|
114
|
+
|
|
115
|
+
def _get_matching_method(self, func_node: Node, methods: set[str]) -> str | None:
|
|
116
|
+
"""Get method name if it matches the configured methods."""
|
|
117
|
+
method_node = self.find_child_by_type(func_node, "property_identifier")
|
|
118
|
+
if method_node is None:
|
|
119
|
+
return None
|
|
120
|
+
method_name = self.extract_node_text(method_node)
|
|
121
|
+
return method_name if method_name in methods else None
|
|
122
|
+
|
|
123
|
+
def _find_object_node(self, member_expr: Node) -> Node | None:
|
|
124
|
+
"""Find the object node in a member expression.
|
|
125
|
+
|
|
126
|
+
Args:
|
|
127
|
+
member_expr: Tree-sitter member_expression node
|
|
128
|
+
|
|
129
|
+
Returns:
|
|
130
|
+
Object node (identifier) or None
|
|
131
|
+
"""
|
|
132
|
+
for child in member_expr.children:
|
|
133
|
+
if child.type == "identifier":
|
|
134
|
+
return child
|
|
135
|
+
return None
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
"""
|
|
2
|
+
File: src/linters/print_statements/violation_builder.py
|
|
3
|
+
|
|
4
|
+
Purpose: Builds Violation objects for print statement detection
|
|
5
|
+
|
|
6
|
+
Exports: ViolationBuilder class
|
|
7
|
+
|
|
8
|
+
Depends: ast, pathlib.Path, src.core.types.Violation
|
|
9
|
+
|
|
10
|
+
Implements: ViolationBuilder.create_python_violation(node, line, file_path) -> Violation,
|
|
11
|
+
ViolationBuilder.create_typescript_violation(method, line, file_path) -> Violation
|
|
12
|
+
|
|
13
|
+
Related: src/linters/magic_numbers/violation_builder.py, src/core/types.py
|
|
14
|
+
|
|
15
|
+
Overview: Provides ViolationBuilder class that creates Violation objects for print statement
|
|
16
|
+
detections. Generates descriptive messages suggesting the use of proper logging instead of
|
|
17
|
+
print/console statements. Constructs complete Violation instances with rule_id, file_path,
|
|
18
|
+
line number, column, message, and suggestions. Provides separate methods for Python print()
|
|
19
|
+
violations and TypeScript/JavaScript console.* violations with language-appropriate messages.
|
|
20
|
+
|
|
21
|
+
Usage: builder = ViolationBuilder("print-statements.detected")
|
|
22
|
+
violation = builder.create_python_violation(node, line, file_path)
|
|
23
|
+
|
|
24
|
+
Notes: Message templates suggest logging as alternative, consistent with other linter patterns
|
|
25
|
+
"""
|
|
26
|
+
|
|
27
|
+
import ast
|
|
28
|
+
from pathlib import Path
|
|
29
|
+
|
|
30
|
+
from src.core.types import Violation
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
class ViolationBuilder:
|
|
34
|
+
"""Builds violations for print statement detections."""
|
|
35
|
+
|
|
36
|
+
def __init__(self, rule_id: str) -> None:
|
|
37
|
+
"""Initialize the violation builder.
|
|
38
|
+
|
|
39
|
+
Args:
|
|
40
|
+
rule_id: The rule ID to use in violations
|
|
41
|
+
"""
|
|
42
|
+
self.rule_id = rule_id
|
|
43
|
+
|
|
44
|
+
def create_python_violation(
|
|
45
|
+
self,
|
|
46
|
+
node: ast.Call,
|
|
47
|
+
line: int,
|
|
48
|
+
file_path: Path | None,
|
|
49
|
+
) -> Violation:
|
|
50
|
+
"""Create a violation for a Python print() call.
|
|
51
|
+
|
|
52
|
+
Args:
|
|
53
|
+
node: The AST Call node containing the print statement
|
|
54
|
+
line: Line number where the violation occurs
|
|
55
|
+
file_path: Path to the file
|
|
56
|
+
|
|
57
|
+
Returns:
|
|
58
|
+
Violation object with details about the print statement
|
|
59
|
+
"""
|
|
60
|
+
message = "print() statement should be replaced with proper logging"
|
|
61
|
+
suggestion = "Use logging.info(), logging.debug(), or similar instead of print()"
|
|
62
|
+
|
|
63
|
+
return Violation(
|
|
64
|
+
rule_id=self.rule_id,
|
|
65
|
+
file_path=str(file_path) if file_path else "",
|
|
66
|
+
line=line,
|
|
67
|
+
column=node.col_offset if hasattr(node, "col_offset") else 0,
|
|
68
|
+
message=message,
|
|
69
|
+
suggestion=suggestion,
|
|
70
|
+
)
|
|
71
|
+
|
|
72
|
+
def create_typescript_violation(
|
|
73
|
+
self,
|
|
74
|
+
method: str,
|
|
75
|
+
line: int,
|
|
76
|
+
file_path: Path | None,
|
|
77
|
+
) -> Violation:
|
|
78
|
+
"""Create a violation for a TypeScript/JavaScript console.* call.
|
|
79
|
+
|
|
80
|
+
Args:
|
|
81
|
+
method: The console method name (log, warn, error, etc.)
|
|
82
|
+
line: Line number where the violation occurs
|
|
83
|
+
file_path: Path to the file
|
|
84
|
+
|
|
85
|
+
Returns:
|
|
86
|
+
Violation object with details about the console statement
|
|
87
|
+
"""
|
|
88
|
+
message = f"console.{method}() should be replaced with proper logging"
|
|
89
|
+
suggestion = f"Use a logging library instead of console.{method}()"
|
|
90
|
+
|
|
91
|
+
return Violation(
|
|
92
|
+
rule_id=self.rule_id,
|
|
93
|
+
file_path=str(file_path) if file_path else "",
|
|
94
|
+
line=line,
|
|
95
|
+
column=0, # Tree-sitter nodes don't provide easy column access
|
|
96
|
+
message=message,
|
|
97
|
+
suggestion=suggestion,
|
|
98
|
+
)
|
src/linters/srp/__init__.py
CHANGED
|
@@ -22,7 +22,7 @@ Implementation: Simple re-export pattern for package interface, convenience func
|
|
|
22
22
|
from pathlib import Path
|
|
23
23
|
from typing import Any
|
|
24
24
|
|
|
25
|
-
from .config import SRPConfig
|
|
25
|
+
from .config import DEFAULT_MAX_LOC_PER_CLASS, DEFAULT_MAX_METHODS_PER_CLASS, SRPConfig
|
|
26
26
|
from .linter import SRPRule
|
|
27
27
|
from .python_analyzer import PythonSRPAnalyzer
|
|
28
28
|
from .typescript_analyzer import TypeScriptSRPAnalyzer
|
|
@@ -39,8 +39,8 @@ __all__ = [
|
|
|
39
39
|
def lint(
|
|
40
40
|
path: Path | str,
|
|
41
41
|
config: dict[str, Any] | None = None,
|
|
42
|
-
max_methods: int =
|
|
43
|
-
max_loc: int =
|
|
42
|
+
max_methods: int = DEFAULT_MAX_METHODS_PER_CLASS,
|
|
43
|
+
max_loc: int = DEFAULT_MAX_LOC_PER_CLASS,
|
|
44
44
|
) -> list:
|
|
45
45
|
"""Lint a file or directory for SRP violations.
|
|
46
46
|
|
src/linters/srp/config.py
CHANGED
|
@@ -23,13 +23,17 @@ Implementation: Dataclass with validation and defaults, heuristic-based SRP dete
|
|
|
23
23
|
from dataclasses import dataclass, field
|
|
24
24
|
from typing import Any
|
|
25
25
|
|
|
26
|
+
# Default SRP threshold constants
|
|
27
|
+
DEFAULT_MAX_METHODS_PER_CLASS = 7
|
|
28
|
+
DEFAULT_MAX_LOC_PER_CLASS = 200
|
|
29
|
+
|
|
26
30
|
|
|
27
31
|
@dataclass
|
|
28
32
|
class SRPConfig:
|
|
29
33
|
"""Configuration for SRP linter."""
|
|
30
34
|
|
|
31
|
-
max_methods: int =
|
|
32
|
-
max_loc: int =
|
|
35
|
+
max_methods: int = DEFAULT_MAX_METHODS_PER_CLASS # Maximum methods per class
|
|
36
|
+
max_loc: int = DEFAULT_MAX_LOC_PER_CLASS # Maximum lines of code per class
|
|
33
37
|
enabled: bool = True
|
|
34
38
|
check_keywords: bool = True
|
|
35
39
|
keywords: list[str] = field(
|
|
@@ -58,11 +62,13 @@ class SRPConfig:
|
|
|
58
62
|
# Get language-specific config if available
|
|
59
63
|
if language and language in config:
|
|
60
64
|
lang_config = config[language]
|
|
61
|
-
max_methods = lang_config.get(
|
|
62
|
-
|
|
65
|
+
max_methods = lang_config.get(
|
|
66
|
+
"max_methods", config.get("max_methods", DEFAULT_MAX_METHODS_PER_CLASS)
|
|
67
|
+
)
|
|
68
|
+
max_loc = lang_config.get("max_loc", config.get("max_loc", DEFAULT_MAX_LOC_PER_CLASS))
|
|
63
69
|
else:
|
|
64
|
-
max_methods = config.get("max_methods",
|
|
65
|
-
max_loc = config.get("max_loc",
|
|
70
|
+
max_methods = config.get("max_methods", DEFAULT_MAX_METHODS_PER_CLASS)
|
|
71
|
+
max_loc = config.get("max_loc", DEFAULT_MAX_LOC_PER_CLASS)
|
|
66
72
|
|
|
67
73
|
return cls(
|
|
68
74
|
max_methods=max_methods,
|
src/linters/srp/linter.py
CHANGED
|
@@ -18,8 +18,8 @@ Interfaces: SRPRule.check(context) -> list[Violation], properties for rule metad
|
|
|
18
18
|
Implementation: Composition pattern with helper classes, heuristic-based SRP analysis
|
|
19
19
|
"""
|
|
20
20
|
|
|
21
|
-
from src.core.base import BaseLintContext,
|
|
22
|
-
from src.core.linter_utils import
|
|
21
|
+
from src.core.base import BaseLintContext, MultiLanguageLintRule
|
|
22
|
+
from src.core.linter_utils import load_linter_config
|
|
23
23
|
from src.core.types import Violation
|
|
24
24
|
from src.linter_config.ignore import IgnoreDirectiveParser
|
|
25
25
|
|
|
@@ -29,7 +29,7 @@ from .metrics_evaluator import evaluate_metrics
|
|
|
29
29
|
from .violation_builder import ViolationBuilder
|
|
30
30
|
|
|
31
31
|
|
|
32
|
-
class SRPRule(
|
|
32
|
+
class SRPRule(MultiLanguageLintRule):
|
|
33
33
|
"""Detects Single Responsibility Principle violations in classes."""
|
|
34
34
|
|
|
35
35
|
def __init__(self) -> None:
|
|
@@ -54,7 +54,9 @@ class SRPRule(BaseLintRule):
|
|
|
54
54
|
return "Classes should have a single, well-defined responsibility"
|
|
55
55
|
|
|
56
56
|
def check(self, context: BaseLintContext) -> list[Violation]:
|
|
57
|
-
"""Check for SRP violations.
|
|
57
|
+
"""Check for SRP violations with custom ignore pattern handling.
|
|
58
|
+
|
|
59
|
+
Overrides parent to add file-level ignore pattern checking before dispatch.
|
|
58
60
|
|
|
59
61
|
Args:
|
|
60
62
|
context: Lint context with file information
|
|
@@ -62,39 +64,33 @@ class SRPRule(BaseLintRule):
|
|
|
62
64
|
Returns:
|
|
63
65
|
List of violations found
|
|
64
66
|
"""
|
|
65
|
-
|
|
66
|
-
return []
|
|
67
|
+
from src.core.linter_utils import has_file_content
|
|
67
68
|
|
|
68
|
-
|
|
69
|
-
if not self._is_linter_enabled(context, config):
|
|
69
|
+
if not has_file_content(context):
|
|
70
70
|
return []
|
|
71
71
|
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
"""Check if file has content to analyze.
|
|
76
|
-
|
|
77
|
-
Args:
|
|
78
|
-
context: Lint context
|
|
72
|
+
config = self._load_config(context)
|
|
73
|
+
if not self._should_process_file(context, config):
|
|
74
|
+
return []
|
|
79
75
|
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
"""
|
|
83
|
-
return has_file_content(context)
|
|
76
|
+
# Standard language dispatch
|
|
77
|
+
return self._dispatch_by_language(context, config)
|
|
84
78
|
|
|
85
|
-
def
|
|
86
|
-
"""Check if
|
|
79
|
+
def _should_process_file(self, context: BaseLintContext, config: SRPConfig) -> bool:
|
|
80
|
+
"""Check if file should be processed.
|
|
87
81
|
|
|
88
82
|
Args:
|
|
89
83
|
context: Lint context
|
|
90
84
|
config: SRP configuration
|
|
91
85
|
|
|
92
86
|
Returns:
|
|
93
|
-
True if
|
|
87
|
+
True if file should be processed
|
|
94
88
|
"""
|
|
95
|
-
|
|
89
|
+
if not config.enabled:
|
|
90
|
+
return False
|
|
91
|
+
return not self._is_file_ignored(context, config)
|
|
96
92
|
|
|
97
|
-
def
|
|
93
|
+
def _dispatch_by_language(self, context: BaseLintContext, config: SRPConfig) -> list[Violation]:
|
|
98
94
|
"""Dispatch to language-specific checker.
|
|
99
95
|
|
|
100
96
|
Args:
|
|
@@ -106,10 +102,23 @@ class SRPRule(BaseLintRule):
|
|
|
106
102
|
"""
|
|
107
103
|
if context.language == "python":
|
|
108
104
|
return self._check_python(context, config)
|
|
105
|
+
|
|
109
106
|
if context.language in ("typescript", "javascript"):
|
|
110
107
|
return self._check_typescript(context, config)
|
|
108
|
+
|
|
111
109
|
return []
|
|
112
110
|
|
|
111
|
+
def _load_config(self, context: BaseLintContext) -> SRPConfig:
|
|
112
|
+
"""Load configuration from context.
|
|
113
|
+
|
|
114
|
+
Args:
|
|
115
|
+
context: Lint context
|
|
116
|
+
|
|
117
|
+
Returns:
|
|
118
|
+
SRPConfig instance
|
|
119
|
+
"""
|
|
120
|
+
return load_linter_config(context, "srp", SRPConfig)
|
|
121
|
+
|
|
113
122
|
def _is_file_ignored(self, context: BaseLintContext, config: SRPConfig) -> bool:
|
|
114
123
|
"""Check if file matches ignore patterns.
|
|
115
124
|
|
src/orchestrator/core.py
CHANGED
|
@@ -101,8 +101,9 @@ class Orchestrator:
|
|
|
101
101
|
self.config_loader = LinterConfigLoader()
|
|
102
102
|
self.ignore_parser = IgnoreDirectiveParser(self.project_root)
|
|
103
103
|
|
|
104
|
-
#
|
|
105
|
-
|
|
104
|
+
# Performance optimization: Defer rule discovery until first file is linted
|
|
105
|
+
# This eliminates ~0.077s overhead for commands that don't need rules (--help, config, etc.)
|
|
106
|
+
self._rules_discovered = False
|
|
106
107
|
|
|
107
108
|
# Use provided config or load from project root
|
|
108
109
|
if config is not None:
|
|
@@ -208,6 +209,12 @@ class Orchestrator:
|
|
|
208
209
|
|
|
209
210
|
return violations
|
|
210
211
|
|
|
212
|
+
def _ensure_rules_discovered(self) -> None:
|
|
213
|
+
"""Ensure rules have been discovered and registered (lazy initialization)."""
|
|
214
|
+
if not self._rules_discovered:
|
|
215
|
+
self.registry.discover_rules("src.linters")
|
|
216
|
+
self._rules_discovered = True
|
|
217
|
+
|
|
211
218
|
def _get_rules_for_file(self, file_path: Path, language: str) -> list[BaseLintRule]:
|
|
212
219
|
"""Get rules applicable to this file.
|
|
213
220
|
|
|
@@ -218,6 +225,9 @@ class Orchestrator:
|
|
|
218
225
|
Returns:
|
|
219
226
|
List of rules to execute against this file.
|
|
220
227
|
"""
|
|
228
|
+
# Lazy initialization: discover rules on first lint operation
|
|
229
|
+
self._ensure_rules_discovered()
|
|
230
|
+
|
|
221
231
|
# For now, return all registered rules
|
|
222
232
|
# Future: filter by language, configuration, etc.
|
|
223
233
|
return self.registry.list_all()
|
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
# thai-lint Configuration File
|
|
2
|
+
# Generated by: thailint init-config
|
|
3
|
+
#
|
|
4
|
+
# For non-interactive mode (AI agents): thailint init-config --non-interactive
|
|
5
|
+
#
|
|
6
|
+
# Full documentation: https://github.com/your-org/thai-lint
|
|
7
|
+
|
|
8
|
+
# ============================================================================
|
|
9
|
+
# MAGIC NUMBERS LINTER
|
|
10
|
+
# ============================================================================
|
|
11
|
+
# Detects unnamed numeric literals that should be extracted as constants
|
|
12
|
+
#
|
|
13
|
+
# Preset: {{PRESET}}
|
|
14
|
+
#
|
|
15
|
+
magic-numbers:
|
|
16
|
+
enabled: true
|
|
17
|
+
|
|
18
|
+
# Numbers that are acceptable without being named constants
|
|
19
|
+
# Default: [-1, 0, 1, 2, 3, 4, 5, 10, 100, 1000]
|
|
20
|
+
allowed_numbers: {{ALLOWED_NUMBERS}}
|
|
21
|
+
|
|
22
|
+
# Maximum integer allowed in range() or enumerate() without flagging
|
|
23
|
+
# Default: 10
|
|
24
|
+
max_small_integer: {{MAX_SMALL_INTEGER}}
|
|
25
|
+
|
|
26
|
+
# -------------------------------------------------------------------------
|
|
27
|
+
# OPTIONAL: Uncomment to add time conversions (lenient mode)
|
|
28
|
+
# -------------------------------------------------------------------------
|
|
29
|
+
# allowed_numbers: [-1, 0, 1, 2, 3, 4, 5, 10, 60, 100, 1000, 3600]
|
|
30
|
+
|
|
31
|
+
# -------------------------------------------------------------------------
|
|
32
|
+
# OPTIONAL: Uncomment to add common HTTP status codes
|
|
33
|
+
# -------------------------------------------------------------------------
|
|
34
|
+
# allowed_numbers: [-1, 0, 1, 2, 3, 4, 5, 10, 100, 200, 201, 204, 400, 401, 403, 404, 500, 502, 503, 1000]
|
|
35
|
+
|
|
36
|
+
# -------------------------------------------------------------------------
|
|
37
|
+
# OPTIONAL: Uncomment to add decimal proportions (0.0-1.0)
|
|
38
|
+
# -------------------------------------------------------------------------
|
|
39
|
+
# allowed_numbers: [-1, 0, 1, 2, 3, 4, 5, 10, 100, 1000, 0.0, 0.1, 0.2, 0.25, 0.3, 0.4, 0.5, 0.6, 0.7, 0.75, 0.8, 0.9, 1.0]
|
|
40
|
+
|
|
41
|
+
# ============================================================================
|
|
42
|
+
# NESTING LINTER
|
|
43
|
+
# ============================================================================
|
|
44
|
+
# Checks for excessive nesting depth (if/for/while/try statements)
|
|
45
|
+
#
|
|
46
|
+
nesting:
|
|
47
|
+
enabled: true
|
|
48
|
+
|
|
49
|
+
# Maximum nesting depth allowed
|
|
50
|
+
# Default: 4
|
|
51
|
+
max_nesting_depth: 4
|
|
52
|
+
|
|
53
|
+
# ============================================================================
|
|
54
|
+
# SINGLE RESPONSIBILITY PRINCIPLE (SRP) LINTER
|
|
55
|
+
# ============================================================================
|
|
56
|
+
# Detects classes that may have too many responsibilities
|
|
57
|
+
#
|
|
58
|
+
srp:
|
|
59
|
+
enabled: true
|
|
60
|
+
|
|
61
|
+
# Maximum methods per class
|
|
62
|
+
# Default: 7
|
|
63
|
+
max_methods: 7
|
|
64
|
+
|
|
65
|
+
# Maximum lines of code per class
|
|
66
|
+
# Default: 200
|
|
67
|
+
max_loc: 200
|
|
68
|
+
|
|
69
|
+
# ============================================================================
|
|
70
|
+
# DRY (DON'T REPEAT YOURSELF) LINTER
|
|
71
|
+
# ============================================================================
|
|
72
|
+
# Detects duplicate code blocks
|
|
73
|
+
#
|
|
74
|
+
dry:
|
|
75
|
+
enabled: true
|
|
76
|
+
|
|
77
|
+
# Minimum lines for a block to be considered duplicate
|
|
78
|
+
# Default: 6
|
|
79
|
+
min_duplicate_lines: 6
|
|
80
|
+
|
|
81
|
+
# Enable SQLite caching for faster incremental scans
|
|
82
|
+
# Default: true
|
|
83
|
+
cache_enabled: true
|
|
84
|
+
|
|
85
|
+
# Cache file location (relative to project root)
|
|
86
|
+
# Default: .thailint-cache/dry.db
|
|
87
|
+
cache_path: .thailint-cache/dry.db
|
|
88
|
+
|
|
89
|
+
# ============================================================================
|
|
90
|
+
# FILE PLACEMENT LINTER
|
|
91
|
+
# ============================================================================
|
|
92
|
+
# Ensures files are in appropriate directories
|
|
93
|
+
#
|
|
94
|
+
file-placement:
|
|
95
|
+
enabled: true
|
|
96
|
+
|
|
97
|
+
# Rules for file placement
|
|
98
|
+
rules:
|
|
99
|
+
# Test files should be in tests/ directory
|
|
100
|
+
- pattern: "test_*.py"
|
|
101
|
+
required_dir: "tests/"
|
|
102
|
+
message: "Test files must be in tests/ directory"
|
|
103
|
+
|
|
104
|
+
# Config files should be in config/ or root
|
|
105
|
+
- pattern: "*config*.py"
|
|
106
|
+
required_dir: ["config/", "./"]
|
|
107
|
+
message: "Config files should be in config/ or project root"
|
|
108
|
+
|
|
109
|
+
# ============================================================================
|
|
110
|
+
# PRINT STATEMENTS LINTER
|
|
111
|
+
# ============================================================================
|
|
112
|
+
# Detects print()/console.* statements that should use proper logging
|
|
113
|
+
#
|
|
114
|
+
print-statements:
|
|
115
|
+
enabled: true
|
|
116
|
+
|
|
117
|
+
# Allow print() in if __name__ == "__main__": blocks (Python only)
|
|
118
|
+
# Default: true
|
|
119
|
+
allow_in_scripts: true
|
|
120
|
+
|
|
121
|
+
# Console methods to detect in TypeScript/JavaScript
|
|
122
|
+
# Default: [log, warn, error, debug, info]
|
|
123
|
+
console_methods:
|
|
124
|
+
- log
|
|
125
|
+
- warn
|
|
126
|
+
- error
|
|
127
|
+
- debug
|
|
128
|
+
- info
|
|
129
|
+
|
|
130
|
+
# File patterns to ignore (glob syntax)
|
|
131
|
+
# ignore:
|
|
132
|
+
# - "scripts/**"
|
|
133
|
+
# - "**/debug.py"
|
|
134
|
+
|
|
135
|
+
# ============================================================================
|
|
136
|
+
# GLOBAL SETTINGS
|
|
137
|
+
# ============================================================================
|
|
138
|
+
#
|
|
139
|
+
# Exclude patterns (files/directories to ignore)
|
|
140
|
+
exclude:
|
|
141
|
+
- ".git/"
|
|
142
|
+
- ".venv/"
|
|
143
|
+
- "venv/"
|
|
144
|
+
- "node_modules/"
|
|
145
|
+
- "__pycache__/"
|
|
146
|
+
- "*.pyc"
|
|
147
|
+
- ".pytest_cache/"
|
|
148
|
+
- "dist/"
|
|
149
|
+
- "build/"
|
|
150
|
+
- ".eggs/"
|
|
151
|
+
|
|
152
|
+
# Output format (text or json)
|
|
153
|
+
# Default: text
|
|
154
|
+
output_format: text
|
|
155
|
+
|
|
156
|
+
# Exit with error code if violations found
|
|
157
|
+
# Default: true
|
|
158
|
+
fail_on_violations: true
|