thailint 0.15.6__py3-none-any.whl → 0.16.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/utils.py CHANGED
@@ -21,7 +21,6 @@ Implementation: Uses Click decorators for option definitions, deferred imports f
21
21
  to support test environments, caches project root in context for efficiency
22
22
  """
23
23
 
24
- import logging
25
24
  import sys
26
25
  from collections.abc import Callable
27
26
  from contextlib import suppress
@@ -29,13 +28,11 @@ from pathlib import Path
29
28
  from typing import TYPE_CHECKING, Any, TypeVar, cast
30
29
 
31
30
  import click
31
+ from loguru import logger
32
32
 
33
33
  if TYPE_CHECKING:
34
34
  from src.orchestrator.core import Orchestrator
35
35
 
36
- # Configure module logger
37
- logger = logging.getLogger(__name__)
38
-
39
36
 
40
37
  # =============================================================================
41
38
  # Common Option Decorators
@@ -132,8 +129,7 @@ def _resolve_explicit_project_root(explicit_root: str, verbose: bool) -> Path:
132
129
  # Now resolve after validation
133
130
  root = root.resolve()
134
131
 
135
- if verbose:
136
- logger.debug(f"Using explicit project root: {root}")
132
+ logger.debug(f"Using explicit project root: {root}")
137
133
  return root
138
134
 
139
135
 
@@ -150,8 +146,7 @@ def _infer_root_from_config(config_path: str, verbose: bool) -> Path:
150
146
  config_file = Path(config_path).resolve()
151
147
  inferred_root = config_file.parent
152
148
 
153
- if verbose:
154
- logger.debug(f"Inferred project root from config path: {inferred_root}")
149
+ logger.debug(f"Inferred project root from config path: {inferred_root}")
155
150
  return inferred_root
156
151
 
157
152
 
@@ -168,8 +163,7 @@ def _autodetect_project_root(
168
163
  Auto-detected project root
169
164
  """
170
165
  auto_root = get_project_root(None)
171
- if verbose:
172
- logger.debug(f"Auto-detected project root: {auto_root}")
166
+ logger.debug(f"Auto-detected project root: {auto_root}")
173
167
  return auto_root
174
168
 
175
169
 
@@ -217,8 +211,7 @@ def _determine_project_root_for_context(ctx: click.Context) -> Path | None:
217
211
  return _infer_root_from_config(config_path, verbose)
218
212
 
219
213
  # No explicit root - return None for auto-detection from target paths
220
- if verbose:
221
- logger.debug("No explicit project root, will auto-detect from target paths")
214
+ logger.debug("No explicit project root, will auto-detect from target paths")
222
215
  return None
223
216
 
224
217
 
@@ -262,8 +255,7 @@ def handle_linting_error(error: Exception, verbose: bool) -> None:
262
255
  verbose: Whether verbose logging is enabled
