thailint 0.12.0__py3-none-any.whl → 0.13.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 (121) hide show
  1. src/analyzers/__init__.py +4 -3
  2. src/analyzers/ast_utils.py +54 -0
  3. src/analyzers/typescript_base.py +4 -0
  4. src/cli/__init__.py +3 -0
  5. src/cli/config.py +12 -12
  6. src/cli/config_merge.py +241 -0
  7. src/cli/linters/__init__.py +3 -0
  8. src/cli/linters/code_patterns.py +113 -5
  9. src/cli/linters/code_smells.py +4 -0
  10. src/cli/linters/documentation.py +3 -0
  11. src/cli/linters/structure.py +3 -0
  12. src/cli/linters/structure_quality.py +3 -0
  13. src/cli_main.py +3 -0
  14. src/config.py +2 -1
  15. src/core/base.py +3 -2
  16. src/core/cli_utils.py +3 -1
  17. src/core/config_parser.py +5 -2
  18. src/core/constants.py +54 -0
  19. src/core/linter_utils.py +4 -0
  20. src/core/rule_discovery.py +5 -1
  21. src/core/violation_builder.py +3 -0
  22. src/linter_config/directive_markers.py +109 -0
  23. src/linter_config/ignore.py +225 -383
  24. src/linter_config/pattern_utils.py +65 -0
  25. src/linter_config/rule_matcher.py +89 -0
  26. src/linters/collection_pipeline/any_all_analyzer.py +281 -0
  27. src/linters/collection_pipeline/ast_utils.py +40 -0
  28. src/linters/collection_pipeline/config.py +12 -0
  29. src/linters/collection_pipeline/continue_analyzer.py +2 -8
  30. src/linters/collection_pipeline/detector.py +262 -32
  31. src/linters/collection_pipeline/filter_map_analyzer.py +402 -0
  32. src/linters/collection_pipeline/linter.py +18 -35
  33. src/linters/collection_pipeline/suggestion_builder.py +68 -1
  34. src/linters/dry/base_token_analyzer.py +16 -9
  35. src/linters/dry/block_filter.py +7 -4
  36. src/linters/dry/cache.py +7 -2
  37. src/linters/dry/config.py +7 -1
  38. src/linters/dry/constant_matcher.py +34 -25
  39. src/linters/dry/file_analyzer.py +4 -2
  40. src/linters/dry/inline_ignore.py +7 -16
  41. src/linters/dry/linter.py +48 -25
  42. src/linters/dry/python_analyzer.py +18 -10
  43. src/linters/dry/python_constant_extractor.py +51 -52
  44. src/linters/dry/single_statement_detector.py +14 -12
  45. src/linters/dry/token_hasher.py +115 -115
  46. src/linters/dry/typescript_analyzer.py +11 -6
  47. src/linters/dry/typescript_constant_extractor.py +4 -0
  48. src/linters/dry/typescript_statement_detector.py +208 -208
  49. src/linters/dry/typescript_value_extractor.py +3 -0
  50. src/linters/dry/violation_filter.py +1 -4
  51. src/linters/dry/violation_generator.py +1 -4
  52. src/linters/file_header/atemporal_detector.py +4 -0
  53. src/linters/file_header/base_parser.py +4 -0
  54. src/linters/file_header/bash_parser.py +4 -0
  55. src/linters/file_header/field_validator.py +5 -8
  56. src/linters/file_header/linter.py +19 -12
  57. src/linters/file_header/markdown_parser.py +6 -0
  58. src/linters/file_placement/config_loader.py +3 -1
  59. src/linters/file_placement/linter.py +22 -8
  60. src/linters/file_placement/pattern_matcher.py +21 -4
  61. src/linters/file_placement/pattern_validator.py +21 -7
  62. src/linters/file_placement/rule_checker.py +2 -2
  63. src/linters/lazy_ignores/__init__.py +43 -0
  64. src/linters/lazy_ignores/config.py +66 -0
  65. src/linters/lazy_ignores/directive_utils.py +121 -0
  66. src/linters/lazy_ignores/header_parser.py +177 -0
  67. src/linters/lazy_ignores/linter.py +158 -0
  68. src/linters/lazy_ignores/matcher.py +135 -0
  69. src/linters/lazy_ignores/python_analyzer.py +201 -0
  70. src/linters/lazy_ignores/rule_id_utils.py +180 -0
  71. src/linters/lazy_ignores/skip_detector.py +298 -0
  72. src/linters/lazy_ignores/types.py +67 -0
  73. src/linters/lazy_ignores/typescript_analyzer.py +146 -0
  74. src/linters/lazy_ignores/violation_builder.py +131 -0
  75. src/linters/lbyl/__init__.py +29 -0
  76. src/linters/lbyl/config.py +63 -0
  77. src/linters/lbyl/pattern_detectors/__init__.py +25 -0
  78. src/linters/lbyl/pattern_detectors/base.py +46 -0
  79. src/linters/magic_numbers/context_analyzer.py +227 -229
  80. src/linters/magic_numbers/linter.py +20 -15
  81. src/linters/magic_numbers/python_analyzer.py +4 -16
  82. src/linters/magic_numbers/typescript_analyzer.py +9 -16
  83. src/linters/method_property/config.py +4 -0
  84. src/linters/method_property/linter.py +5 -4
  85. src/linters/method_property/python_analyzer.py +5 -4
  86. src/linters/method_property/violation_builder.py +3 -0
  87. src/linters/nesting/typescript_analyzer.py +6 -12
  88. src/linters/nesting/typescript_function_extractor.py +0 -4
  89. src/linters/print_statements/linter.py +6 -4
  90. src/linters/print_statements/python_analyzer.py +85 -81
  91. src/linters/print_statements/typescript_analyzer.py +6 -15
  92. src/linters/srp/heuristics.py +4 -4
  93. src/linters/srp/linter.py +12 -12
  94. src/linters/srp/violation_builder.py +0 -4
  95. src/linters/stateless_class/linter.py +30 -36
  96. src/linters/stateless_class/python_analyzer.py +11 -20
  97. src/linters/stringly_typed/config.py +4 -5
  98. src/linters/stringly_typed/context_filter.py +410 -410
  99. src/linters/stringly_typed/function_call_violation_builder.py +93 -95
  100. src/linters/stringly_typed/linter.py +48 -16
  101. src/linters/stringly_typed/python/analyzer.py +5 -1
  102. src/linters/stringly_typed/python/call_tracker.py +8 -5
  103. src/linters/stringly_typed/python/comparison_tracker.py +10 -5
  104. src/linters/stringly_typed/python/condition_extractor.py +3 -0
  105. src/linters/stringly_typed/python/conditional_detector.py +4 -1
  106. src/linters/stringly_typed/python/match_analyzer.py +8 -2
  107. src/linters/stringly_typed/python/validation_detector.py +3 -0
  108. src/linters/stringly_typed/storage.py +14 -14
  109. src/linters/stringly_typed/typescript/call_tracker.py +9 -3
  110. src/linters/stringly_typed/typescript/comparison_tracker.py +9 -3
  111. src/linters/stringly_typed/violation_generator.py +288 -259
  112. src/orchestrator/core.py +13 -4
  113. src/templates/thailint_config_template.yaml +166 -0
  114. src/utils/project_root.py +3 -0
  115. thailint-0.13.0.dist-info/METADATA +184 -0
  116. thailint-0.13.0.dist-info/RECORD +189 -0
  117. thailint-0.12.0.dist-info/METADATA +0 -1667
  118. thailint-0.12.0.dist-info/RECORD +0 -164
  119. {thailint-0.12.0.dist-info → thailint-0.13.0.dist-info}/WHEEL +0 -0
  120. {thailint-0.12.0.dist-info → thailint-0.13.0.dist-info}/entry_points.txt +0 -0
  121. {thailint-0.12.0.dist-info → thailint-0.13.0.dist-info}/licenses/LICENSE +0 -0
