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
@@ -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,20 +21,28 @@ 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
31
37
  from src.core.types import Violation
32
- from src.linter_config.ignore import IgnoreDirectiveParser
38
+ from src.core.violation_utils import get_violation_line, has_python_noqa
39
+ from src.linter_config.ignore import get_ignore_parser
33
40
 
34
41
  from .config import MagicNumberConfig
35
- from .context_analyzer import ContextAnalyzer
42
+ from .context_analyzer import is_acceptable_context
36
43
  from .python_analyzer import PythonMagicNumberAnalyzer
37
44
  from .typescript_analyzer import TypeScriptMagicNumberAnalyzer
45
+ from .typescript_ignore_checker import TypeScriptIgnoreChecker
38
46
  from .violation_builder import ViolationBuilder
39
47
 
40
48
 
@@ -43,9 +51,9 @@ class MagicNumberRule(MultiLanguageLintRule): # thailint: ignore[srp]
43
51
 
44
52
  def __init__(self) -> None:
45
53
  """Initialize the magic numbers rule."""
46
- self._ignore_parser = IgnoreDirectiveParser()
54
+ self._ignore_parser = get_ignore_parser()
47
55
  self._violation_builder = ViolationBuilder(self.rule_id)
48
- self._context_analyzer = ContextAnalyzer()
56
+ self._typescript_ignore_checker = TypeScriptIgnoreChecker()
49
57
 
50
58
  @property
51
59
  def rule_id(self) -> str:
@@ -131,10 +139,7 @@ class MagicNumberRule(MultiLanguageLintRule): # thailint: ignore[srp]
131
139
  return False
132
140
 
133
141
  file_path = Path(context.file_path)
134
- for pattern in config.ignore:
135
- if self._matches_pattern(file_path, pattern):
136
- return True
137
- return False
142
+ return any(self._matches_pattern(file_path, pattern) for pattern in config.ignore)
138
143
 
139
144
  def _matches_pattern(self, file_path: Path, pattern: str) -> bool:
140
145
  """Check if file path matches a glob pattern.
@@ -248,9 +253,7 @@ class MagicNumberRule(MultiLanguageLintRule): # thailint: ignore[srp]
248
253
  "allowed_numbers": config.allowed_numbers,
249
254
  }
250
255
 
251
- if self._context_analyzer.is_acceptable_context(
252
- node, parent, context.file_path, config_dict
253
- ):
256
+ if is_acceptable_context(node, parent, context.file_path, config_dict):
254
257
  return False
255
258
 
256
259
  return True
@@ -282,28 +285,17 @@ class MagicNumberRule(MultiLanguageLintRule): # thailint: ignore[srp]
282
285
  Returns:
283
286
  True if line has generic ignore directive
284
287
  """
285
- line_text = self._get_violation_line(violation, context)
288
+ line_text = get_violation_line(violation, context)
286
289
  if line_text is None:
287
290
  return False
288
291
 
289
292
  return self._has_generic_ignore_directive(line_text)
290
293
 
291
- def _get_violation_line(self, violation: Violation, context: BaseLintContext) -> str | None:
292
- """Get the line text for a violation."""
293
- if not context.file_content:
294
- return None
295
-
296
- lines = context.file_content.splitlines()
297
- if violation.line <= 0 or violation.line > len(lines):
298
- return None
299
-
300
- return lines[violation.line - 1].lower()
301
-
302
294
  def _has_generic_ignore_directive(self, line_text: str) -> bool:
303
295
  """Check if line has generic ignore directive."""
304
296
  if self._has_generic_thailint_ignore(line_text):
305
297
  return True
306
- return self._has_noqa_directive(line_text)
298
+ return has_python_noqa(line_text)
307
299
 
308
300
  def _has_generic_thailint_ignore(self, line_text: str) -> bool:
309
301
  """Check for generic thailint: ignore (no brackets)."""
@@ -312,10 +304,6 @@ class MagicNumberRule(MultiLanguageLintRule): # thailint: ignore[srp]
312
304
  after_ignore = line_text.split("# thailint: ignore")[1].split("#")[0]
313
305
  return "[" not in after_ignore
314
306
 
315
- def _has_noqa_directive(self, line_text: str) -> bool:
316
- """Check for noqa-style comments."""
317
- return "# noqa" in line_text
318
-
319
307
  def _check_typescript(
320
308
  self, context: BaseLintContext, config: MagicNumberConfig
321
309
  ) -> list[Violation]:
@@ -433,12 +421,17 @@ class MagicNumberRule(MultiLanguageLintRule): # thailint: ignore[srp]
433
421
  return value in config.allowed_numbers or self._is_test_file(context.file_path)
434
422
 
435
423
  def _is_typescript_special_context(
436
- self, node: object, analyzer: TypeScriptMagicNumberAnalyzer, context: BaseLintContext
424
+ self, node: Any, analyzer: TypeScriptMagicNumberAnalyzer, context: BaseLintContext
437
425
  ) -> bool:
438
- """Check if in TypeScript-specific special context."""
439
- # Calls require type: ignore because node is typed as object but analyzer expects Node
440
- in_enum = analyzer.is_enum_context(node) # type: ignore[arg-type]
441
- 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 "")
442
435
  return in_enum or in_const_def
443
436
 
444
437
  def _is_test_file(self, file_path: object) -> bool:
@@ -466,51 +459,4 @@ class MagicNumberRule(MultiLanguageLintRule): # thailint: ignore[srp]
466
459
  Returns:
467
460
  True if should ignore
468
461
  """
469
- # Check standard ignore directives
470
- if self._ignore_parser.should_ignore_violation(violation, context.file_content or ""):
471
- return True
472
-
473
- # Check TypeScript-style comments
474
- return self._check_typescript_ignore(violation, context)
475
-
476
- def _check_typescript_ignore(self, violation: Violation, context: BaseLintContext) -> bool:
477
- """Check for TypeScript-style ignore directives.
478
-
479
- Args:
480
- violation: Violation to check
481
- context: Lint context
482
-
483
- Returns:
484
- True if line has ignore directive
485
- """
486
- line_text = self._get_violation_line(violation, context)
487
- if line_text is None:
488
- return False
489
-
490
- # Check for // thailint: ignore or // noqa
491
- return self._has_typescript_ignore_directive(line_text)
492
-
493
- def _has_typescript_ignore_directive(self, line_text: str) -> bool:
494
- """Check if line has TypeScript-style ignore directive.
495
-
496
- Args:
497
- line_text: Line text to check
498
-
499
- Returns:
500
- True if has ignore directive
501
- """
502
- # Check for // thailint: ignore[magic-numbers]
503
- if "// thailint: ignore[magic-numbers]" in line_text:
504
- return True
505
-
506
- # Check for // thailint: ignore (generic)
507
- if "// thailint: ignore" in line_text:
508
- after_ignore = line_text.split("// thailint: ignore")[1].split("//")[0]
509
- if "[" not in after_ignore:
510
- return True
511
-
512
- # Check for // noqa
513
- if "// noqa" in line_text:
514
- return True
515
-
516
- return False
462
+ return self._typescript_ignore_checker.should_ignore(violation, context)
@@ -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
23
 
27
- from src.analyzers.typescript_base import TypeScriptBaseAnalyzer
28
-
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]
@@ -0,0 +1,81 @@
1
+ """
2
+ Purpose: TypeScript-specific ignore directive checking for magic numbers linter
3
+
4
+ Scope: Ignore directive detection for TypeScript/JavaScript files
5
+
6
+ Overview: Provides ignore directive checking functionality specifically for TypeScript and JavaScript
7
+ files in the magic numbers linter. Handles both thailint-style and noqa-style ignore comments
8
+ using TypeScript comment syntax (// instead of #). Extracted from linter.py to reduce file
9
+ size and improve modularity.
10
+
11
+ Dependencies: IgnoreDirectiveParser from src.linter_config.ignore, Violation type, violation_utils
12
+
13
+ Exports: TypeScriptIgnoreChecker class
14
+
15
+ Interfaces: TypeScriptIgnoreChecker.should_ignore(violation, context) -> bool
16
+
17
+ Implementation: Comment parsing with TypeScript-specific syntax handling, uses shared utilities
18
+ """
19
+
20
+ from src.core.base import BaseLintContext
21
+ from src.core.types import Violation
22
+ from src.core.violation_utils import get_violation_line, has_typescript_noqa
23
+ from src.linter_config.ignore import get_ignore_parser
24
+
25
+
26
+ class TypeScriptIgnoreChecker:
27
+ """Checks for TypeScript-style ignore directives in magic numbers linter."""
28
+
29
+ def __init__(self) -> None:
30
+ """Initialize with standard ignore parser."""
31
+ self._ignore_parser = get_ignore_parser()
32
+
33
+ def should_ignore(self, violation: Violation, context: BaseLintContext) -> bool:
34
+ """Check if TypeScript violation should be ignored.
35
+
36
+ Args:
37
+ violation: Violation to check
38
+ context: Lint context
39
+
40
+ Returns:
41
+ True if should ignore
42
+ """
43
+ if self._ignore_parser.should_ignore_violation(violation, context.file_content or ""):
44
+ return True
45
+
46
+ return self._check_typescript_ignore(violation, context)
47
+
48
+ def _check_typescript_ignore(self, violation: Violation, context: BaseLintContext) -> bool:
49
+ """Check for TypeScript-style ignore directives.
50
+
51
+ Args:
52
+ violation: Violation to check
53
+ context: Lint context
54
+
55
+ Returns:
56
+ True if line has ignore directive
57
+ """
58
+ line_text = get_violation_line(violation, context)
59
+ if line_text is None:
60
+ return False
61
+
62
+ return self._has_typescript_ignore_directive(line_text)
63
+
64
+ def _has_typescript_ignore_directive(self, line_text: str) -> bool:
65
+ """Check if line has TypeScript-style ignore directive.
66
+
67
+ Args:
68
+ line_text: Line text to check
69
+
70
+ Returns:
71
+ True if has ignore directive
72
+ """
73
+ if "// thailint: ignore[magic-numbers]" in line_text:
74
+ return True
75
+
76
+ if "// thailint: ignore" in line_text:
77
+ after_ignore = line_text.split("// thailint: ignore")[1].split("//")[0]
78
+ if "[" not in after_ignore:
79
+ return True
80
+
81
+ return has_typescript_noqa(line_text)
@@ -0,0 +1,49 @@
1
+ """
2
+ Purpose: Package exports for method-should-be-property linter
3
+
4
+ Scope: Method property linter public API
5
+
6
+ Overview: Exports the MethodPropertyRule class and MethodPropertyConfig dataclass for use by
7
+ the orchestrator and external consumers. Provides a convenience lint() function for
8
+ standalone usage of the linter.
9
+
10
+ Dependencies: MethodPropertyRule from linter module, MethodPropertyConfig from config module
11
+
12
+ Exports: MethodPropertyRule, MethodPropertyConfig, lint function
13
+
14
+ Interfaces: lint(file_path, content, config) -> list[Violation] convenience function
15
+
16
+ Implementation: Simple re-exports from submodules with optional convenience wrapper
17
+ """
18
+
19
+ from .config import MethodPropertyConfig
20
+ from .linter import MethodPropertyRule
21
+
22
+ __all__ = ["MethodPropertyRule", "MethodPropertyConfig", "lint"]
23
+
24
+
25
+ def lint(
26
+ file_path: str,
27
+ content: str,
28
+ config: dict | None = None,
29
+ ) -> list:
30
+ """Lint a file for method-should-be-property violations.
31
+
32
+ Args:
33
+ file_path: Path to the file being linted
34
+ content: Content of the file
35
+ config: Optional configuration dictionary
36
+
37
+ Returns:
38
+ List of Violation objects
39
+ """
40
+ from unittest.mock import Mock
41
+
42
+ rule = MethodPropertyRule()
43
+ context = Mock()
44
+ context.file_path = file_path
45
+ context.file_content = content
46
+ context.language = "python"
47
+ context.config = config
48
+
49
+ return rule.check(context)
@@ -0,0 +1,138 @@
1
+ """
2
+ Purpose: Configuration schema for method-should-be-property linter
3
+
4
+ Scope: Method property linter configuration for Python files
5
+
6
+ Overview: Defines configuration schema for method-should-be-property linter. Provides
7
+ MethodPropertyConfig dataclass with enabled flag, max_body_statements threshold (default 3)
8
+ for determining when a method body is too complex to be a property candidate, and ignore
9
+ patterns list for excluding specific files or directories. Includes configurable action verb
10
+ exclusions (prefixes and names) with sensible defaults that can be extended or overridden.
11
+ Supports per-file and per-directory config overrides through from_dict class method.
12
+ Integrates with orchestrator's configuration system via .thailint.yaml.
13
+
14
+ Dependencies: dataclasses module for configuration structure, typing module for type hints
15
+
16
+ Exports: MethodPropertyConfig dataclass, DEFAULT_EXCLUDE_PREFIXES, DEFAULT_EXCLUDE_NAMES
17
+
18
+ Interfaces: from_dict(config, language) -> MethodPropertyConfig for configuration loading
19
+
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.
25
+ """
26
+
27
+ from dataclasses import dataclass, field
28
+ from typing import Any
29
+
30
+ # Default action verb prefixes - methods starting with these are excluded
31
+ # These represent actions/transformations, not property access
32
+ DEFAULT_EXCLUDE_PREFIXES: tuple[str, ...] = (
33
+ "to_", # Transformation: to_dict, to_json, to_string
34
+ "dump_", # Serialization: dump_to_json, dump_to_apigw
35
+ "generate_", # Factory: generate_report, generate_html
36
+ "create_", # Factory: create_instance, create_config
37
+ "build_", # Construction: build_query, build_html
38
+ "make_", # Factory: make_request, make_connection
39
+ "render_", # Output: render_template, render_html
40
+ "compute_", # Calculation: compute_hash, compute_total
41
+ "calculate_", # Calculation: calculate_sum, calculate_average
42
+ )
43
+
44
+ # Default action verb names - exact method names that are excluded
45
+ # These are lifecycle hooks, display actions, and resource operations
46
+ DEFAULT_EXCLUDE_NAMES: frozenset[str] = frozenset(
47
+ {
48
+ "finalize", # Lifecycle hook
49
+ "serialize", # Transformation
50
+ "dump", # Serialization
51
+ "validate", # Validation action
52
+ "show", # Display action
53
+ "display", # Display action
54
+ "print", # Output action
55
+ "refresh", # Update action
56
+ "reset", # State action
57
+ "clear", # State action
58
+ "close", # Resource action
59
+ "open", # Resource action
60
+ "save", # Persistence action
61
+ "load", # Persistence action
62
+ "execute", # Action
63
+ "run", # Action
64
+ }
65
+ )
66
+
67
+
68
+ def _load_list_config(
69
+ config: dict[str, Any], key: str, override_key: str, default: tuple[str, ...]
70
+ ) -> tuple[str, ...]:
71
+ """Load a list config with extend/override semantics."""
72
+ if override_key in config and isinstance(config[override_key], list):
73
+ return tuple(config[override_key])
74
+ if key in config and isinstance(config[key], list):
75
+ return default + tuple(config[key])
76
+ return default
77
+
78
+
79
+ def _load_set_config(
80
+ config: dict[str, Any], key: str, override_key: str, default: frozenset[str]
81
+ ) -> frozenset[str]:
82
+ """Load a set config with extend/override semantics."""
83
+ if override_key in config and isinstance(config[override_key], list):
84
+ return frozenset(config[override_key])
85
+ if key in config and isinstance(config[key], list):
86
+ return default | frozenset(config[key])
87
+ return default
88
+
89
+
90
+ @dataclass
91
+ class MethodPropertyConfig: # thailint: ignore[dry]
92
+ """Configuration for method-should-be-property linter."""
93
+
94
+ enabled: bool = True
95
+ max_body_statements: int = 3
96
+ ignore: list[str] = field(default_factory=list)
97
+ ignore_methods: list[str] = field(default_factory=list)
98
+
99
+ # Action verb exclusions (extend defaults or override)
100
+ exclude_prefixes: tuple[str, ...] = DEFAULT_EXCLUDE_PREFIXES
101
+ exclude_names: frozenset[str] = DEFAULT_EXCLUDE_NAMES
102
+
103
+ @classmethod
104
+ def from_dict(
105
+ cls, config: dict[str, Any] | None, language: str | None = None
106
+ ) -> "MethodPropertyConfig":
107
+ """Load configuration from dictionary.
108
+
109
+ Args:
110
+ config: Dictionary containing configuration values, or None
111
+ language: Programming language (unused, for interface compatibility)
112
+
113
+ Returns:
114
+ MethodPropertyConfig instance with values from dictionary
115
+ """
116
+ if config is None:
117
+ return cls()
118
+
119
+ ignore_patterns = config.get("ignore", [])
120
+ if not isinstance(ignore_patterns, list):
121
+ ignore_patterns = []
122
+
123
+ ignore_methods = config.get("ignore_methods", [])
124
+ if not isinstance(ignore_methods, list):
125
+ ignore_methods = []
126
+
127
+ return cls(
128
+ enabled=config.get("enabled", True),
129
+ max_body_statements=config.get("max_body_statements", 3),
130
+ ignore=ignore_patterns,
131
+ ignore_methods=ignore_methods,
132
+ exclude_prefixes=_load_list_config(
133
+ config, "exclude_prefixes", "exclude_prefixes_override", DEFAULT_EXCLUDE_PREFIXES
134
+ ),
135
+ exclude_names=_load_set_config(
136
+ config, "exclude_names", "exclude_names_override", DEFAULT_EXCLUDE_NAMES
137
+ ),
138
+ )