thailint 0.5.0__py3-none-any.whl → 0.15.3__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 (204) hide show
  1. src/__init__.py +1 -0
  2. src/analyzers/__init__.py +4 -3
  3. src/analyzers/ast_utils.py +54 -0
  4. src/analyzers/rust_base.py +155 -0
  5. src/analyzers/rust_context.py +141 -0
  6. src/analyzers/typescript_base.py +4 -0
  7. src/cli/__init__.py +30 -0
  8. src/cli/__main__.py +22 -0
  9. src/cli/config.py +480 -0
  10. src/cli/config_merge.py +241 -0
  11. src/cli/linters/__init__.py +67 -0
  12. src/cli/linters/code_patterns.py +270 -0
  13. src/cli/linters/code_smells.py +342 -0
  14. src/cli/linters/documentation.py +83 -0
  15. src/cli/linters/performance.py +287 -0
  16. src/cli/linters/shared.py +331 -0
  17. src/cli/linters/structure.py +327 -0
  18. src/cli/linters/structure_quality.py +328 -0
  19. src/cli/main.py +120 -0
  20. src/cli/utils.py +395 -0
  21. src/cli_main.py +37 -0
  22. src/config.py +38 -25
  23. src/core/base.py +7 -2
  24. src/core/cli_utils.py +19 -2
  25. src/core/config_parser.py +5 -2
  26. src/core/constants.py +54 -0
  27. src/core/linter_utils.py +95 -6
  28. src/core/python_lint_rule.py +101 -0
  29. src/core/registry.py +1 -1
  30. src/core/rule_discovery.py +147 -84
  31. src/core/types.py +13 -0
  32. src/core/violation_builder.py +78 -15
  33. src/core/violation_utils.py +69 -0
  34. src/formatters/__init__.py +22 -0
  35. src/formatters/sarif.py +202 -0
  36. src/linter_config/directive_markers.py +109 -0
  37. src/linter_config/ignore.py +254 -395
  38. src/linter_config/loader.py +45 -12
  39. src/linter_config/pattern_utils.py +65 -0
  40. src/linter_config/rule_matcher.py +89 -0
  41. src/linters/collection_pipeline/__init__.py +90 -0
  42. src/linters/collection_pipeline/any_all_analyzer.py +281 -0
  43. src/linters/collection_pipeline/ast_utils.py +40 -0
  44. src/linters/collection_pipeline/config.py +75 -0
  45. src/linters/collection_pipeline/continue_analyzer.py +94 -0
  46. src/linters/collection_pipeline/detector.py +360 -0
  47. src/linters/collection_pipeline/filter_map_analyzer.py +402 -0
  48. src/linters/collection_pipeline/linter.py +420 -0
  49. src/linters/collection_pipeline/suggestion_builder.py +130 -0
  50. src/linters/cqs/__init__.py +54 -0
  51. src/linters/cqs/config.py +55 -0
  52. src/linters/cqs/function_analyzer.py +201 -0
  53. src/linters/cqs/input_detector.py +139 -0
  54. src/linters/cqs/linter.py +159 -0
  55. src/linters/cqs/output_detector.py +84 -0
  56. src/linters/cqs/python_analyzer.py +54 -0
  57. src/linters/cqs/types.py +82 -0
  58. src/linters/cqs/typescript_cqs_analyzer.py +61 -0
  59. src/linters/cqs/typescript_function_analyzer.py +192 -0
  60. src/linters/cqs/typescript_input_detector.py +203 -0
  61. src/linters/cqs/typescript_output_detector.py +117 -0
  62. src/linters/cqs/violation_builder.py +94 -0
  63. src/linters/dry/base_token_analyzer.py +16 -9
  64. src/linters/dry/block_filter.py +120 -20
  65. src/linters/dry/block_grouper.py +4 -0
  66. src/linters/dry/cache.py +104 -10
  67. src/linters/dry/cache_query.py +4 -0
  68. src/linters/dry/config.py +54 -11
  69. src/linters/dry/constant.py +92 -0
  70. src/linters/dry/constant_matcher.py +223 -0
  71. src/linters/dry/constant_violation_builder.py +98 -0
  72. src/linters/dry/duplicate_storage.py +5 -4
  73. src/linters/dry/file_analyzer.py +4 -2
  74. src/linters/dry/inline_ignore.py +7 -16
  75. src/linters/dry/linter.py +183 -48
  76. src/linters/dry/python_analyzer.py +60 -439
  77. src/linters/dry/python_constant_extractor.py +100 -0
  78. src/linters/dry/single_statement_detector.py +417 -0
  79. src/linters/dry/token_hasher.py +116 -112
  80. src/linters/dry/typescript_analyzer.py +68 -382
  81. src/linters/dry/typescript_constant_extractor.py +138 -0
  82. src/linters/dry/typescript_statement_detector.py +255 -0
  83. src/linters/dry/typescript_value_extractor.py +70 -0
  84. src/linters/dry/violation_builder.py +4 -0
  85. src/linters/dry/violation_filter.py +5 -4
  86. src/linters/dry/violation_generator.py +71 -14
  87. src/linters/file_header/atemporal_detector.py +68 -50
  88. src/linters/file_header/base_parser.py +93 -0
  89. src/linters/file_header/bash_parser.py +66 -0
  90. src/linters/file_header/config.py +90 -16
  91. src/linters/file_header/css_parser.py +70 -0
  92. src/linters/file_header/field_validator.py +36 -33
  93. src/linters/file_header/linter.py +140 -144
  94. src/linters/file_header/markdown_parser.py +130 -0
  95. src/linters/file_header/python_parser.py +14 -58
  96. src/linters/file_header/typescript_parser.py +73 -0
  97. src/linters/file_header/violation_builder.py +13 -12
  98. src/linters/file_placement/config_loader.py +3 -1
  99. src/linters/file_placement/directory_matcher.py +4 -0
  100. src/linters/file_placement/linter.py +66 -34
  101. src/linters/file_placement/pattern_matcher.py +41 -6
  102. src/linters/file_placement/pattern_validator.py +31 -12
  103. src/linters/file_placement/rule_checker.py +12 -7
  104. src/linters/lazy_ignores/__init__.py +43 -0
  105. src/linters/lazy_ignores/config.py +74 -0
  106. src/linters/lazy_ignores/directive_utils.py +164 -0
  107. src/linters/lazy_ignores/header_parser.py +177 -0
  108. src/linters/lazy_ignores/linter.py +158 -0
  109. src/linters/lazy_ignores/matcher.py +168 -0
  110. src/linters/lazy_ignores/python_analyzer.py +209 -0
  111. src/linters/lazy_ignores/rule_id_utils.py +180 -0
  112. src/linters/lazy_ignores/skip_detector.py +298 -0
  113. src/linters/lazy_ignores/types.py +71 -0
  114. src/linters/lazy_ignores/typescript_analyzer.py +146 -0
  115. src/linters/lazy_ignores/violation_builder.py +135 -0
  116. src/linters/lbyl/__init__.py +31 -0
  117. src/linters/lbyl/config.py +63 -0
  118. src/linters/lbyl/linter.py +67 -0
  119. src/linters/lbyl/pattern_detectors/__init__.py +53 -0
  120. src/linters/lbyl/pattern_detectors/base.py +63 -0
  121. src/linters/lbyl/pattern_detectors/dict_key_detector.py +107 -0
  122. src/linters/lbyl/pattern_detectors/division_check_detector.py +232 -0
  123. src/linters/lbyl/pattern_detectors/file_exists_detector.py +220 -0
  124. src/linters/lbyl/pattern_detectors/hasattr_detector.py +119 -0
  125. src/linters/lbyl/pattern_detectors/isinstance_detector.py +119 -0
  126. src/linters/lbyl/pattern_detectors/len_check_detector.py +173 -0
  127. src/linters/lbyl/pattern_detectors/none_check_detector.py +146 -0
  128. src/linters/lbyl/pattern_detectors/string_validator_detector.py +145 -0
  129. src/linters/lbyl/python_analyzer.py +215 -0
  130. src/linters/lbyl/violation_builder.py +354 -0
  131. src/linters/magic_numbers/context_analyzer.py +227 -225
  132. src/linters/magic_numbers/linter.py +28 -82
  133. src/linters/magic_numbers/python_analyzer.py +4 -16
  134. src/linters/magic_numbers/typescript_analyzer.py +9 -12
  135. src/linters/magic_numbers/typescript_ignore_checker.py +81 -0
  136. src/linters/method_property/__init__.py +49 -0
  137. src/linters/method_property/config.py +138 -0
  138. src/linters/method_property/linter.py +414 -0
  139. src/linters/method_property/python_analyzer.py +473 -0
  140. src/linters/method_property/violation_builder.py +119 -0
  141. src/linters/nesting/linter.py +24 -16
  142. src/linters/nesting/python_analyzer.py +4 -0
  143. src/linters/nesting/typescript_analyzer.py +6 -12
  144. src/linters/nesting/violation_builder.py +1 -0
  145. src/linters/performance/__init__.py +91 -0
  146. src/linters/performance/config.py +43 -0
  147. src/linters/performance/constants.py +49 -0
  148. src/linters/performance/linter.py +149 -0
  149. src/linters/performance/python_analyzer.py +365 -0
  150. src/linters/performance/regex_analyzer.py +312 -0
  151. src/linters/performance/regex_linter.py +139 -0
  152. src/linters/performance/typescript_analyzer.py +236 -0
  153. src/linters/performance/violation_builder.py +160 -0
  154. src/linters/print_statements/config.py +7 -12
  155. src/linters/print_statements/linter.py +26 -43
  156. src/linters/print_statements/python_analyzer.py +91 -93
  157. src/linters/print_statements/typescript_analyzer.py +15 -25
  158. src/linters/print_statements/violation_builder.py +12 -14
  159. src/linters/srp/class_analyzer.py +11 -7
  160. src/linters/srp/heuristics.py +56 -22
  161. src/linters/srp/linter.py +15 -16
  162. src/linters/srp/python_analyzer.py +55 -20
  163. src/linters/srp/typescript_metrics_calculator.py +110 -50
  164. src/linters/stateless_class/__init__.py +25 -0
  165. src/linters/stateless_class/config.py +58 -0
  166. src/linters/stateless_class/linter.py +349 -0
  167. src/linters/stateless_class/python_analyzer.py +290 -0
  168. src/linters/stringly_typed/__init__.py +36 -0
  169. src/linters/stringly_typed/config.py +189 -0
  170. src/linters/stringly_typed/context_filter.py +451 -0
  171. src/linters/stringly_typed/function_call_violation_builder.py +135 -0
  172. src/linters/stringly_typed/ignore_checker.py +100 -0
  173. src/linters/stringly_typed/ignore_utils.py +51 -0
  174. src/linters/stringly_typed/linter.py +376 -0
  175. src/linters/stringly_typed/python/__init__.py +33 -0
  176. src/linters/stringly_typed/python/analyzer.py +348 -0
  177. src/linters/stringly_typed/python/call_tracker.py +175 -0
  178. src/linters/stringly_typed/python/comparison_tracker.py +257 -0
  179. src/linters/stringly_typed/python/condition_extractor.py +134 -0
  180. src/linters/stringly_typed/python/conditional_detector.py +179 -0
  181. src/linters/stringly_typed/python/constants.py +21 -0
  182. src/linters/stringly_typed/python/match_analyzer.py +94 -0
  183. src/linters/stringly_typed/python/validation_detector.py +189 -0
  184. src/linters/stringly_typed/python/variable_extractor.py +96 -0
  185. src/linters/stringly_typed/storage.py +620 -0
  186. src/linters/stringly_typed/storage_initializer.py +45 -0
  187. src/linters/stringly_typed/typescript/__init__.py +28 -0
  188. src/linters/stringly_typed/typescript/analyzer.py +157 -0
  189. src/linters/stringly_typed/typescript/call_tracker.py +335 -0
  190. src/linters/stringly_typed/typescript/comparison_tracker.py +378 -0
  191. src/linters/stringly_typed/violation_generator.py +419 -0
  192. src/orchestrator/core.py +252 -14
  193. src/orchestrator/language_detector.py +5 -3
  194. src/templates/thailint_config_template.yaml +196 -0
  195. src/utils/project_root.py +3 -0
  196. thailint-0.15.3.dist-info/METADATA +187 -0
  197. thailint-0.15.3.dist-info/RECORD +226 -0
  198. thailint-0.15.3.dist-info/entry_points.txt +4 -0
  199. src/cli.py +0 -1665
  200. thailint-0.5.0.dist-info/METADATA +0 -1286
  201. thailint-0.5.0.dist-info/RECORD +0 -96
  202. thailint-0.5.0.dist-info/entry_points.txt +0 -4
  203. {thailint-0.5.0.dist-info → thailint-0.15.3.dist-info}/WHEEL +0 -0
  204. {thailint-0.5.0.dist-info → thailint-0.15.3.dist-info}/licenses/LICENSE +0 -0
