thailint 0.1.5__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/__init__.py +7 -2
- src/analyzers/__init__.py +23 -0
- src/analyzers/typescript_base.py +148 -0
- src/api.py +1 -1
- src/cli.py +1111 -144
- src/config.py +12 -33
- src/core/base.py +102 -5
- src/core/cli_utils.py +206 -0
- src/core/config_parser.py +126 -0
- src/core/linter_utils.py +168 -0
- src/core/registry.py +17 -92
- src/core/rule_discovery.py +132 -0
- src/core/violation_builder.py +122 -0
- src/linter_config/ignore.py +112 -40
- src/linter_config/loader.py +3 -13
- src/linters/dry/__init__.py +23 -0
- src/linters/dry/base_token_analyzer.py +76 -0
- src/linters/dry/block_filter.py +265 -0
- src/linters/dry/block_grouper.py +59 -0
- src/linters/dry/cache.py +172 -0
- src/linters/dry/cache_query.py +61 -0
- src/linters/dry/config.py +134 -0
- src/linters/dry/config_loader.py +44 -0
- src/linters/dry/deduplicator.py +120 -0
- src/linters/dry/duplicate_storage.py +63 -0
- src/linters/dry/file_analyzer.py +90 -0
- src/linters/dry/inline_ignore.py +140 -0
- src/linters/dry/linter.py +163 -0
- src/linters/dry/python_analyzer.py +668 -0
- src/linters/dry/storage_initializer.py +42 -0
- src/linters/dry/token_hasher.py +169 -0
- src/linters/dry/typescript_analyzer.py +592 -0
- src/linters/dry/violation_builder.py +74 -0
- src/linters/dry/violation_filter.py +94 -0
- src/linters/dry/violation_generator.py +174 -0
- 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/config_loader.py +86 -0
- src/linters/file_placement/directory_matcher.py +80 -0
- src/linters/file_placement/linter.py +262 -471
- src/linters/file_placement/path_resolver.py +61 -0
- src/linters/file_placement/pattern_matcher.py +55 -0
- src/linters/file_placement/pattern_validator.py +106 -0
- src/linters/file_placement/rule_checker.py +229 -0
- src/linters/file_placement/violation_factory.py +177 -0
- 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 +17 -4
- src/linters/nesting/linter.py +81 -168
- src/linters/nesting/typescript_analyzer.py +39 -102
- src/linters/nesting/typescript_function_extractor.py +130 -0
- src/linters/nesting/violation_builder.py +139 -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 +99 -0
- src/linters/srp/class_analyzer.py +113 -0
- src/linters/srp/config.py +82 -0
- src/linters/srp/heuristics.py +89 -0
- src/linters/srp/linter.py +234 -0
- src/linters/srp/metrics_evaluator.py +47 -0
- src/linters/srp/python_analyzer.py +72 -0
- src/linters/srp/typescript_analyzer.py +75 -0
- src/linters/srp/typescript_metrics_calculator.py +90 -0
- src/linters/srp/violation_builder.py +117 -0
- src/orchestrator/core.py +54 -9
- src/templates/thailint_config_template.yaml +158 -0
- src/utils/__init__.py +4 -0
- src/utils/project_root.py +203 -0
- thailint-0.5.0.dist-info/METADATA +1286 -0
- thailint-0.5.0.dist-info/RECORD +96 -0
- {thailint-0.1.5.dist-info → thailint-0.5.0.dist-info}/WHEEL +1 -1
- src/.ai/layout.yaml +0 -48
- thailint-0.1.5.dist-info/METADATA +0 -629
- thailint-0.1.5.dist-info/RECORD +0 -28
- {thailint-0.1.5.dist-info → thailint-0.5.0.dist-info}/entry_points.txt +0 -0
- {thailint-0.1.5.dist-info → thailint-0.5.0.dist-info/licenses}/LICENSE +0 -0
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Purpose: Violation creation for nesting depth linter
|
|
3
|
+
|
|
4
|
+
Scope: Builds Violation objects for nesting depth violations
|
|
5
|
+
|
|
6
|
+
Overview: Provides violation building functionality for the nesting depth linter. Creates
|
|
7
|
+
violations for Python and TypeScript functions with excessive nesting, generates contextual
|
|
8
|
+
error messages with actual vs maximum depth, and provides actionable refactoring suggestions
|
|
9
|
+
(early returns, guard clauses, extract method). Handles syntax errors gracefully. Isolates
|
|
10
|
+
violation construction from analysis and checking logic.
|
|
11
|
+
|
|
12
|
+
Dependencies: ast, BaseLintContext, Violation, Severity, NestingConfig, src.core.violation_builder
|
|
13
|
+
|
|
14
|
+
Exports: NestingViolationBuilder
|
|
15
|
+
|
|
16
|
+
Interfaces: create_nesting_violation, create_typescript_nesting_violation, create_syntax_error_violation
|
|
17
|
+
|
|
18
|
+
Implementation: Formats messages with depth information, provides targeted refactoring suggestions,
|
|
19
|
+
extends BaseViolationBuilder for consistent violation construction
|
|
20
|
+
"""
|
|
21
|
+
|
|
22
|
+
import ast
|
|
23
|
+
from typing import Any
|
|
24
|
+
|
|
25
|
+
from src.core.base import BaseLintContext
|
|
26
|
+
from src.core.types import Severity, Violation
|
|
27
|
+
from src.core.violation_builder import BaseViolationBuilder
|
|
28
|
+
|
|
29
|
+
from .config import NestingConfig
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
class NestingViolationBuilder(BaseViolationBuilder):
|
|
33
|
+
"""Builds violations for nesting depth issues."""
|
|
34
|
+
|
|
35
|
+
def __init__(self, rule_id: str):
|
|
36
|
+
"""Initialize violation builder.
|
|
37
|
+
|
|
38
|
+
Args:
|
|
39
|
+
rule_id: Rule identifier for violations
|
|
40
|
+
"""
|
|
41
|
+
self.rule_id = rule_id
|
|
42
|
+
|
|
43
|
+
def create_syntax_error_violation(
|
|
44
|
+
self, error: SyntaxError, context: BaseLintContext
|
|
45
|
+
) -> Violation:
|
|
46
|
+
"""Create violation for syntax error.
|
|
47
|
+
|
|
48
|
+
Args:
|
|
49
|
+
error: SyntaxError exception
|
|
50
|
+
context: Lint context
|
|
51
|
+
|
|
52
|
+
Returns:
|
|
53
|
+
Syntax error violation
|
|
54
|
+
"""
|
|
55
|
+
return self.build_from_params(
|
|
56
|
+
rule_id=self.rule_id,
|
|
57
|
+
file_path=str(context.file_path or ""),
|
|
58
|
+
line=error.lineno or 0,
|
|
59
|
+
column=error.offset or 0,
|
|
60
|
+
message=f"Syntax error: {error.msg}",
|
|
61
|
+
severity=Severity.ERROR,
|
|
62
|
+
suggestion="Fix syntax errors before checking nesting depth",
|
|
63
|
+
)
|
|
64
|
+
|
|
65
|
+
def create_nesting_violation(
|
|
66
|
+
self,
|
|
67
|
+
func: ast.FunctionDef | ast.AsyncFunctionDef,
|
|
68
|
+
max_depth: int,
|
|
69
|
+
config: NestingConfig,
|
|
70
|
+
context: BaseLintContext,
|
|
71
|
+
) -> Violation:
|
|
72
|
+
"""Create violation for excessive nesting in Python function.
|
|
73
|
+
|
|
74
|
+
Args:
|
|
75
|
+
func: Python function AST node
|
|
76
|
+
max_depth: Actual max nesting depth found
|
|
77
|
+
config: Nesting configuration
|
|
78
|
+
context: Lint context
|
|
79
|
+
|
|
80
|
+
Returns:
|
|
81
|
+
Nesting depth violation
|
|
82
|
+
"""
|
|
83
|
+
return self.build_from_params(
|
|
84
|
+
rule_id=self.rule_id,
|
|
85
|
+
file_path=str(context.file_path or ""),
|
|
86
|
+
line=func.lineno,
|
|
87
|
+
column=func.col_offset,
|
|
88
|
+
message=f"Function '{func.name}' has excessive nesting depth ({max_depth})",
|
|
89
|
+
severity=Severity.ERROR,
|
|
90
|
+
suggestion=self._generate_suggestion(max_depth, config.max_nesting_depth),
|
|
91
|
+
)
|
|
92
|
+
|
|
93
|
+
def create_typescript_nesting_violation(
|
|
94
|
+
self,
|
|
95
|
+
func_info: tuple[Any, str],
|
|
96
|
+
max_depth: int,
|
|
97
|
+
config: NestingConfig,
|
|
98
|
+
context: BaseLintContext,
|
|
99
|
+
) -> Violation:
|
|
100
|
+
"""Create violation for excessive nesting in TypeScript function.
|
|
101
|
+
|
|
102
|
+
Args:
|
|
103
|
+
func_info: Tuple of (func_node, func_name)
|
|
104
|
+
max_depth: Actual max nesting depth found
|
|
105
|
+
config: Nesting configuration
|
|
106
|
+
context: Lint context
|
|
107
|
+
|
|
108
|
+
Returns:
|
|
109
|
+
Nesting depth violation
|
|
110
|
+
"""
|
|
111
|
+
func_node, func_name = func_info
|
|
112
|
+
line = func_node.start_point[0] + 1 # Convert to 1-indexed
|
|
113
|
+
column = func_node.start_point[1]
|
|
114
|
+
|
|
115
|
+
return self.build_from_params(
|
|
116
|
+
rule_id=self.rule_id,
|
|
117
|
+
file_path=str(context.file_path or ""),
|
|
118
|
+
line=line,
|
|
119
|
+
column=column,
|
|
120
|
+
message=f"Function '{func_name}' has excessive nesting depth ({max_depth})",
|
|
121
|
+
severity=Severity.ERROR,
|
|
122
|
+
suggestion=self._generate_suggestion(max_depth, config.max_nesting_depth),
|
|
123
|
+
)
|
|
124
|
+
|
|
125
|
+
def _generate_suggestion(self, actual_depth: int, max_depth: int) -> str:
|
|
126
|
+
"""Generate refactoring suggestion based on depth.
|
|
127
|
+
|
|
128
|
+
Args:
|
|
129
|
+
actual_depth: Actual nesting depth found
|
|
130
|
+
max_depth: Maximum allowed depth
|
|
131
|
+
|
|
132
|
+
Returns:
|
|
133
|
+
Suggestion string with refactoring advice
|
|
134
|
+
"""
|
|
135
|
+
return (
|
|
136
|
+
f"Maximum nesting depth of {actual_depth} exceeds limit of {max_depth}. "
|
|
137
|
+
"Consider extracting nested logic to separate functions, using early returns, "
|
|
138
|
+
"or applying guard clauses to reduce nesting."
|
|
139
|
+
)
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
"""
|
|
2
|
+
File: src/linters/print_statements/__init__.py
|
|
3
|
+
|
|
4
|
+
Purpose: Print statements linter package exports and convenience functions
|
|
5
|
+
|
|
6
|
+
Exports: PrintStatementRule class, PrintStatementConfig dataclass, lint() convenience function
|
|
7
|
+
|
|
8
|
+
Depends: .linter for PrintStatementRule, .config for PrintStatementConfig
|
|
9
|
+
|
|
10
|
+
Implements: lint(file_path, config) -> list[Violation] for simple linting operations
|
|
11
|
+
|
|
12
|
+
Related: src/linters/magic_numbers/__init__.py, src/core/base.py
|
|
13
|
+
|
|
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.
|
|
20
|
+
|
|
21
|
+
Usage: from src.linters.print_statements import PrintStatementRule, lint
|
|
22
|
+
violations = lint("path/to/file.py")
|
|
23
|
+
|
|
24
|
+
Notes: Module-level exports with __all__ definition, convenience function wrapper
|
|
25
|
+
"""
|
|
26
|
+
|
|
27
|
+
from .config import PrintStatementConfig
|
|
28
|
+
from .linter import PrintStatementRule
|
|
29
|
+
|
|
30
|
+
__all__ = ["PrintStatementRule", "PrintStatementConfig", "lint"]
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def lint(file_path: str, config: dict | None = None) -> list:
|
|
34
|
+
"""Convenience function for linting a file for print statements.
|
|
35
|
+
|
|
36
|
+
Args:
|
|
37
|
+
file_path: Path to the file to lint
|
|
38
|
+
config: Optional configuration dictionary
|
|
39
|
+
|
|
40
|
+
Returns:
|
|
41
|
+
List of violations found
|
|
42
|
+
"""
|
|
43
|
+
from pathlib import Path
|
|
44
|
+
|
|
45
|
+
from src.orchestrator.core import FileLintContext
|
|
46
|
+
|
|
47
|
+
rule = PrintStatementRule()
|
|
48
|
+
context = FileLintContext(
|
|
49
|
+
path=Path(file_path),
|
|
50
|
+
lang="python",
|
|
51
|
+
)
|
|
52
|
+
|
|
53
|
+
return rule.check(context)
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
"""
|
|
2
|
+
File: src/linters/print_statements/config.py
|
|
3
|
+
|
|
4
|
+
Purpose: Configuration schema for print statements linter
|
|
5
|
+
|
|
6
|
+
Exports: PrintStatementConfig dataclass
|
|
7
|
+
|
|
8
|
+
Depends: dataclasses, typing
|
|
9
|
+
|
|
10
|
+
Implements: PrintStatementConfig(enabled, ignore, allow_in_scripts, console_methods),
|
|
11
|
+
from_dict class method for loading configuration from dictionary
|
|
12
|
+
|
|
13
|
+
Related: src/linters/magic_numbers/config.py, src/core/types.py
|
|
14
|
+
|
|
15
|
+
Overview: Defines configuration schema for print statements linter. Provides PrintStatementConfig
|
|
16
|
+
dataclass with enabled flag, ignore patterns list, allow_in_scripts setting (default True to
|
|
17
|
+
allow print in __main__ blocks), and console_methods set (default includes log, warn, error,
|
|
18
|
+
debug, info) for TypeScript/JavaScript console method detection. Supports per-file and
|
|
19
|
+
per-directory config overrides through from_dict class method. Integrates with orchestrator's
|
|
20
|
+
configuration system to allow users to customize detection via .thailint.yaml configuration.
|
|
21
|
+
|
|
22
|
+
Usage: config = PrintStatementConfig.from_dict(yaml_config, language="python")
|
|
23
|
+
|
|
24
|
+
Notes: Dataclass with defaults matching common use cases, language-specific override support
|
|
25
|
+
"""
|
|
26
|
+
|
|
27
|
+
from dataclasses import dataclass, field
|
|
28
|
+
from typing import Any
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
@dataclass
|
|
32
|
+
class PrintStatementConfig:
|
|
33
|
+
"""Configuration for print statements linter."""
|
|
34
|
+
|
|
35
|
+
enabled: bool = True
|
|
36
|
+
ignore: list[str] = field(default_factory=list)
|
|
37
|
+
allow_in_scripts: bool = True
|
|
38
|
+
console_methods: set[str] = field(
|
|
39
|
+
default_factory=lambda: {"log", "warn", "error", "debug", "info"}
|
|
40
|
+
)
|
|
41
|
+
|
|
42
|
+
@classmethod
|
|
43
|
+
def from_dict(
|
|
44
|
+
cls, config: dict[str, Any], language: str | None = None
|
|
45
|
+
) -> "PrintStatementConfig":
|
|
46
|
+
"""Load configuration from dictionary with language-specific overrides.
|
|
47
|
+
|
|
48
|
+
Args:
|
|
49
|
+
config: Dictionary containing configuration values
|
|
50
|
+
language: Programming language (python, typescript, javascript)
|
|
51
|
+
for language-specific settings
|
|
52
|
+
|
|
53
|
+
Returns:
|
|
54
|
+
PrintStatementConfig instance with values from dictionary
|
|
55
|
+
"""
|
|
56
|
+
# Get language-specific config if available
|
|
57
|
+
if language and language in config:
|
|
58
|
+
lang_config = config[language]
|
|
59
|
+
allow_in_scripts = lang_config.get(
|
|
60
|
+
"allow_in_scripts", config.get("allow_in_scripts", True)
|
|
61
|
+
)
|
|
62
|
+
console_methods = set(
|
|
63
|
+
lang_config.get(
|
|
64
|
+
"console_methods",
|
|
65
|
+
config.get("console_methods", ["log", "warn", "error", "debug", "info"]),
|
|
66
|
+
)
|
|
67
|
+
)
|
|
68
|
+
else:
|
|
69
|
+
allow_in_scripts = config.get("allow_in_scripts", True)
|
|
70
|
+
console_methods = set(
|
|
71
|
+
config.get("console_methods", ["log", "warn", "error", "debug", "info"])
|
|
72
|
+
)
|
|
73
|
+
|
|
74
|
+
ignore_patterns = config.get("ignore", [])
|
|
75
|
+
if not isinstance(ignore_patterns, list):
|
|
76
|
+
ignore_patterns = []
|
|
77
|
+
|
|
78
|
+
return cls(
|
|
79
|
+
enabled=config.get("enabled", True),
|
|
80
|
+
ignore=ignore_patterns,
|
|
81
|
+
allow_in_scripts=allow_in_scripts,
|
|
82
|
+
console_methods=console_methods,
|
|
83
|
+
)
|
|
@@ -0,0 +1,430 @@
|
|
|
1
|
+
"""
|
|
2
|
+
File: src/linters/print_statements/linter.py
|
|
3
|
+
|
|
4
|
+
Purpose: Main print statements linter rule implementation
|
|
5
|
+
|
|
6
|
+
Exports: PrintStatementRule class
|
|
7
|
+
|
|
8
|
+
Depends: BaseLintContext, MultiLanguageLintRule, PythonPrintStatementAnalyzer,
|
|
9
|
+
TypeScriptPrintStatementAnalyzer, ViolationBuilder, PrintStatementConfig, IgnoreDirectiveParser
|
|
10
|
+
|
|
11
|
+
Implements: PrintStatementRule.check(context) -> list[Violation], properties for rule metadata
|
|
12
|
+
|
|
13
|
+
Related: src/linters/magic_numbers/linter.py, src/core/base.py
|
|
14
|
+
|
|
15
|
+
Overview: Implements print statements linter rule following BaseLintRule interface. Orchestrates
|
|
16
|
+
configuration loading, Python AST analysis for print() calls, TypeScript tree-sitter analysis
|
|
17
|
+
for console.* calls, and violation building through focused helper classes. Detects print and
|
|
18
|
+
console statements that should be replaced with proper logging. Supports configurable
|
|
19
|
+
allow_in_scripts option to permit print() in __main__ blocks and configurable console_methods
|
|
20
|
+
set for TypeScript/JavaScript. Handles ignore directives for suppressing specific violations.
|
|
21
|
+
|
|
22
|
+
Usage: rule = PrintStatementRule()
|
|
23
|
+
violations = rule.check(context)
|
|
24
|
+
|
|
25
|
+
Notes: Composition pattern with helper classes, AST-based analysis for Python, tree-sitter for TS/JS
|
|
26
|
+
"""
|
|
27
|
+
|
|
28
|
+
import ast
|
|
29
|
+
from pathlib import Path
|
|
30
|
+
|
|
31
|
+
from src.core.base import BaseLintContext, MultiLanguageLintRule
|
|
32
|
+
from src.core.linter_utils import load_linter_config
|
|
33
|
+
from src.core.types import Violation
|
|
34
|
+
from src.linter_config.ignore import IgnoreDirectiveParser
|
|
35
|
+
|
|
36
|
+
from .config import PrintStatementConfig
|
|
37
|
+
from .python_analyzer import PythonPrintStatementAnalyzer
|
|
38
|
+
from .typescript_analyzer import TypeScriptPrintStatementAnalyzer
|
|
39
|
+
from .violation_builder import ViolationBuilder
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
class PrintStatementRule(MultiLanguageLintRule): # thailint: ignore[srp]
|
|
43
|
+
"""Detects print/console statements that should be replaced with proper logging."""
|
|
44
|
+
|
|
45
|
+
def __init__(self) -> None:
|
|
46
|
+
"""Initialize the print statements rule."""
|
|
47
|
+
self._ignore_parser = IgnoreDirectiveParser()
|
|
48
|
+
self._violation_builder = ViolationBuilder(self.rule_id)
|
|
49
|
+
|
|
50
|
+
@property
|
|
51
|
+
def rule_id(self) -> str:
|
|
52
|
+
"""Unique identifier for this rule."""
|
|
53
|
+
return "print-statements.detected"
|
|
54
|
+
|
|
55
|
+
@property
|
|
56
|
+
def rule_name(self) -> str:
|
|
57
|
+
"""Human-readable name for this rule."""
|
|
58
|
+
return "Print Statements"
|
|
59
|
+
|
|
60
|
+
@property
|
|
61
|
+
def description(self) -> str:
|
|
62
|
+
"""Description of what this rule checks."""
|
|
63
|
+
return "Print/console statements should be replaced with proper logging"
|
|
64
|
+
|
|
65
|
+
def _load_config(self, context: BaseLintContext) -> PrintStatementConfig:
|
|
66
|
+
"""Load configuration from context.
|
|
67
|
+
|
|
68
|
+
Args:
|
|
69
|
+
context: Lint context
|
|
70
|
+
|
|
71
|
+
Returns:
|
|
72
|
+
PrintStatementConfig instance
|
|
73
|
+
"""
|
|
74
|
+
test_config = self._try_load_test_config(context)
|
|
75
|
+
if test_config is not None:
|
|
76
|
+
return test_config
|
|
77
|
+
|
|
78
|
+
prod_config = self._try_load_production_config(context)
|
|
79
|
+
if prod_config is not None:
|
|
80
|
+
return prod_config
|
|
81
|
+
|
|
82
|
+
return PrintStatementConfig()
|
|
83
|
+
|
|
84
|
+
def _try_load_test_config(self, context: BaseLintContext) -> PrintStatementConfig | None:
|
|
85
|
+
"""Try to load test-style configuration."""
|
|
86
|
+
if not hasattr(context, "config"):
|
|
87
|
+
return None
|
|
88
|
+
config_attr = context.config
|
|
89
|
+
if config_attr is None or not isinstance(config_attr, dict):
|
|
90
|
+
return None
|
|
91
|
+
return PrintStatementConfig.from_dict(config_attr, context.language)
|
|
92
|
+
|
|
93
|
+
def _try_load_production_config(self, context: BaseLintContext) -> PrintStatementConfig | None:
|
|
94
|
+
"""Try to load production configuration."""
|
|
95
|
+
if not hasattr(context, "metadata") or not isinstance(context.metadata, dict):
|
|
96
|
+
return None
|
|
97
|
+
|
|
98
|
+
metadata = context.metadata
|
|
99
|
+
|
|
100
|
+
if "print_statements" in metadata:
|
|
101
|
+
return load_linter_config(context, "print_statements", PrintStatementConfig)
|
|
102
|
+
|
|
103
|
+
if "print-statements" in metadata:
|
|
104
|
+
return load_linter_config(context, "print-statements", PrintStatementConfig)
|
|
105
|
+
|
|
106
|
+
return None
|
|
107
|
+
|
|
108
|
+
def _is_file_ignored(self, context: BaseLintContext, config: PrintStatementConfig) -> bool:
|
|
109
|
+
"""Check if file matches ignore patterns.
|
|
110
|
+
|
|
111
|
+
Args:
|
|
112
|
+
context: Lint context
|
|
113
|
+
config: Print statements configuration
|
|
114
|
+
|
|
115
|
+
Returns:
|
|
116
|
+
True if file should be ignored
|
|
117
|
+
"""
|
|
118
|
+
if not config.ignore:
|
|
119
|
+
return False
|
|
120
|
+
|
|
121
|
+
if not context.file_path:
|
|
122
|
+
return False
|
|
123
|
+
|
|
124
|
+
file_path = Path(context.file_path)
|
|
125
|
+
for pattern in config.ignore:
|
|
126
|
+
if self._matches_pattern(file_path, pattern):
|
|
127
|
+
return True
|
|
128
|
+
return False
|
|
129
|
+
|
|
130
|
+
def _matches_pattern(self, file_path: Path, pattern: str) -> bool:
|
|
131
|
+
"""Check if file path matches a glob pattern.
|
|
132
|
+
|
|
133
|
+
Args:
|
|
134
|
+
file_path: Path to check
|
|
135
|
+
pattern: Glob pattern
|
|
136
|
+
|
|
137
|
+
Returns:
|
|
138
|
+
True if path matches pattern
|
|
139
|
+
"""
|
|
140
|
+
if file_path.match(pattern):
|
|
141
|
+
return True
|
|
142
|
+
if pattern in str(file_path):
|
|
143
|
+
return True
|
|
144
|
+
return False
|
|
145
|
+
|
|
146
|
+
def _check_python(
|
|
147
|
+
self, context: BaseLintContext, config: PrintStatementConfig
|
|
148
|
+
) -> list[Violation]:
|
|
149
|
+
"""Check Python code for print() violations.
|
|
150
|
+
|
|
151
|
+
Args:
|
|
152
|
+
context: Lint context with Python file information
|
|
153
|
+
config: Print statements configuration
|
|
154
|
+
|
|
155
|
+
Returns:
|
|
156
|
+
List of violations found in Python code
|
|
157
|
+
"""
|
|
158
|
+
if self._is_file_ignored(context, config):
|
|
159
|
+
return []
|
|
160
|
+
|
|
161
|
+
tree = self._parse_python_code(context.file_content)
|
|
162
|
+
if tree is None:
|
|
163
|
+
return []
|
|
164
|
+
|
|
165
|
+
analyzer = PythonPrintStatementAnalyzer()
|
|
166
|
+
print_calls = analyzer.find_print_calls(tree)
|
|
167
|
+
return self._collect_python_violations(print_calls, context, config, analyzer)
|
|
168
|
+
|
|
169
|
+
def _parse_python_code(self, code: str | None) -> ast.AST | None:
|
|
170
|
+
"""Parse Python code into AST."""
|
|
171
|
+
try:
|
|
172
|
+
return ast.parse(code or "")
|
|
173
|
+
except SyntaxError:
|
|
174
|
+
return None
|
|
175
|
+
|
|
176
|
+
def _collect_python_violations(
|
|
177
|
+
self,
|
|
178
|
+
print_calls: list,
|
|
179
|
+
context: BaseLintContext,
|
|
180
|
+
config: PrintStatementConfig,
|
|
181
|
+
analyzer: PythonPrintStatementAnalyzer,
|
|
182
|
+
) -> list[Violation]:
|
|
183
|
+
"""Collect violations from Python print() calls.
|
|
184
|
+
|
|
185
|
+
Args:
|
|
186
|
+
print_calls: List of (node, parent, line_number) tuples
|
|
187
|
+
context: Lint context
|
|
188
|
+
config: Configuration
|
|
189
|
+
analyzer: Python analyzer instance
|
|
190
|
+
|
|
191
|
+
Returns:
|
|
192
|
+
List of violations
|
|
193
|
+
"""
|
|
194
|
+
violations = []
|
|
195
|
+
for node, _parent, line_number in print_calls:
|
|
196
|
+
violation = self._try_create_python_violation(
|
|
197
|
+
node, line_number, context, config, analyzer
|
|
198
|
+
)
|
|
199
|
+
if violation is not None:
|
|
200
|
+
violations.append(violation)
|
|
201
|
+
return violations
|
|
202
|
+
|
|
203
|
+
def _try_create_python_violation( # pylint: disable=too-many-arguments,too-many-positional-arguments
|
|
204
|
+
self,
|
|
205
|
+
node: ast.Call,
|
|
206
|
+
line_number: int,
|
|
207
|
+
context: BaseLintContext,
|
|
208
|
+
config: PrintStatementConfig,
|
|
209
|
+
analyzer: PythonPrintStatementAnalyzer,
|
|
210
|
+
) -> Violation | None:
|
|
211
|
+
"""Try to create a violation for a Python print() call.
|
|
212
|
+
|
|
213
|
+
Args:
|
|
214
|
+
node: AST Call node
|
|
215
|
+
line_number: Line number
|
|
216
|
+
context: Lint context
|
|
217
|
+
config: Configuration
|
|
218
|
+
analyzer: Python analyzer
|
|
219
|
+
|
|
220
|
+
Returns:
|
|
221
|
+
Violation or None if should not flag
|
|
222
|
+
"""
|
|
223
|
+
# Check if in __main__ block and allow_in_scripts is enabled
|
|
224
|
+
if config.allow_in_scripts and analyzer.is_in_main_block(node):
|
|
225
|
+
return None
|
|
226
|
+
|
|
227
|
+
violation = self._violation_builder.create_python_violation(
|
|
228
|
+
node, line_number, context.file_path
|
|
229
|
+
)
|
|
230
|
+
|
|
231
|
+
if self._should_ignore(violation, context):
|
|
232
|
+
return None
|
|
233
|
+
|
|
234
|
+
return violation
|
|
235
|
+
|
|
236
|
+
def _should_ignore(self, violation: Violation, context: BaseLintContext) -> bool:
|
|
237
|
+
"""Check if violation should be ignored based on inline directives.
|
|
238
|
+
|
|
239
|
+
Args:
|
|
240
|
+
violation: Violation to check
|
|
241
|
+
context: Lint context with file content
|
|
242
|
+
|
|
243
|
+
Returns:
|
|
244
|
+
True if violation should be ignored
|
|
245
|
+
"""
|
|
246
|
+
if self._ignore_parser.should_ignore_violation(violation, context.file_content or ""):
|
|
247
|
+
return True
|
|
248
|
+
return self._check_generic_ignore(violation, context)
|
|
249
|
+
|
|
250
|
+
def _check_generic_ignore(self, violation: Violation, context: BaseLintContext) -> bool:
|
|
251
|
+
"""Check for generic ignore directives.
|
|
252
|
+
|
|
253
|
+
Args:
|
|
254
|
+
violation: Violation to check
|
|
255
|
+
context: Lint context
|
|
256
|
+
|
|
257
|
+
Returns:
|
|
258
|
+
True if line has generic ignore directive
|
|
259
|
+
"""
|
|
260
|
+
line_text = self._get_violation_line(violation, context)
|
|
261
|
+
if line_text is None:
|
|
262
|
+
return False
|
|
263
|
+
return self._has_generic_ignore_directive(line_text)
|
|
264
|
+
|
|
265
|
+
def _get_violation_line(self, violation: Violation, context: BaseLintContext) -> str | None:
|
|
266
|
+
"""Get the line text for a violation."""
|
|
267
|
+
if not context.file_content:
|
|
268
|
+
return None
|
|
269
|
+
|
|
270
|
+
lines = context.file_content.splitlines()
|
|
271
|
+
if violation.line <= 0 or violation.line > len(lines):
|
|
272
|
+
return None
|
|
273
|
+
|
|
274
|
+
return lines[violation.line - 1].lower()
|
|
275
|
+
|
|
276
|
+
def _has_generic_ignore_directive(self, line_text: str) -> bool:
|
|
277
|
+
"""Check if line has generic ignore directive."""
|
|
278
|
+
if self._has_generic_thailint_ignore(line_text):
|
|
279
|
+
return True
|
|
280
|
+
return self._has_noqa_directive(line_text)
|
|
281
|
+
|
|
282
|
+
def _has_generic_thailint_ignore(self, line_text: str) -> bool:
|
|
283
|
+
"""Check for generic thailint: ignore (no brackets)."""
|
|
284
|
+
if "# thailint: ignore" not in line_text:
|
|
285
|
+
return False
|
|
286
|
+
after_ignore = line_text.split("# thailint: ignore")[1].split("#")[0]
|
|
287
|
+
return "[" not in after_ignore
|
|
288
|
+
|
|
289
|
+
def _has_noqa_directive(self, line_text: str) -> bool:
|
|
290
|
+
"""Check for noqa-style comments."""
|
|
291
|
+
return "# noqa" in line_text
|
|
292
|
+
|
|
293
|
+
def _check_typescript(
|
|
294
|
+
self, context: BaseLintContext, config: PrintStatementConfig
|
|
295
|
+
) -> list[Violation]:
|
|
296
|
+
"""Check TypeScript/JavaScript code for console.* violations.
|
|
297
|
+
|
|
298
|
+
Args:
|
|
299
|
+
context: Lint context with TypeScript/JavaScript file information
|
|
300
|
+
config: Print statements configuration
|
|
301
|
+
|
|
302
|
+
Returns:
|
|
303
|
+
List of violations found in TypeScript/JavaScript code
|
|
304
|
+
"""
|
|
305
|
+
if self._is_file_ignored(context, config):
|
|
306
|
+
return []
|
|
307
|
+
|
|
308
|
+
analyzer = TypeScriptPrintStatementAnalyzer()
|
|
309
|
+
root_node = analyzer.parse_typescript(context.file_content or "")
|
|
310
|
+
if root_node is None:
|
|
311
|
+
return []
|
|
312
|
+
|
|
313
|
+
console_calls = analyzer.find_console_calls(root_node, config.console_methods)
|
|
314
|
+
return self._collect_typescript_violations(console_calls, context)
|
|
315
|
+
|
|
316
|
+
def _collect_typescript_violations(
|
|
317
|
+
self,
|
|
318
|
+
console_calls: list,
|
|
319
|
+
context: BaseLintContext,
|
|
320
|
+
) -> list[Violation]:
|
|
321
|
+
"""Collect violations from TypeScript console.* calls.
|
|
322
|
+
|
|
323
|
+
Args:
|
|
324
|
+
console_calls: List of (node, method_name, line_number) tuples
|
|
325
|
+
context: Lint context
|
|
326
|
+
|
|
327
|
+
Returns:
|
|
328
|
+
List of violations
|
|
329
|
+
"""
|
|
330
|
+
violations = []
|
|
331
|
+
for _node, method_name, line_number in console_calls:
|
|
332
|
+
violation = self._try_create_typescript_violation(method_name, line_number, context)
|
|
333
|
+
if violation is not None:
|
|
334
|
+
violations.append(violation)
|
|
335
|
+
return violations
|
|
336
|
+
|
|
337
|
+
def _try_create_typescript_violation(
|
|
338
|
+
self,
|
|
339
|
+
method_name: str,
|
|
340
|
+
line_number: int,
|
|
341
|
+
context: BaseLintContext,
|
|
342
|
+
) -> Violation | None:
|
|
343
|
+
"""Try to create a violation for a TypeScript console.* call.
|
|
344
|
+
|
|
345
|
+
Args:
|
|
346
|
+
method_name: Console method name (log, warn, etc.)
|
|
347
|
+
line_number: Line number
|
|
348
|
+
context: Lint context
|
|
349
|
+
|
|
350
|
+
Returns:
|
|
351
|
+
Violation or None if should not flag
|
|
352
|
+
"""
|
|
353
|
+
# Check if test file (skip test files)
|
|
354
|
+
if self._is_test_file(context.file_path):
|
|
355
|
+
return None
|
|
356
|
+
|
|
357
|
+
violation = self._violation_builder.create_typescript_violation(
|
|
358
|
+
method_name, line_number, context.file_path
|
|
359
|
+
)
|
|
360
|
+
|
|
361
|
+
if self._should_ignore_typescript(violation, context):
|
|
362
|
+
return None
|
|
363
|
+
|
|
364
|
+
return violation
|
|
365
|
+
|
|
366
|
+
def _is_test_file(self, file_path: object) -> bool:
|
|
367
|
+
"""Check if file is a test file.
|
|
368
|
+
|
|
369
|
+
Args:
|
|
370
|
+
file_path: Path to check
|
|
371
|
+
|
|
372
|
+
Returns:
|
|
373
|
+
True if test file
|
|
374
|
+
"""
|
|
375
|
+
path_str = str(file_path)
|
|
376
|
+
return any(
|
|
377
|
+
pattern in path_str
|
|
378
|
+
for pattern in [".test.", ".spec.", "test_", "_test.", "/tests/", "/test/"]
|
|
379
|
+
)
|
|
380
|
+
|
|
381
|
+
def _should_ignore_typescript(self, violation: Violation, context: BaseLintContext) -> bool:
|
|
382
|
+
"""Check if TypeScript violation should be ignored.
|
|
383
|
+
|
|
384
|
+
Args:
|
|
385
|
+
violation: Violation to check
|
|
386
|
+
context: Lint context
|
|
387
|
+
|
|
388
|
+
Returns:
|
|
389
|
+
True if should ignore
|
|
390
|
+
"""
|
|
391
|
+
if self._ignore_parser.should_ignore_violation(violation, context.file_content or ""):
|
|
392
|
+
return True
|
|
393
|
+
return self._check_typescript_ignore(violation, context)
|
|
394
|
+
|
|
395
|
+
def _check_typescript_ignore(self, violation: Violation, context: BaseLintContext) -> bool:
|
|
396
|
+
"""Check for TypeScript-style ignore directives.
|
|
397
|
+
|
|
398
|
+
Args:
|
|
399
|
+
violation: Violation to check
|
|
400
|
+
context: Lint context
|
|
401
|
+
|
|
402
|
+
Returns:
|
|
403
|
+
True if line has ignore directive
|
|
404
|
+
"""
|
|
405
|
+
line_text = self._get_violation_line(violation, context)
|
|
406
|
+
if line_text is None:
|
|
407
|
+
return False
|
|
408
|
+
return self._has_typescript_ignore_directive(line_text)
|
|
409
|
+
|
|
410
|
+
def _has_typescript_ignore_directive(self, line_text: str) -> bool:
|
|
411
|
+
"""Check if line has TypeScript-style ignore directive.
|
|
412
|
+
|
|
413
|
+
Args:
|
|
414
|
+
line_text: Line text to check
|
|
415
|
+
|
|
416
|
+
Returns:
|
|
417
|
+
True if has ignore directive
|
|
418
|
+
"""
|
|
419
|
+
if "// thailint: ignore[print-statements]" in line_text:
|
|
420
|
+
return True
|
|
421
|
+
|
|
422
|
+
if "// thailint: ignore" in line_text:
|
|
423
|
+
after_ignore = line_text.split("// thailint: ignore")[1].split("//")[0]
|
|
424
|
+
if "[" not in after_ignore:
|
|
425
|
+
return True
|
|
426
|
+
|
|
427
|
+
if "// noqa" in line_text:
|
|
428
|
+
return True
|
|
429
|
+
|
|
430
|
+
return False
|