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/config.py +4 -12
- src/cli/linters/__init__.py +5 -2
- src/cli/linters/code_patterns.py +42 -38
- src/cli/linters/code_smells.py +8 -17
- src/cli/linters/documentation.py +3 -6
- src/cli/linters/performance.py +4 -10
- src/cli/linters/shared.py +2 -7
- src/cli/linters/structure.py +4 -11
- src/cli/linters/structure_quality.py +4 -11
- src/cli/main.py +9 -12
- src/cli/utils.py +7 -16
- src/core/__init__.py +14 -0
- src/core/rule_aliases.py +84 -0
- src/linter_config/rule_matcher.py +53 -8
- src/linters/print_statements/__init__.py +23 -11
- src/linters/print_statements/conditional_verbose_analyzer.py +200 -0
- src/linters/print_statements/conditional_verbose_rule.py +254 -0
- src/linters/print_statements/linter.py +2 -2
- {thailint-0.15.6.dist-info → thailint-0.16.0.dist-info}/METADATA +7 -4
- {thailint-0.15.6.dist-info → thailint-0.16.0.dist-info}/RECORD +23 -20
- {thailint-0.15.6.dist-info → thailint-0.16.0.dist-info}/WHEEL +0 -0
- {thailint-0.15.6.dist-info → thailint-0.16.0.dist-info}/entry_points.txt +0 -0
- {thailint-0.15.6.dist-info → thailint-0.16.0.dist-info}/licenses/LICENSE +0 -0
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
]
|
src/core/rule_aliases.py
ADDED
|
@@ -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),
|
|
8
|
-
category.specific)
|
|
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
|
|
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
|
|
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., "
|
|
28
|
-
pattern: Pattern with optional wildcard (e.g., "nesting.*" or "
|
|
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:
|
|
4
|
+
Purpose: Improper logging linter package exports and convenience functions
|
|
5
5
|
|
|
6
|
-
Exports: PrintStatementRule
|
|
6
|
+
Exports: PrintStatementRule, ConditionalVerboseRule classes, PrintStatementConfig dataclass,
|
|
7
|
+
lint() convenience function, ImproperLoggingPrintRule alias
|
|
7
8
|
|
|
8
|
-
Depends: .linter for PrintStatementRule, .
|
|
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
|
|
15
|
-
PrintStatementRule
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
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
|
-
|
|
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
|