263
256
  """
264
257
  click.echo(f"Error during linting: {error}", err=True)
265
- if verbose:
266
- logger.exception("Linting failed with exception")
258
+ logger.exception("Linting failed with exception")
267
259
  sys.exit(2)
268
260
 
269
261
 
@@ -334,8 +326,7 @@ def load_config_file(orchestrator: "Orchestrator", config_file: str, verbose: bo
334
326
  # Load config into orchestrator
335
327
  orchestrator.config = orchestrator.config_loader.load(config_path)
336
328
 
337
- if verbose:
338
- logger.debug(f"Loaded config from: {config_file}")
329
+ logger.debug(f"Loaded config from: {config_file}")
339
330
 
340
331
 
341
332
  # =============================================================================
src/core/__init__.py CHANGED
@@ -6,12 +6,26 @@ power the plugin architecture.
6
6
 
7
7
  from .base import BaseLintContext, BaseLintRule
8
8
  from .registry import RuleRegistry
9
+ from .rule_aliases import (
10
+ LINTER_ALIASES,
11
+ RULE_ID_ALIASES,
12
+ is_deprecated_linter,
13
+ is_deprecated_rule_id,
14
+ resolve_linter_name,
15
+ resolve_rule_id,
16
+ )
9
17
  from .types import Severity, Violation
10
18
 
11
19
  __all__ = [
12
20
  "BaseLintContext",
13
21
  "BaseLintRule",
22
+ "LINTER_ALIASES",
23
+ "RULE_ID_ALIASES",
14
24
  "RuleRegistry",
15
25
  "Severity",
16
26
  "Violation",
27
+ "is_deprecated_linter",
28
+ "is_deprecated_rule_id",
29
+ "resolve_linter_name",
30
+ "resolve_rule_id",
17
31
  ]
@@ -0,0 +1,84 @@
1
+ """
2
+ Purpose: Rule ID aliasing system for backward compatibility during rule renaming
3
+
4
+ Scope: Maps deprecated rule IDs to canonical rule IDs for configuration and filtering
5
+
6
+ Overview: Provides a mapping system for rule IDs that allows backward compatibility when rules
7
+ are renamed. Users can continue using deprecated rule IDs in configuration files and ignore
8
+ directives, which are transparently resolved to their canonical forms. Supports both
9
+ direct rule ID mapping and linter-level command aliasing. Used by configuration parsing,
10
+ violation filtering, and ignore directive processing.
11
+
12
+ Dependencies: None (pure Python module)
13
+
14
+ Exports: RULE_ID_ALIASES dict, LINTER_ALIASES dict, resolve_rule_id function,
15
+ resolve_linter_name function
16
+
17
+ Interfaces: resolve_rule_id(rule_id) -> str, resolve_linter_name(name) -> str
18
+
19
+ Implementation: Simple dictionary-based lookup with identity fallback for unknown rule IDs
20
+ """
21
+
22
+ # Maps deprecated rule IDs to their canonical replacements
23
+ RULE_ID_ALIASES: dict[str, str] = {
24
+ "print-statements.detected": "improper-logging.print-statement",
25
+ }
26
+
27
+ # Maps deprecated linter command names to their canonical replacements
28
+ LINTER_ALIASES: dict[str, str] = {
29
+ "print-statements": "improper-logging",
30
+ }
31
+
32
+
33
+ def resolve_rule_id(rule_id: str) -> str:
34
+ """Resolve a rule ID to its canonical form.
35
+
36
+ If the rule ID has been renamed, returns the new canonical name.
37
+ Otherwise, returns the rule ID unchanged.
38
+
39
+ Args:
40
+ rule_id: The rule ID to resolve (may be deprecated or canonical)
41
+
42
+ Returns:
43
+ The canonical rule ID
44
+ """
45
+ return RULE_ID_ALIASES.get(rule_id, rule_id)
46
+
47
+
48
+ def resolve_linter_name(name: str) -> str:
49
+ """Resolve a linter command name to its canonical form.
50
+
51
+ If the linter has been renamed, returns the new canonical name.
52
+ Otherwise, returns the name unchanged.
53
+
54
+ Args:
55
+ name: The linter command name to resolve (may be deprecated or canonical)
56
+
57
+ Returns:
58
+ The canonical linter name
59
+ """
60
+ return LINTER_ALIASES.get(name, name)
61
+
62
+
63
+ def is_deprecated_rule_id(rule_id: str) -> bool:
64
+ """Check if a rule ID is deprecated.
65
+
66
+ Args:
67
+ rule_id: The rule ID to check
68
+
69
+ Returns:
70
+ True if the rule ID is deprecated (has an alias)
71
+ """
72
+ return rule_id in RULE_ID_ALIASES
73
+
74
+
75
+ def is_deprecated_linter(name: str) -> bool:
76
+ """Check if a linter name is deprecated.
77
+
78
+ Args:
79
+ name: The linter name to check
80
+
81
+ Returns:
82
+ True if the linter name is deprecated (has an alias)
83
+ """
84
+ return name in LINTER_ALIASES
@@ -4,32 +4,46 @@ Purpose: Rule ID matching utilities for ignore directive processing
4
4
  Scope: Pattern matching between rule IDs and ignore patterns
5
5
 
6
6
  Overview: Provides functions for matching rule IDs against ignore patterns. Supports
7
- exact matching, wildcard matching (*.suffix), and prefix matching (category matches
8
- category.specific). All comparisons are case-insensitive to handle variations in
9
- rule ID formatting.
7
+ exact matching, wildcard matching (*.suffix), prefix matching (category matches
8
+ category.specific), and alias resolution for backward compatibility with renamed
9
+ rules. All comparisons are case-insensitive to handle variations in rule ID formatting.
10
10
 
11
- Dependencies: re for regex operations
11
+ Dependencies: re for regex operations, src.core.rule_aliases for alias resolution
12
12
 
13
13
  Exports: rule_matches, check_bracket_rules, check_space_separated_rules
14
14
 
15
15
  Interfaces: rule_matches(rule_id, pattern) -> bool for checking if rule matches pattern
16
16
 
17
- Implementation: String-based pattern matching with wildcard and prefix support
17
+ Implementation: String-based pattern matching with wildcard, prefix, and alias support
18
18
  """