@@ -12,7 +12,7 @@ Overview: Implements magic numbers linter rule following BaseLintRule interface.
12
12
  because refactoring for A-grade complexity requires extracting helper methods. Class maintains
13
13
  single responsibility of magic number detection - all methods support this core purpose.
14
14
 
15
- Dependencies: BaseLintRule, BaseLintContext, PythonMagicNumberAnalyzer, ContextAnalyzer,
15
+ Dependencies: BaseLintRule, BaseLintContext, PythonMagicNumberAnalyzer, is_acceptable_context,
16
16
  ViolationBuilder, MagicNumberConfig, IgnoreDirectiveParser
17
17
 
18
18
  Exports: MagicNumberRule class
@@ -21,10 +21,16 @@ Interfaces: MagicNumberRule.check(context) -> list[Violation], properties for ru
21
21
 
22
22
  Implementation: Composition pattern with helper classes, AST-based analysis with configurable
23
23
  allowed numbers and context detection
24
+
25
+ Suppressions:
26
+ - too-many-arguments,too-many-positional-arguments: TypeScript violation creation with related params
27
+ - srp: Rule class coordinates analyzers and violation builders. Method count exceeds limit
28
+ due to complexity refactoring. All methods support magic number detection.
24
29
  """
25
30
 
26
31
  import ast
27
32
  from pathlib import Path
33
+ from typing import Any
28
34
 
29
35
  from src.core.base import BaseLintContext, MultiLanguageLintRule
30
36
  from src.core.linter_utils import load_linter_config
@@ -33,7 +39,7 @@ from src.core.violation_utils import get_violation_line, has_python_noqa
33
39
  from src.linter_config.ignore import get_ignore_parser
34
40
 
35
41
  from .config import MagicNumberConfig
36
- from .context_analyzer import ContextAnalyzer
42
+ from .context_analyzer import is_acceptable_context
37
43
  from .python_analyzer import PythonMagicNumberAnalyzer
38
44
  from .typescript_analyzer import TypeScriptMagicNumberAnalyzer
39
45
  from .typescript_ignore_checker import TypeScriptIgnoreChecker
@@ -47,7 +53,6 @@ class MagicNumberRule(MultiLanguageLintRule): # thailint: ignore[srp]
47
53
  """Initialize the magic numbers rule."""
48
54
  self._ignore_parser = get_ignore_parser()
49
55
  self._violation_builder = ViolationBuilder(self.rule_id)
50
- self._context_analyzer = ContextAnalyzer()
51
56
  self._typescript_ignore_checker = TypeScriptIgnoreChecker()
52
57
 
53
58
  @property
@@ -134,10 +139,7 @@ class MagicNumberRule(MultiLanguageLintRule): # thailint: ignore[srp]
134
139
  return False
135
140
 
136
141
  file_path = Path(context.file_path)
137
- for pattern in config.ignore:
138
- if self._matches_pattern(file_path, pattern):
139
- return True
140
- return False
142
+ return any(self._matches_pattern(file_path, pattern) for pattern in config.ignore)
141
143
 
142
144
  def _matches_pattern(self, file_path: Path, pattern: str) -> bool:
143
145
  """Check if file path matches a glob pattern.