@@ -1,28 +1,31 @@
1
1
  """
2
- File: src/linters/print_statements/linter.py
3
-
4
2
  Purpose: Main print statements linter rule implementation
5
3
 
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
4
+ Scope: Print and console statement detection for Python, TypeScript, and JavaScript files
12
5
 
13
- Related: src/linters/magic_numbers/linter.py, src/core/base.py
14
-
15
- Overview: Implements print statements linter rule following BaseLintRule interface. Orchestrates
6
+ Overview: Implements print statements linter rule following MultiLanguageLintRule interface. Orchestrates
16
7
  configuration loading, Python AST analysis for print() calls, TypeScript tree-sitter analysis
17
8
  for console.* calls, and violation building through focused helper classes. Detects print and
18
9
  console statements that should be replaced with proper logging. Supports configurable
19
10
  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.
11
+ set for TypeScript/JavaScript. Handles ignore directives for suppressing specific violations
12
+ through inline comments and configuration patterns.
13
+
14
+ Dependencies: BaseLintContext and MultiLanguageLintRule from core, ast module, pathlib,
15
+ analyzer classes, config classes
21
16
 
22
- Usage: rule = PrintStatementRule()
23
- violations = rule.check(context)
17
+ Exports: PrintStatementRule class implementing MultiLanguageLintRule interface
24
18
 
25
- Notes: Composition pattern with helper classes, AST-based analysis for Python, tree-sitter for TS/JS
19
+ Interfaces: check(context) -> list[Violation] for rule validation, standard rule properties
20
+ (rule_id, rule_name, description)
21
+
22
+ Implementation: Composition pattern with helper classes (analyzers, violation builder),
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).
26
29
  """
