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.
Files changed (52) hide show
  1. src/cli.py +646 -36
  2. src/config.py +6 -2
  3. src/core/base.py +90 -5
  4. src/core/config_parser.py +31 -4
  5. src/linters/dry/block_filter.py +5 -2
  6. src/linters/dry/cache.py +46 -92
  7. src/linters/dry/config.py +17 -13
  8. src/linters/dry/duplicate_storage.py +17 -80
  9. src/linters/dry/file_analyzer.py +11 -48
  10. src/linters/dry/linter.py +5 -12
  11. src/linters/dry/python_analyzer.py +188 -37
  12. src/linters/dry/storage_initializer.py +9 -18
  13. src/linters/dry/token_hasher.py +63 -9
  14. src/linters/dry/typescript_analyzer.py +7 -5
  15. src/linters/dry/violation_filter.py +4 -1
  16. src/linters/file_header/__init__.py +24 -0
  17. src/linters/file_header/atemporal_detector.py +87 -0
  18. src/linters/file_header/config.py +66 -0
  19. src/linters/file_header/field_validator.py +69 -0
  20. src/linters/file_header/linter.py +313 -0
  21. src/linters/file_header/python_parser.py +86 -0
  22. src/linters/file_header/violation_builder.py +78 -0
  23. src/linters/file_placement/linter.py +15 -4
  24. src/linters/magic_numbers/__init__.py +48 -0
  25. src/linters/magic_numbers/config.py +82 -0
  26. src/linters/magic_numbers/context_analyzer.py +247 -0
  27. src/linters/magic_numbers/linter.py +516 -0
  28. src/linters/magic_numbers/python_analyzer.py +76 -0
  29. src/linters/magic_numbers/typescript_analyzer.py +218 -0
  30. src/linters/magic_numbers/violation_builder.py +98 -0
  31. src/linters/nesting/__init__.py +6 -2
  32. src/linters/nesting/config.py +6 -3
  33. src/linters/nesting/linter.py +8 -19
  34. src/linters/nesting/typescript_analyzer.py +1 -0
  35. src/linters/print_statements/__init__.py +53 -0
  36. src/linters/print_statements/config.py +83 -0
  37. src/linters/print_statements/linter.py +430 -0
  38. src/linters/print_statements/python_analyzer.py +155 -0
  39. src/linters/print_statements/typescript_analyzer.py +135 -0
  40. src/linters/print_statements/violation_builder.py +98 -0
  41. src/linters/srp/__init__.py +3 -3
  42. src/linters/srp/config.py +12 -6
  43. src/linters/srp/linter.py +33 -24
  44. src/orchestrator/core.py +12 -2
  45. src/templates/thailint_config_template.yaml +158 -0
  46. src/utils/project_root.py +135 -16
  47. {thailint-0.2.0.dist-info → thailint-0.5.0.dist-info}/METADATA +387 -81
  48. thailint-0.5.0.dist-info/RECORD +96 -0
  49. {thailint-0.2.0.dist-info → thailint-0.5.0.dist-info}/WHEEL +1 -1
  50. thailint-0.2.0.dist-info/RECORD +0 -75
  51. {thailint-0.2.0.dist-info → thailint-0.5.0.dist-info}/entry_points.txt +0 -0
  52. {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
+ )
@@ -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 = 7,
43
- max_loc: int = 200,
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 = 7 # Maximum methods per class
32
- max_loc: int = 200 # Maximum lines of code per class
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("max_methods", config.get("max_methods", 7))
62
- max_loc = lang_config.get("max_loc", config.get("max_loc", 200))
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", 7)
65
- max_loc = config.get("max_loc", 200)
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, BaseLintRule
22
- from src.core.linter_utils import has_file_content, load_linter_config
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(BaseLintRule):
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
- if not self._should_check_file(context):
66
- return []
67
+ from src.core.linter_utils import has_file_content
67
68
 
68
- config = load_linter_config(context, "srp", SRPConfig)
69
- if not self._is_linter_enabled(context, config):
69
+ if not has_file_content(context):
70
70
  return []
71
71
 
72
- return self._check_by_language(context, config)
73
-
74
- def _should_check_file(self, context: BaseLintContext) -> bool:
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
- Returns:
81
- True if file should be checked
82
- """
83
- return has_file_content(context)
76
+ # Standard language dispatch
77
+ return self._dispatch_by_language(context, config)
84
78
 
85
- def _is_linter_enabled(self, context: BaseLintContext, config: SRPConfig) -> bool:
86
- """Check if linter is enabled and file is not ignored.
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 linter should run on this file
87
+ True if file should be processed
94
88
  """
95
- return config.enabled and not self._is_file_ignored(context, config)
89
+ if not config.enabled:
90
+ return False
91
+ return not self._is_file_ignored(context, config)
96
92
 
97
- def _check_by_language(self, context: BaseLintContext, config: SRPConfig) -> list[Violation]:
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
- # Auto-discover and register all linting rules from src.linters
105
- self.registry.discover_rules("src.linters")
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