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
@@ -20,13 +20,20 @@ Interfaces: StatelessClassRule.check(context) -> list[Violation]
20
20
 
21
21
  Implementation: Composition pattern delegating analysis to specialized analyzer with
22
22
  config loading and comprehensive ignore checking
23
+
24
+ Suppressions:
25
+ - B101: Type narrowing assertion after _should_analyze guard (can't fail)
26
+ - srp,dry: Rule class coordinates analyzer, config, and ignore checking. Method count
27
+ exceeds limit due to comprehensive 5-level ignore system support.
23
28
  """
24
29
 
25
30
  from pathlib import Path
26
31
 
27
32
  from src.core.base import BaseLintContext, BaseLintRule
33
+ from src.core.constants import HEADER_SCAN_LINES, IgnoreDirective, Language
28
34
  from src.core.types import Severity, Violation
29
35
  from src.linter_config.ignore import get_ignore_parser
36
+ from src.linter_config.rule_matcher import rule_matches
30
37
 
31
38
  from .config import StatelessClassConfig
32
39
  from .python_analyzer import ClassInfo, StatelessClassAnalyzer
@@ -67,20 +74,29 @@ class StatelessClassRule(BaseLintRule): # thailint: ignore[srp,dry]
67
74
  return []
68
75
 
69
76
  config = self._load_config(context)
70
- if not config.enabled:
71
- return []
72
-
73
- if self._is_file_ignored(context, config):
77
+ if not config.enabled or self._should_skip_file(context, config):
74
78
  return []
75
79
 
76
- if self._has_file_level_ignore(context):
77
- return []
80
+ # _should_analyze ensures file_content is set
81
+ assert context.file_content is not None # nosec B101
78
82
 
79
83
  analyzer = StatelessClassAnalyzer(min_methods=config.min_methods)
80
- stateless_classes = analyzer.analyze(context.file_content) # type: ignore[arg-type]
84
+ stateless_classes = analyzer.analyze(context.file_content)
81
85
 
82
86
  return self._filter_ignored_violations(stateless_classes, context)
83
87
 
88
+ def _should_skip_file(self, context: BaseLintContext, config: StatelessClassConfig) -> bool:
89
+ """Check if file should be skipped due to ignore patterns or directives.
90
+
91
+ Args:
92
+ context: Lint context
93
+ config: Configuration
94
+
95
+ Returns:
96
+ True if file should be skipped
97
+ """
98
+ return self._is_file_ignored(context, config) or self._has_file_level_ignore(context)
99
+
84
100
  def _should_analyze(self, context: BaseLintContext) -> bool:
85
101
  """Check if context should be analyzed.
86
102
 
@@ -90,7 +106,7 @@ class StatelessClassRule(BaseLintRule): # thailint: ignore[srp,dry]
90
106
  Returns:
91
107
  True if should analyze
92
108
  """
93
- return context.language == "python" and context.file_content is not None
109
+ return context.language == Language.PYTHON and context.file_content is not None
94
110
 
95
111
  def _load_config(self, context: BaseLintContext) -> StatelessClassConfig:
96
112
  """Load configuration from context.
@@ -129,10 +145,7 @@ class StatelessClassRule(BaseLintRule): # thailint: ignore[srp,dry]
129
145
  return False
130
146
 
131
147
  file_path = Path(context.file_path)
132
- for pattern in config.ignore:
133
- if self._matches_pattern(file_path, pattern):
134
- return True
135
- return False
148
+ return any(self._matches_pattern(file_path, pattern) for pattern in config.ignore)
136
149
 
137
150
  def _matches_pattern(self, file_path: Path, pattern: str) -> bool:
138
151
  """Check if file path matches a glob pattern.
@@ -162,12 +175,9 @@ class StatelessClassRule(BaseLintRule): # thailint: ignore[srp,dry]
162
175
  if not context.file_content:
163
176
  return False
164
177
 
165
- # Check first 10 lines for ignore-file directive
166
- lines = context.file_content.splitlines()[:10]
167
- for line in lines:
168
- if self._is_file_ignore_directive(line):
169
- return True
170
- return False
178
+ # Check first lines for ignore-file directive
179
+ lines = context.file_content.splitlines()[:HEADER_SCAN_LINES]
180
+ return any(self._is_file_ignore_directive(line) for line in lines)
171
181
 
172
182
  def _is_file_ignore_directive(self, line: str) -> bool:
173
183
  """Check if line is a file-level ignore directive.
@@ -218,23 +228,7 @@ class StatelessClassRule(BaseLintRule): # thailint: ignore[srp,dry]
218
228
  Returns:
219
229
  True if pattern matches this rule
220
230
  """
221
- rule_id_lower = self.rule_id.lower()
222
- pattern_lower = rule_pattern.lower()
223
-
224
- # Exact match
225
- if rule_id_lower == pattern_lower:
226
- return True
227
-
228
- # Prefix match: stateless-class matches stateless-class.violation
229
- if rule_id_lower.startswith(pattern_lower + "."):
230
- return True
231
-
232
- # Wildcard match: stateless-class.* matches stateless-class.violation
233
- if pattern_lower.endswith("*"):
234
- prefix = pattern_lower[:-1]
235
- return rule_id_lower.startswith(prefix)
236
-
237
- return False
231
+ return rule_matches(self.rule_id, rule_pattern)
238
232
 
239
233
  def _filter_ignored_violations(
240
234
  self, classes: list[ClassInfo], context: BaseLintContext
@@ -330,7 +324,7 @@ class StatelessClassRule(BaseLintRule): # thailint: ignore[srp,dry]
330
324
  return True
331
325
 
332
326
  # Rule-specific ignore
333
- return self._matches_rule_ignore(line, "ignore")
327
+ return self._matches_rule_ignore(line, IgnoreDirective.IGNORE)
334
328
 
335
329
  def _create_violation(self, info: ClassInfo, context: BaseLintContext) -> Violation:
336
330
  """Create violation from class info.
@@ -145,10 +145,10 @@ def _has_constructor(class_node: ast.ClassDef) -> bool:
145
145
  True if class has constructor
146
146
  """
147
147
  constructor_names = ("__init__", "__new__")
148
- for item in class_node.body:
149
- if isinstance(item, ast.FunctionDef) and item.name in constructor_names:
150
- return True
151
- return False
148
+ return any(
149
+ isinstance(item, ast.FunctionDef) and item.name in constructor_names
150
+ for item in class_node.body
151
+ )
152
152
 
153
153
 
154
154
  def _is_exception_case(class_node: ast.ClassDef) -> bool:
@@ -174,10 +174,7 @@ def _inherits_from_abc_or_protocol(class_node: ast.ClassDef) -> bool:
174
174
  Returns:
175
175
  True if inherits from ABC or Protocol
176
176
  """
177
- for base in class_node.bases:
178
- if _get_base_name(base) in ("ABC", "Protocol"):
179
- return True
180
- return False
177
+ return any(_get_base_name(base) in ("ABC", "Protocol") for base in class_node.bases)
181
178
 
182
179
 
183
180
  def _get_base_name(base: ast.expr) -> str:
@@ -205,10 +202,7 @@ def _has_class_attributes(class_node: ast.ClassDef) -> bool:
205
202
  Returns:
206
203
  True if class has class attributes
207
204
  """
208
- for item in class_node.body:
209
- if isinstance(item, (ast.Assign, ast.AnnAssign)):
210
- return True
211
- return False
205
+ return any(isinstance(item, (ast.Assign, ast.AnnAssign)) for item in class_node.body)
212
206
 
213
207
 
214
208
  def _has_instance_attributes(class_node: ast.ClassDef) -> bool:
@@ -220,10 +214,10 @@ def _has_instance_attributes(class_node: ast.ClassDef) -> bool:
220
214
  Returns:
221
215
  True if any method assigns to self
222
216
  """
223
- for item in class_node.body:
224
- if isinstance(item, ast.FunctionDef) and _method_has_self_assignment(item):
225
- return True
226
- return False
217
+ return any(
218
+ isinstance(item, ast.FunctionDef) and _method_has_self_assignment(item)
219
+ for item in class_node.body
220
+ )
227
221
 
228
222
 
229
223
  def _method_has_self_assignment(method: ast.FunctionDef) -> bool:
@@ -235,10 +229,7 @@ def _method_has_self_assignment(method: ast.FunctionDef) -> bool:
235
229
  Returns:
236
230
  True if method assigns to self
237
231
  """
238
- for node in ast.walk(method):
239
- if _is_self_attribute_assignment(node):
240
- return True
241
- return False
232
+ return any(_is_self_attribute_assignment(node) for node in ast.walk(method))
242
233
 
243
234
 
244
235
  def _is_self_attribute_assignment(node: ast.AST) -> bool:
@@ -18,11 +18,10 @@ Exports: StringlyTypedConfig dataclass, default constants
18
18
  Interfaces: StringlyTypedConfig.from_dict() class method for configuration loading
19
19
 
20
20
  Implementation: Dataclass with sensible defaults, validation in __post_init__, and config
21
- loading from dictionary with language-specific override support. Pylint
22
- too-many-instance-attributes suppressed because configuration dataclasses inherently
23
- require multiple cohesive fields (8 attributes for detection thresholds, filtering,
24
- cross-file settings). Splitting would reduce cohesion without benefit. This follows
25
- the established pattern in DRYConfig which has the same suppression.
21
+ loading from dictionary with language-specific override support
22
+
23
+ Suppressions:
24
+ - too-many-instance-attributes: Configuration dataclass with cohesive detection settings
26
25
  """
27
26
 
28
27
  from dataclasses import dataclass, field