27
30
 
28
31
  import ast
@@ -31,7 +34,8 @@ from pathlib import Path
31
34
  from src.core.base import BaseLintContext, MultiLanguageLintRule
32
35
  from src.core.linter_utils import load_linter_config
33
36
  from src.core.types import Violation
34
- from src.linter_config.ignore import IgnoreDirectiveParser
37
+ from src.core.violation_utils import get_violation_line, has_python_noqa, has_typescript_noqa
38
+ from src.linter_config.ignore import get_ignore_parser
35
39
 
36
40
  from .config import PrintStatementConfig
37
41
  from .python_analyzer import PythonPrintStatementAnalyzer
@@ -44,7 +48,7 @@ class PrintStatementRule(MultiLanguageLintRule): # thailint: ignore[srp]
44
48
 
45
49
  def __init__(self) -> None:
46
50
  """Initialize the print statements rule."""
47
- self._ignore_parser = IgnoreDirectiveParser()
51
+ self._ignore_parser = get_ignore_parser()
48
52
  self._violation_builder = ViolationBuilder(self.rule_id)
49
53
 
50
54
  @property
@@ -122,10 +126,7 @@ class PrintStatementRule(MultiLanguageLintRule): # thailint: ignore[srp]
122
126
  return False
123
127
 
124
128
  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