19
19
 
20
20
  import re
21
21
 
22
+ from src.core.rule_aliases import RULE_ID_ALIASES
23
+
22
24
 
23
25
  def rule_matches(rule_id: str, pattern: str) -> bool:
24
- """Check if rule ID matches pattern (supports wildcards and prefixes).
26
+ """Check if rule ID matches pattern (supports wildcards, prefixes, and aliases).
27
+
28
+ Supports backward compatibility through alias resolution:
29
+ - Pattern "print-statements" matches rule_id "improper-logging.print-statement"
30
+ - Pattern "print-statements.*" matches rule_id "improper-logging.print-statement"
25
31
 
26
32
  Args:
27
- rule_id: Rule ID to check (e.g., "nesting.excessive-depth").
28
- pattern: Pattern with optional wildcard (e.g., "nesting.*" or "nesting").
33
+ rule_id: Rule ID to check (e.g., "improper-logging.print-statement").
34
+ pattern: Pattern with optional wildcard (e.g., "nesting.*" or "print-statements").
29
35
 
30
36
  Returns:
31
37
  True if rule matches pattern.
32
38
  """
39
+ if _matches_pattern_directly(rule_id, pattern):
40
+ return True
41
+
42
+ return _matches_via_alias(rule_id, pattern)
43
+
44
+
45
+ def _matches_pattern_directly(rule_id: str, pattern: str) -> bool:
46
+ """Check if rule ID matches pattern without alias resolution."""
33
47
  rule_id_lower = rule_id.lower()
34
48
  pattern_lower = pattern.lower()
35
49
 
@@ -46,6 +60,37 @@ def rule_matches(rule_id: str, pattern: str) -> bool:
46
60
  return False
47
61
 
48
62
 
63
+ def _matches_via_alias(rule_id: str, pattern: str) -> bool:
64
+ """Check if rule ID matches pattern through alias resolution."""
65
+ pattern_lower = pattern.lower()
66
+ rule_id_lower = rule_id.lower()
67
+
68
+ # Find deprecated IDs that alias to our rule_id and check pattern match
69
+ return any(
70
+ _pattern_matches_deprecated_id(pattern_lower, deprecated_id)
71
+ for deprecated_id, canonical_id in RULE_ID_ALIASES.items()
72
+ if canonical_id.lower() == rule_id_lower
73
+ )
74
+
75
+
76
+ def _pattern_matches_deprecated_id(pattern_lower: str, deprecated_id: str) -> bool:
77
+ """Check if pattern matches a deprecated rule ID."""
78
+ deprecated_id_lower = deprecated_id.lower()
79
+
80
+ # Pattern exactly matches deprecated ID
81
+ if pattern_lower == deprecated_id_lower:
82
+ return True
83
+
84
+ # Pattern is prefix/wildcard that matches deprecated ID's category
85
+ deprecated_category = deprecated_id.split(".", maxsplit=1)[0].lower()
86
+ if pattern_lower == deprecated_category:
87
+ return True
88
+ if pattern_lower == deprecated_category + ".*":
89
+ return True
90
+
91
+ return False
92
+
93
+
49
94
  def check_bracket_rules(rules_text: str, rule_id: str) -> bool:
50
95
  """Check if bracketed rules match the rule ID.