@@ -251,9 +253,7 @@ class MagicNumberRule(MultiLanguageLintRule): # thailint: ignore[srp]
251
253
  "allowed_numbers": config.allowed_numbers,
252
254
  }
253
255
 
254
- if self._context_analyzer.is_acceptable_context(
255
- node, parent, context.file_path, config_dict
256
- ):
256
+ if is_acceptable_context(node, parent, context.file_path, config_dict):
257
257
  return False
258
258
 
259
259
  return True
@@ -421,12 +421,17 @@ class MagicNumberRule(MultiLanguageLintRule): # thailint: ignore[srp]
421
421
  return value in config.allowed_numbers or self._is_test_file(context.file_path)
422
422
 
423
423
  def _is_typescript_special_context(
424
- self, node: object, analyzer: TypeScriptMagicNumberAnalyzer, context: BaseLintContext
424
+ self, node: Any, analyzer: TypeScriptMagicNumberAnalyzer, context: BaseLintContext
425
425
  ) -> bool:
426
- """Check if in TypeScript-specific special context."""
427
- # Calls require type: ignore because node is typed as object but analyzer expects Node
428
- in_enum = analyzer.is_enum_context(node) # type: ignore[arg-type]
429
- in_const_def = analyzer.is_constant_definition(node, context.file_content or "") # type: ignore[arg-type]
426
+ """Check if in TypeScript-specific special context.
427
+
428
+ Args:
429
+ node: Tree-sitter Node (typed as Any due to optional dependency)
430
+ analyzer: TypeScript analyzer
431
+ context: Lint context
432
+ """
433
+ in_enum = analyzer.is_enum_context(node)
434
+ in_const_def = analyzer.is_constant_definition(node, context.file_content or "")
430
435
  return in_enum or in_const_def
431
436
 
432
437
  def _is_test_file(self, file_path: object) -> bool:
@@ -10,7 +10,7 @@ Overview: Provides PythonMagicNumberAnalyzer class that traverses Python AST to
10
10
  value, and source location. This analyzer handles Python-specific AST structure and provides
11
11
  the foundation for magic number detection by identifying all candidates before context filtering.
12
12
 
13
- Dependencies: ast module for AST parsing and node types
13
+ Dependencies: ast module for AST parsing and node types, analyzers.ast_utils
14
14
 
15
15
  Exports: PythonMagicNumberAnalyzer class
16
16
 
@@ -23,6 +23,8 @@ Implementation: AST NodeVisitor pattern with parent tracking, filters for numeri
23
23
  import ast
24
24
  from typing import Any
25
25
 
26
+ from src.analyzers.ast_utils import build_parent_map
27
+
26
28
 
27
29
  class PythonMagicNumberAnalyzer(ast.NodeVisitor):