+ return any(self._matches_pattern(file_path, pattern) for pattern in config.ignore)
129
130
 
130
131
  def _matches_pattern(self, file_path: Path, pattern: str) -> bool:
131
132
  """Check if file path matches a glob pattern.
@@ -257,27 +258,16 @@ class PrintStatementRule(MultiLanguageLintRule): # thailint: ignore[srp]
257
258
  Returns:
258
259
  True if line has generic ignore directive
259
260
  """
260
- line_text = self._get_violation_line(violation, context)
261
+ line_text = get_violation_line(violation, context)
261
262
  if line_text is None:
262
263
  return False
263
264
  return self._has_generic_ignore_directive(line_text)
264
265
 
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
266
  def _has_generic_ignore_directive(self, line_text: str) -> bool:
277
267
  """Check if line has generic ignore directive."""
278
268
  if self._has_generic_thailint_ignore(line_text):
279
269
  return True
280
- return self._has_noqa_directive(line_text)
270
+ return has_python_noqa(line_text)
281
271
 
282
272
  def _has_generic_thailint_ignore(self, line_text: str) -> bool:
283
273
  """Check for generic thailint: ignore (no brackets)."""
@@ -286,10 +276,6 @@ class PrintStatementRule(MultiLanguageLintRule): # thailint: ignore[srp]
286
276
  after_ignore = line_text.split("# thailint: ignore")[1].split("#")[0]
287
277
  return "[" not in after_ignore
288
278
 
289
- def _has_noqa_directive(self, line_text: str) -> bool:
290
- """Check for noqa-style comments."""
291
- return "# noqa" in line_text
292
-
293
279
  def _check_typescript(
294
280
  self, context: BaseLintContext, config: PrintStatementConfig
295
281
  ) -> list[Violation]:
@@ -402,7 +388,7 @@ class PrintStatementRule(MultiLanguageLintRule): # thailint: ignore[srp]
402
388
  Returns:
403
389
  True if line has ignore directive
404
390
  """
405
- line_text = self._get_violation_line(violation, context)
391
+ line_text = get_violation_line(violation, context)
406
392
  if line_text is None:
407
393
  return False
408
394
  return self._has_typescript_ignore_directive(line_text)
@@ -424,7 +410,4 @@ class PrintStatementRule(MultiLanguageLintRule): # thailint: ignore[srp]
424
410
  if "[" not in after_ignore:
425
411
  return True
426
412
 
427
- if "// noqa" in line_text:
428
- return True
429
-
430
- return False
413
+ return has_typescript_noqa(line_text)
@@ -1,34 +1,107 @@
1
1
  """