51
96
 
@@ -1,33 +1,45 @@
1
1
  """
2
2
  File: src/linters/print_statements/__init__.py
3
3
 
4
- Purpose: Print statements linter package exports and convenience functions
4
+ Purpose: Improper logging linter package exports and convenience functions
5
5
 
6
- Exports: PrintStatementRule class, PrintStatementConfig dataclass, lint() convenience function
6
+ Exports: PrintStatementRule, ConditionalVerboseRule classes, PrintStatementConfig dataclass,
7
+ lint() convenience function, ImproperLoggingPrintRule alias
7
8
 
8
- Depends: .linter for PrintStatementRule, .config for PrintStatementConfig
9
+ Depends: .linter for PrintStatementRule, .conditional_verbose_rule for ConditionalVerboseRule,
10
+ .config for PrintStatementConfig
9
11
 
10
12
  Implements: lint(file_path, config) -> list[Violation] for simple linting operations
11
13
 
12
14
  Related: src/linters/magic_numbers/__init__.py, src/core/base.py
13
15
 
14
- Overview: Provides the public interface for the print statements linter package. Exports main
15
- PrintStatementRule class for use by the orchestrator and PrintStatementConfig for configuration.
16
- Includes lint() convenience function that provides a simple API for running the print statements
17
- linter on a file without directly interacting with the orchestrator. This module serves as the
18
- entry point for users of the print statements linter, hiding implementation details and exposing
19
- only the essential components needed for linting operations.
16
+ Overview: Provides the public interface for the improper logging linter package (formerly
17
+ print-statements). Exports PrintStatementRule for detecting print/console statements and
18
+ ConditionalVerboseRule for detecting conditional verbose logging anti-patterns. Both rules
19
+ use rule IDs prefixed with 'improper-logging.' for unified filtering. Includes lint()
20
+ convenience function for simple API usage without the orchestrator. ImproperLoggingPrintRule
21
+ is provided as an alias for PrintStatementRule for semantic clarity.
20
22
 
21
- Usage: from src.linters.print_statements import PrintStatementRule, lint
23
+ Usage: from src.linters.print_statements import PrintStatementRule, ConditionalVerboseRule, lint
22
24
  violations = lint("path/to/file.py")
23
25
 
24
26
  Notes: Module-level exports with __all__ definition, convenience function wrapper
25
27
  """
26
28
 
29
+ from .conditional_verbose_rule import ConditionalVerboseRule
27
30
  from .config import PrintStatementConfig
28
31
  from .linter import PrintStatementRule
29
32
 
30
- __all__ = ["PrintStatementRule", "PrintStatementConfig", "lint"]
33
+ # Alias for semantic clarity (both detect improper logging patterns)
34
+ ImproperLoggingPrintRule = PrintStatementRule
35
+
36
+ __all__ = [
37
+ "PrintStatementRule",
38
+ "ConditionalVerboseRule",
39
+ "PrintStatementConfig",
40
+ "ImproperLoggingPrintRule",
41
+ "lint",
42
+ ]
31
43
 
32
44
 
33
45
  def lint(file_path: str, config: dict | None = None) -> list:
@@ -0,0 +1,200 @@
1
+ """
2
+ Purpose: Python AST analysis for finding conditional verbose logging patterns
3
+
4
+ Scope: Detection of if verbose: logger.*() anti-patterns in Python code
5
+
6
+ Overview: Provides ConditionalVerboseAnalyzer class that traverses Python AST to find logging calls
7
+ that are conditionally guarded by verbose flags. Detects patterns like 'if verbose: logger.debug()'
8
+ or 'if self.verbose: logger.info()' which are anti-patterns because logging levels should be
9
+ configured at the logger level rather than through code conditionals. Supports detection of
10
+ various verbose condition patterns including simple names, attribute access, dict access, and
11
+ method calls on context objects.
12
+
13
+ Dependencies: ast module for AST parsing and node types
14
+
15
+ Exports: ConditionalVerboseAnalyzer class, is_verbose_condition function, is_logger_call function
16
+
17
+ Interfaces: find_conditional_verbose_calls(tree) -> list[tuple[If, Call, int]]
18
+
19
+ Implementation: AST walk pattern with condition matching for verbose patterns and logger call detection
20
+ """
21
+
22
+ import ast
23
+
24
+ # Logger methods that indicate a logging call
25
+ LOGGER_METHODS = frozenset({"debug", "info", "warning", "error", "critical", "log", "exception"})
26
+
27
+ # Verbose-related names that typically guard logging
28
+ VERBOSE_NAMES = frozenset({"verbose", "debug", "is_verbose", "is_debug"})
29
+
30
+
31
+ def is_verbose_condition(test: ast.expr) -> bool:
32
+ """Check if an expression is a verbose-like condition.
33
+
34
+ Matches patterns like:
35
+ - verbose
36
+ - self.verbose
37
+ - config.verbose
38
+ - params.verbose
39
+ - ctx.obj.get("verbose")
40
+ - ctx.obj["verbose"]
41
+
42
+ Args:
43
+ test: The condition expression to check
44
+
45
+ Returns:
46
+ True if the condition appears to be a verbose check
47
+ """
48
+ return (
49
+ _is_simple_verbose_name(test)
50
+ or _is_verbose_attribute(test)
51
+ or _is_verbose_subscript(test)
52
+ or _is_verbose_dict_get(test)
53
+ )
54
+
55
+
56
+ def _is_simple_verbose_name(test: ast.expr) -> bool:
57
+ """Check for simple name like 'verbose' or 'debug'."""
58
+ return isinstance(test, ast.Name) and test.id.lower() in VERBOSE_NAMES
59
+
60
+
61
+ def _is_verbose_attribute(test: ast.expr) -> bool:
62
+ """Check for attribute access like 'self.verbose' or 'config.verbose'."""
63
+ if not isinstance(test, ast.Attribute):
64
+ return False
65
+ return test.attr.lower() in VERBOSE_NAMES
66
+
67
+
68
+ def _is_verbose_subscript(test: ast.expr) -> bool:
69
+ """Check for subscript access like 'ctx.obj["verbose"]'."""
70
+ if not isinstance(test, ast.Subscript):
71
+ return False
72
+ if not isinstance(test.slice, ast.Constant):
73
+ return False
74
+ value = test.slice.value
75
+ return isinstance(value, str) and value.lower() in VERBOSE_NAMES
76
+
77
+
78
+ def _is_verbose_dict_get(test: ast.expr) -> bool:
79
+ """Check for dict.get call like 'ctx.obj.get("verbose")'."""
80
+ if not isinstance(test, ast.Call):
81
+ return False
82
+ if not _is_dict_get_call_with_args(test):
83
+ return False
84
+ return _first_arg_is_verbose_string(test.args[0])
85
+
86
+
87
+ def _is_dict_get_call_with_args(call: ast.Call) -> bool:
88
+ """Check if call is a .get() method call with arguments."""
89
+ if not isinstance(call.func, ast.Attribute):
90
+ return False
91
+ if call.func.attr != "get":
92
+ return False
93
+ return bool(call.args)
94
+
95
+
96
+ def _first_arg_is_verbose_string(arg: ast.expr) -> bool:
97
+ """Check if argument is a verbose-related string constant."""
98
+ if not isinstance(arg, ast.Constant):
99
+ return False
100
+ value = arg.value
101
+ return isinstance(value, str) and value.lower() in VERBOSE_NAMES
102
+
103
+
104
+ def is_logger_call(node: ast.Call) -> bool:
105
+ """Check if a Call node is a logger method call.
106
+
107
+ Matches patterns like:
108
+ - logger.debug()
109
+ - logging.info()
110
+ - self.logger.warning()
111
+ - log.error()
112
+
113
+ Args:
114
+ node: The Call node to check
115
+
116
+ Returns:
117
+ True if this appears to be a logging call
118
+ """
119
+ if not isinstance(node.func, ast.Attribute):
120
+ return False
121
+ return node.func.attr in LOGGER_METHODS
122
+
123
+
124
+ def _extract_logger_method(node: ast.Call) -> str:
125
+ """Extract the logger method name from a call node.
126
+
127
+ Args:
128
+ node: The Call node (must be a logger call)
129
+
130
+ Returns:
131
+ The logger method name (e.g., 'debug', 'info')
132
+ """
133
+ if isinstance(node.func, ast.Attribute):
134
+ return node.func.attr
135
+ return ""
136
+
137
+
138
+ class ConditionalVerboseAnalyzer:
139
+ """Analyzes Python AST to find conditional verbose logging patterns."""
140
+
141
+ def __init__(self) -> None:
142
+ """Initialize the analyzer."""
143
+ self.violations: list[tuple[ast.If, ast.Call, int]] = []
144
+
145
+ def find_conditional_verbose_calls(
146
+ self, tree: ast.AST
147
+ ) -> list[tuple[ast.If, ast.Call, str, int]]:
148
+ """Find all conditional verbose logging patterns in the AST.
149
+
150
+ Looks for if statements with verbose-like conditions that contain
151
+ logger method calls in their body.
152
+
153
+ Args:
154
+ tree: The AST to analyze
155
+
156
+ Returns:
157
+ List of tuples (if_node, call_node, logger_method, line_number)
158
+ """
159
+ verbose_if_nodes = (
160
+ node
161
+ for node in ast.walk(tree)
162
+ if isinstance(node, ast.If) and is_verbose_condition(node.test)
163
+ )
164
+
165
+ results: list[tuple[ast.If, ast.Call, str, int]] = []
166
+ for if_node in verbose_if_nodes:
167
+ results.extend(self._extract_logger_call_results(if_node))
168
+
169
+ return results
170
+
171
+ def _extract_logger_call_results(
172
+ self, if_node: ast.If
173
+ ) -> list[tuple[ast.If, ast.Call, str, int]]:
174
+ """Extract logger call results from a verbose if node."""
175
+ logger_calls = self._find_logger_calls_in_body(if_node.body)
176
+ return [
177
+ (
178
+ if_node,
179
+ call_node,
180
+ _extract_logger_method(call_node),
181
+ call_node.lineno if hasattr(call_node, "lineno") else if_node.lineno,
182
+ )
183
+ for call_node in logger_calls
184
+ ]
185
+
186
+ def _find_logger_calls_in_body(self, body: list[ast.stmt]) -> list[ast.Call]:
187
+ """Find all logger calls in a list of statements.
188
+
189
+ Args:
190
+ body: List of AST statements
191
+
192
+ Returns:
193
+ List of Call nodes that are logger calls
194
+ """
195
+ logger_calls: list[ast.Call] = []
196
+ for stmt in body:
197
+ for node in ast.walk(stmt):
198
+ if isinstance(node, ast.Call) and is_logger_call(node):
199
+ logger_calls.append(node)
200
+ return logger_calls