28
30
  """Analyzes Python AST to find numeric literals."""
@@ -44,24 +46,10 @@ class PythonMagicNumberAnalyzer(ast.NodeVisitor):
44
46
  List of tuples (node, parent, value, line_number)
45
47
  """
46
48
  self.numeric_literals = []
47
- self.parent_map = {}
48
- self._build_parent_map(tree)
49
+ self.parent_map = build_parent_map(tree)
49
50
  self.visit(tree)
50
51
  return self.numeric_literals
51
52
 
52
- def _build_parent_map(self, node: ast.AST, parent: ast.AST | None = None) -> None:
53
- """Build a map of nodes to their parents.
54
-
55
- Args:
56
- node: Current AST node
57
- parent: Parent of current node
58
- """
59
- if parent is not None:
60
- self.parent_map[node] = parent
61
-
62
- for child in ast.iter_child_nodes(node):
63
- self._build_parent_map(child, node)
64
-
65
53
  def visit_Constant(self, node: ast.Constant) -> None:
66
54
  """Visit a Constant node and check if it's a numeric literal.
67
55
 
@@ -20,20 +20,17 @@ Interfaces: find_numeric_literals(root_node) -> list[tuple], is_enum_context(nod
20
20
 
21
21
  Implementation: Tree-sitter node traversal with visitor pattern, context-aware filtering
22
22
  for acceptable numeric literal locations
23
- """
24
-
25
- from typing import Any
26
-
27
- from src.analyzers.typescript_base import TypeScriptBaseAnalyzer
28
23
 
29
- # dry: ignore-block - tree-sitter import pattern (common across TypeScript analyzers)
30
- try:
31
- from tree_sitter import Node
24
+ Suppressions:
25
+ - srp: Analyzer implements tree-sitter traversal with context detection methods.
26
+ Methods support single responsibility of magic number detection in TypeScript.
27
+ """
32
28
 
33
- TREE_SITTER_AVAILABLE = True
34
- except ImportError:
35
- TREE_SITTER_AVAILABLE = False
36
- Node = Any # type: ignore
29
+ from src.analyzers.typescript_base import (
30
+ TREE_SITTER_AVAILABLE,
31
+ Node,
32
+ TypeScriptBaseAnalyzer,
33
+ )
37
34
 
38
35
 
39
36
  class TypeScriptMagicNumberAnalyzer(TypeScriptBaseAnalyzer): # thailint: ignore[srp]
@@ -44,10 +41,6 @@ class TypeScriptMagicNumberAnalyzer(TypeScriptBaseAnalyzer): # thailint: ignore
44
41
  of TypeScript magic number detection - all methods support this core purpose.
45
42
  """
46
43
 
47
- def __init__(self) -> None: # pylint: disable=useless-parent-delegation
48
- """Initialize the TypeScript magic number analyzer."""
49
- super().__init__() # Sets self.tree_sitter_available from base class
50
-
51
44
  def find_numeric_literals(self, root_node: Node) -> list[tuple[Node, float | int, int]]:
52
45
  """Find all numeric literal nodes in TypeScript/JavaScript AST.
53
46
 
@@ -18,6 +18,10 @@ Exports: MethodPropertyConfig dataclass, DEFAULT_EXCLUDE_PREFIXES, DEFAULT_EXCLU
18
18
  Interfaces: from_dict(config, language) -> MethodPropertyConfig for configuration loading
19
19
 
20
20
  Implementation: Dataclass with defaults matching Pythonic conventions and common use cases
21
+
22
+ Suppressions:
23
+ - dry: MethodPropertyConfig includes extensive exclusion lists that share patterns with
24
+ other config classes. Lists are maintained separately for clear documentation.
21
25
  """
22
26
 
23
27
  from dataclasses import dataclass, field
@@ -21,6 +21,10 @@ Interfaces: check(context) -> list[Violation] for rule validation, standard rule
21
21
 
22
22
  Implementation: Composition pattern with helper classes (analyzer, violation builder),
23
23
  AST-based analysis for Python with comprehensive exclusion rules
24
+
25
+ Suppressions:
26
+ - srp,dry: Rule class coordinates analyzer, config, and violation building. Method count
27
+ exceeds limit due to comprehensive ignore directive support.
24
28
  """
25
29
 
26
30
  import ast
@@ -109,10 +113,7 @@ class MethodPropertyRule(MultiLanguageLintRule): # thailint: ignore[srp,dry]
109
113
  return False
110
114
 
111
115
  file_path = Path(context.file_path)
112
- for pattern in config.ignore:
113
- if self._matches_pattern(file_path, pattern):
114
- return True
115
- return False
116
+ return any(self._matches_pattern(file_path, pattern) for pattern in config.ignore)
116
117
 
117
118
  # dry: ignore-block
118
119
  def _matches_pattern(self, file_path: Path, pattern: str) -> bool:
@@ -18,6 +18,10 @@ Exports: PythonMethodAnalyzer class, PropertyCandidate dataclass
18
18
  Interfaces: find_property_candidates(tree) -> list[PropertyCandidate]
19
19
 
20
20
  Implementation: AST walk pattern with comprehensive method body analysis and exclusion checks
21
+
22
+ Suppressions:
23
+ - srp: Analyzer class implements comprehensive exclusion rules requiring many helper methods.
24
+ All methods support single responsibility of property candidate detection.
21
25
  """
22
26
 
23
27
  import ast
@@ -391,10 +395,7 @@ class PythonMethodAnalyzer: # thailint: ignore[srp]
391
395
  Returns:
392
396
  True if assigning to self.*
393
397
  """
394
- for target in targets:
395
- if self._is_self_target(target):
396
- return True
397
- return False
398
+ return any(self._is_self_target(target) for target in targets)
398
399
 
399
400
  def _is_self_target(self, target: ast.expr) -> bool:
400
401
  """Check if target is a self attribute (self.* or self._*).
@@ -16,6 +16,9 @@ Exports: ViolationBuilder class
16
16
  Interfaces: create_violation(method_name, line, column, file_path, is_get_prefix, class_name)
17
17
 
18
18
  Implementation: Builder pattern with message templates suggesting @property decorator conversion
19
+
20
+ Suppressions:
21
+ - too-many-arguments,too-many-positional-arguments: Violation creation with related params
19
22
  """
20
23
 
21
24
  from pathlib import Path
@@ -16,20 +16,14 @@ Exports: TypeScriptNestingAnalyzer class with calculate_max_depth methods
16
16
  Interfaces: calculate_max_depth(func_node) -> tuple[int, int], find_all_functions(root_node)
17
17
 
18
18
  Implementation: Inherits tree-sitter parsing from base, visitor pattern with depth tracking
19
- """
20
-
21
- from typing import Any
22
19
 
23
- from src.analyzers.typescript_base import TypeScriptBaseAnalyzer
24
-
25
- # dry: ignore-block - tree-sitter import pattern (common across TypeScript analyzers)
26
- try:
27
- from tree_sitter import Node
20
+ """
28
21
 
29
- TREE_SITTER_AVAILABLE = True
30
- except ImportError:
31
- TREE_SITTER_AVAILABLE = False
32
- Node = Any # type: ignore
22
+ from src.analyzers.typescript_base import (
23
+ TREE_SITTER_AVAILABLE,
24
+ Node,
25
+ TypeScriptBaseAnalyzer,
26
+ )
33
27
 
34
28
  from .typescript_function_extractor import TypeScriptFunctionExtractor
35
29
 
@@ -27,10 +27,6 @@ from src.analyzers.typescript_base import TypeScriptBaseAnalyzer
27
27
  class TypeScriptFunctionExtractor(TypeScriptBaseAnalyzer):
28
28
  """Extracts function information from TypeScript AST nodes."""
29
29
 
30
- def __init__(self) -> None: # pylint: disable=useless-parent-delegation
31
- """Initialize the TypeScript function extractor."""
32
- super().__init__() # Sets self.tree_sitter_available from base class
33
-
34
30
  def collect_all_functions(self, root_node: Any) -> list[tuple[Any, str]]:
35
31
  """Collect all function nodes from TypeScript AST.
36
32
 
@@ -21,6 +21,11 @@ Interfaces: check(context) -> list[Violation] for rule validation, standard rule
21
21
 
22
22
  Implementation: Composition pattern with helper classes (analyzers, violation builder),
23
23
  AST-based analysis for Python, tree-sitter for TypeScript/JavaScript
24
+
25
+ Suppressions:
26
+ - too-many-arguments,too-many-positional-arguments: Violation creation with related fields
27
+ - srp: Rule class coordinates multiple language analyzers and violation building.
28
+ Method count exceeds limit due to dual-language support (Python + TypeScript).
24
29
  """
25
30
 
26
31
  import ast
@@ -121,10 +126,7 @@ class PrintStatementRule(MultiLanguageLintRule): # thailint: ignore[srp]
121
126
  return False
122
127
 
123
128
  file_path = Path(context.file_path)
124
- for pattern in config.ignore:
125
- if self._matches_pattern(file_path, pattern):
126
- return True
127
- return False
129
+ return any(self._matches_pattern(file_path, pattern) for pattern in config.ignore)
128
130
 
129
131
  def _matches_pattern(self, file_path: Path, pattern: str) -> bool:
130
132
  """Check if file path matches a glob pattern.
@@ -10,9 +10,9 @@ Overview: Provides PythonPrintStatementAnalyzer class that traverses Python AST
10
10
  Returns structured data about each print call including the AST node, parent context,
11
11
  and line number for violation reporting. Handles both simple print() and builtins.print() calls.
12
12
 
13
- Dependencies: ast module for AST parsing and node types
13
+ Dependencies: ast module for AST parsing and node types, analyzers.ast_utils
14
14
 
15
- Exports: PythonPrintStatementAnalyzer class
15
+ Exports: PythonPrintStatementAnalyzer class, is_print_call function, is_main_if_block function
16
16
 
17
17
  Interfaces: find_print_calls(tree) -> list[tuple[Call, AST | None, int]], is_in_main_block(node) -> bool
18
18
 
@@ -21,8 +21,87 @@ Implementation: AST walk pattern with parent map for context detection and __mai
21
21
 
22
22
  import ast
23
23
 
24
+ from src.analyzers.ast_utils import build_parent_map
24
25
 
25
- class PythonPrintStatementAnalyzer: # thailint: ignore[srp]
26
+ # --- Pure helper functions for print call detection ---
27
+
28
+
29
+ def is_print_call(node: ast.Call) -> bool:
30
+ """Check if a Call node is calling print().
31
+
32
+ Args:
33
+ node: The Call node to check
34
+
35
+ Returns:
36
+ True if this is a print() call
37
+ """
38
+ return _is_simple_print(node) or _is_builtins_print(node)
39
+
40
+
41
+ def _is_simple_print(node: ast.Call) -> bool:
42
+ """Check for simple print() call."""
43
+ return isinstance(node.func, ast.Name) and node.func.id == "print"
44
+
45
+
46
+ def _is_builtins_print(node: ast.Call) -> bool:
47
+ """Check for builtins.print() call."""
48
+ if not isinstance(node.func, ast.Attribute):
49
+ return False
50
+ if node.func.attr != "print":
51
+ return False
52
+ return isinstance(node.func.value, ast.Name) and node.func.value.id == "builtins"
53
+
54
+
55
+ # --- Pure helper functions for __main__ block detection ---
56
+
57
+
58
+ def is_main_if_block(node: ast.AST) -> bool:
59
+ """Check if node is an `if __name__ == "__main__":` statement.
60
+
61
+ Args:
62
+ node: AST node to check
63
+
64
+ Returns:
65
+ True if this is a __main__ if block
66
+ """
67
+ if not isinstance(node, ast.If):
68
+ return False
69
+ if not isinstance(node.test, ast.Compare):
70
+ return False
71
+ return _is_main_comparison(node.test)
72
+
73
+
74
+ def _is_main_comparison(test: ast.Compare) -> bool:
75
+ """Check if comparison is __name__ == '__main__'."""
76
+ if not _is_name_identifier(test.left):
77
+ return False
78
+ if not _has_single_eq_operator(test):
79
+ return False
80
+ return _compares_to_main(test)
81
+
82
+
83
+ def _is_name_identifier(node: ast.expr) -> bool:
84
+ """Check if node is the __name__ identifier."""
85
+ return isinstance(node, ast.Name) and node.id == "__name__"
86
+
87
+
88
+ def _has_single_eq_operator(test: ast.Compare) -> bool:
89
+ """Check if comparison has single == operator."""
90
+ return len(test.ops) == 1 and isinstance(test.ops[0], ast.Eq)
91
+
92
+
93
+ def _compares_to_main(test: ast.Compare) -> bool:
94
+ """Check if comparison is to '__main__' string."""
95
+ if len(test.comparators) != 1:
96
+ return False
97
+ comparator = test.comparators[0]
98
+ return isinstance(comparator, ast.Constant) and comparator.value == "__main__"
99
+
100
+
101
+ # --- Analyzer class with stateful parent tracking ---
102
+
103
+
104
+ class PythonPrintStatementAnalyzer:
26
105
  """Analyzes Python AST to find print() calls."""
27
106
 
28
107
  def __init__(self) -> None:
@@ -40,24 +119,10 @@ class PythonPrintStatementAnalyzer: # thailint: ignore[srp]
40
119
  List of tuples (node, parent, line_number)
41
120
  """
42
121
  self.print_calls = []
43
- self.parent_map = {}
44
- self._build_parent_map(tree)
122
+ self.parent_map = build_parent_map(tree)
45
123
  self._collect_print_calls(tree)
46
124
  return self.print_calls
47
125
 
48
- def _build_parent_map(self, node: ast.AST, parent: ast.AST | None = None) -> None:
49
- """Build a map of nodes to their parents.
50
-
51
- Args:
52
- node: Current AST node
53
- parent: Parent of current node
54
- """
55
- if parent is not None:
56
- self.parent_map[node] = parent
57
-
58
- for child in ast.iter_child_nodes(node):
59
- self._build_parent_map(child, node)
60
-
61
126
  def _collect_print_calls(self, tree: ast.AST) -> None:
62
127
  """Walk tree and collect all print() calls.
63
128
 
@@ -65,34 +130,11 @@ class PythonPrintStatementAnalyzer: # thailint: ignore[srp]
65
130
  tree: AST to traverse
66
131
  """
67
132
  for node in ast.walk(tree):
68
- if isinstance(node, ast.Call) and self._is_print_call(node):
133
+ if isinstance(node, ast.Call) and is_print_call(node):
69
134
  parent = self.parent_map.get(node)
70
135
  line_number = node.lineno if hasattr(node, "lineno") else 0
71
136
  self.print_calls.append((node, parent, line_number))
72
137
 
73
- def _is_print_call(self, node: ast.Call) -> bool:
74
- """Check if a Call node is calling print().
75
-
76
- Args:
77
- node: The Call node to check
78
-
79
- Returns:
80
- True if this is a print() call
81
- """
82
- return self._is_simple_print(node) or self._is_builtins_print(node)
83
-
84
- def _is_simple_print(self, node: ast.Call) -> bool:
85
- """Check for simple print() call."""
86
- return isinstance(node.func, ast.Name) and node.func.id == "print"
87
-
88
- def _is_builtins_print(self, node: ast.Call) -> bool:
89
- """Check for builtins.print() call."""
90
- if not isinstance(node.func, ast.Attribute):
91
- return False
92
- if node.func.attr != "print":
93
- return False
94
- return isinstance(node.func.value, ast.Name) and node.func.value.id == "builtins"
95
-
96
138
  def is_in_main_block(self, node: ast.AST) -> bool:
97
139
  """Check if node is within `if __name__ == "__main__":` block.
98
140
 
@@ -105,45 +147,7 @@ class PythonPrintStatementAnalyzer: # thailint: ignore[srp]
105
147
  current = node
106
148
  while current in self.parent_map:
107
149
  parent = self.parent_map[current]
108
- if self._is_main_if_block(parent):
150
+ if is_main_if_block(parent):
109
151
  return True
110
152
  current = parent
111
153
  return False
112
-
113
- def _is_main_if_block(self, node: ast.AST) -> bool:
114
- """Check if node is an `if __name__ == "__main__":` statement.
115
-
116
- Args:
117
- node: AST node to check
118
-
119
- Returns:
120
- True if this is a __main__ if block
121
- """
122
- if not isinstance(node, ast.If):
123
- return False
124
- if not isinstance(node.test, ast.Compare):
125
- return False
126
- return self._is_main_comparison(node.test)
127
-
128
- def _is_main_comparison(self, test: ast.Compare) -> bool:
129
- """Check if comparison is __name__ == '__main__'."""
130
- if not self._is_name_identifier(test.left):
131
- return False
132
- if not self._has_single_eq_operator(test):
133
- return False
134
- return self._compares_to_main(test)
135
-
136
- def _is_name_identifier(self, node: ast.expr) -> bool:
137
- """Check if node is the __name__ identifier."""
138
- return isinstance(node, ast.Name) and node.id == "__name__"
139
-
140
- def _has_single_eq_operator(self, test: ast.Compare) -> bool:
141
- """Check if comparison has single == operator."""
142
- return len(test.ops) == 1 and isinstance(test.ops[0], ast.Eq)
143
-
144
- def _compares_to_main(self, test: ast.Compare) -> bool:
145
- """Check if comparison is to '__main__' string."""
146
- if len(test.comparators) != 1:
147
- return False
148
- comparator = test.comparators[0]
149
- return isinstance(comparator, ast.Constant) and comparator.value == "__main__"
@@ -18,32 +18,23 @@ Exports: TypeScriptPrintStatementAnalyzer class
18
18
  Interfaces: find_console_calls(root_node, methods) -> list[tuple[Node, str, int]]
19
19
 
20
20
  Implementation: Tree-sitter node traversal with call_expression and member_expression pattern matching
21
+
21
22
  """
22
23
 
23
24
  import logging
24
- from typing import Any
25
25
 
26
- from src.analyzers.typescript_base import TypeScriptBaseAnalyzer
26
+ from src.analyzers.typescript_base import (
27
+ TREE_SITTER_AVAILABLE,
28
+ Node,
29
+ TypeScriptBaseAnalyzer,
30
+ )
27
31
 
28
32
  logger = logging.getLogger(__name__)
29
33
 
30
- # dry: ignore-block - tree-sitter import pattern (common across TypeScript analyzers)
31
- try:
32
- from tree_sitter import Node
33
-
34
- TREE_SITTER_AVAILABLE = True
35
- except ImportError:
36
- TREE_SITTER_AVAILABLE = False
37
- Node = Any # type: ignore
38
-
39
34
 
40
35
  class TypeScriptPrintStatementAnalyzer(TypeScriptBaseAnalyzer):
41
36
  """Analyzes TypeScript/JavaScript code for console.* calls using Tree-sitter."""
42
37
 
43
- def __init__(self) -> None: # pylint: disable=useless-parent-delegation
44
- """Initialize the TypeScript print statement analyzer."""
45
- super().__init__() # Sets self.tree_sitter_available from base class
46
-
47
38
  def find_console_calls(self, root_node: Node, methods: set[str]) -> list[tuple[Node, str, int]]:
48
39
  """Find all console.* calls matching the specified methods.
49
40
 
@@ -84,7 +84,7 @@ def has_property_decorator(func_node: ast.FunctionDef | ast.AsyncFunctionDef) ->
84
84
  Returns:
85
85
  True if function has @property decorator
86
86
  """
87
- for decorator in func_node.decorator_list:
88
- if isinstance(decorator, ast.Name) and decorator.id == "property":
89
- return True
90
- return False
87
+ return any(
88
+ isinstance(decorator, ast.Name) and decorator.id == "property"
89
+ for decorator in func_node.decorator_list
90
+ )
src/linters/srp/linter.py CHANGED
@@ -16,9 +16,13 @@ Exports: SRPRule class
16
16
  Interfaces: SRPRule.check(context) -> list[Violation], properties for rule metadata
17
17
 
18
18
  Implementation: Composition pattern with helper classes, heuristic-based SRP analysis
19
+
20
+ Suppressions:
21
+ - type:ignore[return-value]: Generic TypeScript analyzer return type variance
19
22
  """
20
23
 
21
24
  from src.core.base import BaseLintContext, MultiLanguageLintRule
25
+ from src.core.constants import Language
22
26
  from src.core.linter_utils import load_linter_config
23
27
  from src.core.types import Violation
24
28
  from src.linter_config.ignore import get_ignore_parser
@@ -100,10 +104,10 @@ class SRPRule(MultiLanguageLintRule):
100
104
  Returns:
101
105
  List of violations found
102
106
  """
103
- if context.language == "python":
107
+ if context.language == Language.PYTHON:
104
108
  return self._check_python(context, config)
105
109
 
106
- if context.language in ("typescript", "javascript"):
110
+ if context.language in (Language.TYPESCRIPT, Language.JAVASCRIPT):
107
111
  return self._check_typescript(context, config)
108
112
 
109
113
  return []
@@ -133,10 +137,7 @@ class SRPRule(MultiLanguageLintRule):
133
137
  return False
134
138
 
135
139
  file_path = str(context.file_path)
136
- for pattern in config.ignore:
137
- if pattern in file_path:
138
- return True
139
- return False
140
+ return any(pattern in file_path for pattern in config.ignore)
140
141
 
141
142
  def _check_python(self, context: BaseLintContext, config: SRPConfig) -> list[Violation]:
142
143
  """Check Python code for SRP violations.
@@ -170,13 +171,12 @@ class SRPRule(MultiLanguageLintRule):
170
171
  Returns:
171
172
  List of violations
172
173
  """
173
- violations = []
174
174
  valid_metrics = (m for m in metrics_list if isinstance(m, dict))
175
- for metrics in valid_metrics:
176
- violation = self._create_violation_if_needed(metrics, config, context)
177
- if violation:
178
- violations.append(violation)
179
- return violations
175
+ return [
176
+ violation
177
+ for metrics in valid_metrics
178
+ if (violation := self._create_violation_if_needed(metrics, config, context))
179
+ ]
180
180
 
181
181
  def _create_violation_if_needed(
182
182
  self,
@@ -29,10 +29,6 @@ from src.core.violation_builder import BaseViolationBuilder, ViolationInfo
29
29
  class ViolationBuilder(BaseViolationBuilder):
30
30
  """Builds SRP violations with messages and suggestions."""
31
31
 
32
- def __init__(self) -> None: # pylint: disable=useless-parent-delegation
33
- """Initialize the violation builder."""
34
- super().__init__() # Inherits from BaseViolationBuilder
35
-
36
32
  def build_violation(
37
33
  self,
38
34
  metrics: dict[str, Any],