2
- File: src/linters/print_statements/python_analyzer.py
3
-
4
2
  Purpose: Python AST analysis for finding print() call nodes
5
3
 
6
- Exports: PythonPrintStatementAnalyzer class
7
-
8
- Depends: ast module for AST parsing and node types
9
-
10
- Implements: PythonPrintStatementAnalyzer.find_print_calls(tree) -> list[tuple],
11
- PythonPrintStatementAnalyzer.is_in_main_block(node) -> bool
12
-
13
- Related: src/linters/magic_numbers/python_analyzer.py
4
+ Scope: Python print() statement detection and __main__ block context analysis
14
5
 
15
6
  Overview: Provides PythonPrintStatementAnalyzer class that traverses Python AST to find all
16
7
  print() function calls. Uses ast.walk() to traverse the syntax tree and collect
17
8
  Call nodes where the function is 'print'. Tracks parent nodes to detect if print calls
18
9
  are within __main__ blocks (if __name__ == "__main__":) for allow_in_scripts filtering.
19
10
  Returns structured data about each print call including the AST node, parent context,
20
- and line number for violation reporting.
11
+ and line number for violation reporting. Handles both simple print() and builtins.print() calls.
21
12
 
22
- Usage: analyzer = PythonPrintStatementAnalyzer()
23
- print_calls = analyzer.find_print_calls(ast.parse(code))
13
+ Dependencies: ast module for AST parsing and node types, analyzers.ast_utils
24
14
 
25
- Notes: AST walk pattern with parent tracking for context detection
15
+ Exports: PythonPrintStatementAnalyzer class, is_print_call function, is_main_if_block function
16
+
17
+ Interfaces: find_print_calls(tree) -> list[tuple[Call, AST | None, int]], is_in_main_block(node) -> bool
18
+
19
+ Implementation: AST walk pattern with parent map for context detection and __main__ block identification
26
20
  """
27
21
 
28
22
  import ast
29
23
 
24
+ from src.analyzers.ast_utils import build_parent_map
25
+
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
+
30
82
 
31
- class PythonPrintStatementAnalyzer: # thailint: ignore[srp]
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:
32
105
  """Analyzes Python AST to find print() calls."""
33
106
 
34
107
  def __init__(self) -> None:
@@ -46,24 +119,10 @@ class PythonPrintStatementAnalyzer: # thailint: ignore[srp]
46
119
  List of tuples (node, parent, line_number)
47
120
  """
48
121
  self.print_calls = []
49
- self.parent_map = {}
50
- self._build_parent_map(tree)
122
+ self.parent_map = build_parent_map(tree)
51
123
  self._collect_print_calls(tree)
52
124
  return self.print_calls
53
125
 
54
- def _build_parent_map(self, node: ast.AST, parent: ast.AST | None = None) -> None:
55
- """Build a map of nodes to their parents.
56
-
57
- Args:
58
- node: Current AST node
59
- parent: Parent of current node
60
- """
61
- if parent is not None:
62
- self.parent_map[node] = parent
63
-
64
- for child in ast.iter_child_nodes(node):
65
- self._build_parent_map(child, node)
66
-
67
126
  def _collect_print_calls(self, tree: ast.AST) -> None:
68
127
  """Walk tree and collect all print() calls.
69
128
 
@@ -71,34 +130,11 @@ class PythonPrintStatementAnalyzer: # thailint: ignore[srp]
71
130
  tree: AST to traverse
72
131
  """
73
132
  for node in ast.walk(tree):
74
- if isinstance(node, ast.Call) and self._is_print_call(node):
133
+ if isinstance(node, ast.Call) and is_print_call(node):
75
134
  parent = self.parent_map.get(node)
76
135
  line_number = node.lineno if hasattr(node, "lineno") else 0
77
136
  self.print_calls.append((node, parent, line_number))
78
137
 
79
- def _is_print_call(self, node: ast.Call) -> bool:
80
- """Check if a Call node is calling print().
81
-
82
- Args:
83
- node: The Call node to check
84
-
85
- Returns:
86
- True if this is a print() call
87
- """
88
- return self._is_simple_print(node) or self._is_builtins_print(node)
89
-
90
- def _is_simple_print(self, node: ast.Call) -> bool:
91
- """Check for simple print() call."""
92
- return isinstance(node.func, ast.Name) and node.func.id == "print"
93
-
94
- def _is_builtins_print(self, node: ast.Call) -> bool:
95
- """Check for builtins.print() call."""
96
- if not isinstance(node.func, ast.Attribute):
97
- return False
98
- if node.func.attr != "print":
99
- return False
100
- return isinstance(node.func.value, ast.Name) and node.func.value.id == "builtins"
101
-
102
138
  def is_in_main_block(self, node: ast.AST) -> bool:
103
139
  """Check if node is within `if __name__ == "__main__":` block.
104
140
 
@@ -111,45 +147,7 @@ class PythonPrintStatementAnalyzer: # thailint: ignore[srp]
111
147
  current = node
112
148
  while current in self.parent_map:
113
149
  parent = self.parent_map[current]
114
- if self._is_main_if_block(parent):
150
+ if is_main_if_block(parent):
115
151
  return True
116
152
  current = parent
117
153
  return False
118
-
119
- def _is_main_if_block(self, node: ast.AST) -> bool:
120
- """Check if node is an `if __name__ == "__main__":` statement.
121
-
122
- Args:
123
- node: AST node to check
124
-
125
- Returns:
126
- True if this is a __main__ if block
127
- """
128
- if not isinstance(node, ast.If):
129
- return False
130
- if not isinstance(node.test, ast.Compare):
131
- return False
132
- return self._is_main_comparison(node.test)
133
-
134
- def _is_main_comparison(self, test: ast.Compare) -> bool:
135
- """Check if comparison is __name__ == '__main__'."""
136
- if not self._is_name_identifier(test.left):
137
- return False
138
- if not self._has_single_eq_operator(test):
139
- return False
140
- return self._compares_to_main(test)
141
-
142
- def _is_name_identifier(self, node: ast.expr) -> bool:
143
- """Check if node is the __name__ identifier."""
144
- return isinstance(node, ast.Name) and node.id == "__name__"
145
-
146
- def _has_single_eq_operator(self, test: ast.Compare) -> bool:
147
- """Check if comparison has single == operator."""
148
- return len(test.ops) == 1 and isinstance(test.ops[0], ast.Eq)
149
-
150
- def _compares_to_main(self, test: ast.Compare) -> bool:
151
- """Check if comparison is to '__main__' string."""
152
- if len(test.comparators) != 1:
153
- return False
154
- comparator = test.comparators[0]
155
- return isinstance(comparator, ast.Constant) and comparator.value == "__main__"
@@ -1,46 +1,36 @@
1
1
  """
2
- File: src/linters/print_statements/typescript_analyzer.py
3
-
4
2
  Purpose: TypeScript/JavaScript console.* call detection using Tree-sitter AST analysis
5
3
 
6
- Exports: TypeScriptPrintStatementAnalyzer class
7
-
8
- Depends: TypeScriptBaseAnalyzer for tree-sitter parsing, tree-sitter Node type
9
-
10
- Implements: find_console_calls(root_node, methods) -> list[tuple]
11
-
12
- Related: src/linters/magic_numbers/typescript_analyzer.py, src/analyzers/typescript_base.py
4
+ Scope: TypeScript and JavaScript console statement detection
13
5
 
14
6
  Overview: Analyzes TypeScript and JavaScript code to detect console.* method calls that should
15
7
  be replaced with proper logging. Uses Tree-sitter parser to traverse TypeScript/JavaScript
16
8
  AST and identify call expressions where the callee is console.log, console.warn, console.error,
17
9
  console.debug, or console.info (configurable). Returns structured data with the node, method
18
10
  name, and line number for each detected console call. Supports both TypeScript and JavaScript
19
- files with shared detection logic.
11
+ files with shared detection logic. Handles member expression pattern matching to identify
12
+ console object method calls.
20
13
 
21
- Usage: analyzer = TypeScriptPrintStatementAnalyzer()
22
- root = analyzer.parse_typescript(code)
23
- calls = analyzer.find_console_calls(root, {"log", "warn", "error"})
14
+ Dependencies: TypeScriptBaseAnalyzer for tree-sitter parsing infrastructure, tree-sitter Node type, logging module
15
+
16
+ Exports: TypeScriptPrintStatementAnalyzer class
17
+
18
+ Interfaces: find_console_calls(root_node, methods) -> list[tuple[Node, str, int]]
19
+
20
+ Implementation: Tree-sitter node traversal with call_expression and member_expression pattern matching
24
21
 
25
- Notes: Tree-sitter node traversal with call_expression and member_expression pattern matching
26
22
  """
27
23
 
28
24
  import logging
29
- from typing import Any
30
25
 
31
- from src.analyzers.typescript_base import TypeScriptBaseAnalyzer
26
+ from src.analyzers.typescript_base import (
27
+ TREE_SITTER_AVAILABLE,
28
+ Node,
29
+ TypeScriptBaseAnalyzer,
30
+ )
32
31
 
33
32
  logger = logging.getLogger(__name__)
34
33
 
35
- # dry: ignore-block - tree-sitter import pattern (common across TypeScript analyzers)
36
- try:
37
- from tree_sitter import Node
38
-
39
- TREE_SITTER_AVAILABLE = True
40
- except ImportError:
41
- TREE_SITTER_AVAILABLE = False
42
- Node = Any # type: ignore
43
-
44
34
 
45
35
  class TypeScriptPrintStatementAnalyzer(TypeScriptBaseAnalyzer):
46
36
  """Analyzes TypeScript/JavaScript code for console.* calls using Tree-sitter."""
@@ -1,27 +1,25 @@
1
1
  """
2
- File: src/linters/print_statements/violation_builder.py
3
-
4
2
  Purpose: Builds Violation objects for print statement detection
5
3
 
6
- Exports: ViolationBuilder class
7
-
8
- Depends: ast, pathlib.Path, src.core.types.Violation
9
-
10
- Implements: ViolationBuilder.create_python_violation(node, line, file_path) -> Violation,
11
- ViolationBuilder.create_typescript_violation(method, line, file_path) -> Violation
12
-
13
- Related: src/linters/magic_numbers/violation_builder.py, src/core/types.py
4
+ Scope: Violation creation for print and console statement detections
14
5
 
15
6
  Overview: Provides ViolationBuilder class that creates Violation objects for print statement
16
7
  detections. Generates descriptive messages suggesting the use of proper logging instead of
17
8
  print/console statements. Constructs complete Violation instances with rule_id, file_path,
18
9
  line number, column, message, and suggestions. Provides separate methods for Python print()
19
- violations and TypeScript/JavaScript console.* violations with language-appropriate messages.
10
+ violations and TypeScript/JavaScript console.* violations with language-appropriate messages
11
+ and helpful remediation guidance.
12
+
13
+ Dependencies: ast module for Python AST nodes, pathlib.Path for file paths,
14
+ src.core.types.Violation for violation structure
15
+
16
+ Exports: ViolationBuilder class
20
17
 
21
- Usage: builder = ViolationBuilder("print-statements.detected")
22
- violation = builder.create_python_violation(node, line, file_path)
18
+ Interfaces: create_python_violation(node, line, file_path) -> Violation,
19
+ create_typescript_violation(method, line, file_path) -> Violation
23
20
 
24
- Notes: Message templates suggest logging as alternative, consistent with other linter patterns
21
+ Implementation: Builder pattern with message templates suggesting logging as alternative
22
+ to print/console statements
25
23
  """
26
24
 
27
25
  import ast
@@ -31,6 +31,12 @@ from .typescript_analyzer import TypeScriptSRPAnalyzer
31
31
  class ClassAnalyzer:
32
32
  """Coordinates class analysis for Python and TypeScript."""
33
33
 
34
+ def __init__(self) -> None:
35
+ """Initialize the class analyzer with singleton analyzers."""
36
+ # Singleton analyzers for performance (avoid recreating per-file)
37
+ self._python_analyzer = PythonSRPAnalyzer()
38
+ self._typescript_analyzer = TypeScriptSRPAnalyzer()
39
+
34
40
  def analyze_python(
35
41
  self, context: BaseLintContext, config: SRPConfig
36
42
  ) -> list[dict[str, Any]] | list[Violation]:
@@ -47,10 +53,9 @@ class ClassAnalyzer:
47
53
  if isinstance(tree, list): # Syntax error violations
48
54
  return tree
49
55
 
50
- analyzer = PythonSRPAnalyzer()
51
- classes = analyzer.find_all_classes(tree)
56
+ classes = self._python_analyzer.find_all_classes(tree)
52
57
  return [
53
- analyzer.analyze_class(class_node, context.file_content or "", config)
58
+ self._python_analyzer.analyze_class(class_node, context.file_content or "", config)
54
59
  for class_node in classes
55
60
  ]
56
61
 
@@ -66,14 +71,13 @@ class ClassAnalyzer:
66
71
  Returns:
67
72
  List of class metrics dicts
68
73
  """
69
- analyzer = TypeScriptSRPAnalyzer()
70
- root_node = analyzer.parse_typescript(context.file_content or "")
74
+ root_node = self._typescript_analyzer.parse_typescript(context.file_content or "")
71
75
  if not root_node:
72
76
  return []
73
77
 
74
- classes = analyzer.find_all_classes(root_node)
78
+ classes = self._typescript_analyzer.find_all_classes(root_node)
75
79
  return [
76
- analyzer.analyze_class(class_node, context.file_content or "", config)
80
+ self._typescript_analyzer.analyze_class(class_node, context.file_content or "", config)
77
81
  for class_node in classes
78
82
  ]
79
83
 
@@ -4,12 +4,13 @@ Purpose: SRP detection heuristics for analyzing code complexity and responsibili
4
4
  Scope: Helper functions for method counting, LOC calculation, and keyword detection
5
5
 
6
6
  Overview: Provides heuristic-based analysis functions for detecting Single Responsibility
7
- Principle violations. Implements method counting that excludes property decorators and
8
- special methods. Provides LOC calculation that filters out blank lines and comments.
9
- Includes keyword detection for identifying generic class names that often indicate SRP
10
- violations (Manager, Handler, etc.). Supports both Python AST and TypeScript tree-sitter
11
- nodes. These heuristics enable practical SRP detection without requiring perfect semantic
12
- analysis, focusing on measurable code metrics that correlate with responsibility scope.
7
+ Principle violations. Implements method counting that excludes property decorators,
8
+ private methods, and special methods. Provides LOC calculation that filters out blank
9
+ lines and comments. Includes keyword detection for identifying generic class names that
10
+ often indicate SRP violations (Manager, Handler, etc.). Supports both Python AST and
11
+ TypeScript tree-sitter nodes. These heuristics enable practical SRP detection without
12
+ requiring perfect semantic analysis, focusing on measurable code metrics that correlate
13
+ with responsibility scope.
13
14
 
14
15
  Dependencies: ast module for Python AST analysis, typing for type hints
15
16
 
@@ -24,22 +25,55 @@ import ast
24
25
 
25
26
 
26
27
  def count_methods(class_node: ast.ClassDef) -> int:
27
- """Count methods in a class (excludes properties and special methods).
28
+ """Count public methods in a class (excludes properties and private methods).
29
+
30
+ Private methods are those starting with underscore (_), including dunder
31
+ methods (__init__, __str__, etc.). This focuses SRP analysis on the public
32
+ interface rather than implementation details.
28
33
 
29
34
  Args:
30
35
  class_node: AST node representing a class definition
31
36
 
32
37
  Returns:
33
- Number of methods in the class
38
+ Number of public methods in the class
39
+ """
40
+ func_nodes = (
41
+ n for n in class_node.body if isinstance(n, (ast.FunctionDef, ast.AsyncFunctionDef))
42
+ )
43
+ public_methods = [n for n in func_nodes if _is_countable_method(n)]
44
+ return len(public_methods)
45
+
46
+
47
+ def _is_countable_method(node: ast.FunctionDef | ast.AsyncFunctionDef) -> bool:
48
+ """Check if a method should be counted (public and not a property).
49
+
50
+ Args:
51
+ node: Function AST node
52
+
53
+ Returns:
54
+ True if method should be counted
55
+ """
56
+ if has_property_decorator(node):
57
+ return False
58
+ if _is_private_method(node.name):
59
+ return False
60
+ return True
61
+
62
+
63
+ def _is_private_method(method_name: str) -> bool:
64
+ """Check if method is private (starts with underscore).
65
+
66
+ This includes both single underscore (_helper) and dunder methods
67
+ (__init__, __str__). All underscore-prefixed methods are considered
68
+ implementation details.
69
+
70
+ Args:
71
+ method_name: Name of the method to check
72
+
73
+ Returns:
74
+ True if method is private, False otherwise
34
75
  """
35
- methods = 0
36
- for node in class_node.body:
37
- if not isinstance(node, (ast.FunctionDef, ast.AsyncFunctionDef)):
38
- continue
39
- # Don't count @property decorators as methods
40
- if not has_property_decorator(node):
41
- methods += 1
42
- return methods
76
+ return method_name.startswith("_")
43
77
 
44
78
 
45
79
  def count_loc(class_node: ast.ClassDef, source: str) -> int:
@@ -56,8 +90,8 @@ def count_loc(class_node: ast.ClassDef, source: str) -> int:
56
90
  end_line = class_node.end_lineno or start_line
57
91
  lines = source.split("\n")[start_line - 1 : end_line]
58
92
 
59
- # Filter out blank lines and comments
60
- code_lines = [line for line in lines if line.strip() and not line.strip().startswith("#")]
93
+ # Filter out blank lines and comments (using walrus operator to avoid double strip)
94
+ code_lines = [s for line in lines if (s := line.strip()) and not s.startswith("#")]
61
95
  return len(code_lines)
62
96
 
63
97
 
@@ -83,7 +117,7 @@ def has_property_decorator(func_node: ast.FunctionDef | ast.AsyncFunctionDef) ->
83
117
  Returns:
84
118
  True if function has @property decorator
85
119
  """
86
- for decorator in func_node.decorator_list:
87
- if isinstance(decorator, ast.Name) and decorator.id == "property":
88
- return True
89
- return False
120
+ return any(
121
+ isinstance(decorator, ast.Name) and decorator.id == "property"
122
+ for decorator in func_node.decorator_list
123
+ )