thailint 0.11.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 (129) 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 +118 -7
  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/utils.py +29 -9
  14. src/cli_main.py +3 -0
  15. src/config.py +2 -1
  16. src/core/base.py +3 -2
  17. src/core/cli_utils.py +3 -1
  18. src/core/config_parser.py +5 -2
  19. src/core/constants.py +54 -0
  20. src/core/linter_utils.py +4 -0
  21. src/core/rule_discovery.py +5 -1
  22. src/core/violation_builder.py +3 -0
  23. src/linter_config/directive_markers.py +109 -0
  24. src/linter_config/ignore.py +225 -383
  25. src/linter_config/pattern_utils.py +65 -0
  26. src/linter_config/rule_matcher.py +89 -0
  27. src/linters/collection_pipeline/any_all_analyzer.py +281 -0
  28. src/linters/collection_pipeline/ast_utils.py +40 -0
  29. src/linters/collection_pipeline/config.py +12 -0
  30. src/linters/collection_pipeline/continue_analyzer.py +2 -8
  31. src/linters/collection_pipeline/detector.py +262 -32
  32. src/linters/collection_pipeline/filter_map_analyzer.py +402 -0
  33. src/linters/collection_pipeline/linter.py +18 -35
  34. src/linters/collection_pipeline/suggestion_builder.py +68 -1
  35. src/linters/dry/base_token_analyzer.py +16 -9
  36. src/linters/dry/block_filter.py +7 -4
  37. src/linters/dry/cache.py +7 -2
  38. src/linters/dry/config.py +7 -1
  39. src/linters/dry/constant_matcher.py +34 -25
  40. src/linters/dry/file_analyzer.py +4 -2
  41. src/linters/dry/inline_ignore.py +7 -16
  42. src/linters/dry/linter.py +48 -25
  43. src/linters/dry/python_analyzer.py +18 -10
  44. src/linters/dry/python_constant_extractor.py +51 -52
  45. src/linters/dry/single_statement_detector.py +14 -12
  46. src/linters/dry/token_hasher.py +115 -115
  47. src/linters/dry/typescript_analyzer.py +11 -6
  48. src/linters/dry/typescript_constant_extractor.py +4 -0
  49. src/linters/dry/typescript_statement_detector.py +208 -208
  50. src/linters/dry/typescript_value_extractor.py +3 -0
  51. src/linters/dry/violation_filter.py +1 -4
  52. src/linters/dry/violation_generator.py +1 -4
  53. src/linters/file_header/atemporal_detector.py +4 -0
  54. src/linters/file_header/base_parser.py +4 -0
  55. src/linters/file_header/bash_parser.py +4 -0
  56. src/linters/file_header/field_validator.py +5 -8
  57. src/linters/file_header/linter.py +19 -12
  58. src/linters/file_header/markdown_parser.py +6 -0
  59. src/linters/file_placement/config_loader.py +3 -1
  60. src/linters/file_placement/linter.py +22 -8
  61. src/linters/file_placement/pattern_matcher.py +21 -4
  62. src/linters/file_placement/pattern_validator.py +21 -7
  63. src/linters/file_placement/rule_checker.py +2 -2
  64. src/linters/lazy_ignores/__init__.py +43 -0
  65. src/linters/lazy_ignores/config.py +66 -0
  66. src/linters/lazy_ignores/directive_utils.py +121 -0
  67. src/linters/lazy_ignores/header_parser.py +177 -0
  68. src/linters/lazy_ignores/linter.py +158 -0
  69. src/linters/lazy_ignores/matcher.py +135 -0
  70. src/linters/lazy_ignores/python_analyzer.py +201 -0
  71. src/linters/lazy_ignores/rule_id_utils.py +180 -0
  72. src/linters/lazy_ignores/skip_detector.py +298 -0
  73. src/linters/lazy_ignores/types.py +67 -0
  74. src/linters/lazy_ignores/typescript_analyzer.py +146 -0
  75. src/linters/lazy_ignores/violation_builder.py +131 -0
  76. src/linters/lbyl/__init__.py +29 -0
  77. src/linters/lbyl/config.py +63 -0
  78. src/linters/lbyl/pattern_detectors/__init__.py +25 -0
  79. src/linters/lbyl/pattern_detectors/base.py +46 -0
  80. src/linters/magic_numbers/context_analyzer.py +227 -229
  81. src/linters/magic_numbers/linter.py +20 -15
  82. src/linters/magic_numbers/python_analyzer.py +4 -16
  83. src/linters/magic_numbers/typescript_analyzer.py +9 -16
  84. src/linters/method_property/config.py +4 -0
  85. src/linters/method_property/linter.py +5 -4
  86. src/linters/method_property/python_analyzer.py +5 -4
  87. src/linters/method_property/violation_builder.py +3 -0
  88. src/linters/nesting/typescript_analyzer.py +6 -12
  89. src/linters/nesting/typescript_function_extractor.py +0 -4
  90. src/linters/print_statements/linter.py +6 -4
  91. src/linters/print_statements/python_analyzer.py +85 -81
  92. src/linters/print_statements/typescript_analyzer.py +6 -15
  93. src/linters/srp/heuristics.py +4 -4
  94. src/linters/srp/linter.py +12 -12
  95. src/linters/srp/violation_builder.py +0 -4
  96. src/linters/stateless_class/linter.py +30 -36
  97. src/linters/stateless_class/python_analyzer.py +11 -20
  98. src/linters/stringly_typed/__init__.py +22 -9
  99. src/linters/stringly_typed/config.py +32 -8
  100. src/linters/stringly_typed/context_filter.py +451 -0
  101. src/linters/stringly_typed/function_call_violation_builder.py +135 -0
  102. src/linters/stringly_typed/ignore_checker.py +102 -0
  103. src/linters/stringly_typed/ignore_utils.py +51 -0
  104. src/linters/stringly_typed/linter.py +376 -0
  105. src/linters/stringly_typed/python/__init__.py +9 -5
  106. src/linters/stringly_typed/python/analyzer.py +159 -9
  107. src/linters/stringly_typed/python/call_tracker.py +175 -0
  108. src/linters/stringly_typed/python/comparison_tracker.py +257 -0
  109. src/linters/stringly_typed/python/condition_extractor.py +3 -0
  110. src/linters/stringly_typed/python/conditional_detector.py +4 -1
  111. src/linters/stringly_typed/python/match_analyzer.py +8 -2
  112. src/linters/stringly_typed/python/validation_detector.py +3 -0
  113. src/linters/stringly_typed/storage.py +630 -0
  114. src/linters/stringly_typed/storage_initializer.py +45 -0
  115. src/linters/stringly_typed/typescript/__init__.py +28 -0
  116. src/linters/stringly_typed/typescript/analyzer.py +157 -0
  117. src/linters/stringly_typed/typescript/call_tracker.py +335 -0
  118. src/linters/stringly_typed/typescript/comparison_tracker.py +378 -0
  119. src/linters/stringly_typed/violation_generator.py +405 -0
  120. src/orchestrator/core.py +13 -4
  121. src/templates/thailint_config_template.yaml +166 -0
  122. src/utils/project_root.py +3 -0
  123. thailint-0.13.0.dist-info/METADATA +184 -0
  124. thailint-0.13.0.dist-info/RECORD +189 -0
  125. thailint-0.11.0.dist-info/METADATA +0 -1661
  126. thailint-0.11.0.dist-info/RECORD +0 -150
  127. {thailint-0.11.0.dist-info → thailint-0.13.0.dist-info}/WHEEL +0 -0
  128. {thailint-0.11.0.dist-info → thailint-0.13.0.dist-info}/entry_points.txt +0 -0
  129. {thailint-0.11.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:
@@ -4,20 +4,33 @@ Purpose: Stringly-typed linter package exports
4
4
  Scope: Public API for stringly-typed linter module
5
5
 
6
6
  Overview: Provides the public interface for the stringly-typed linter package. Exports
7
- StringlyTypedConfig for configuration of the linter. The stringly-typed linter detects
8
- code patterns where plain strings are used instead of proper enums or typed alternatives,
9
- helping identify potential type safety improvements. This module serves as the entry
10
- point for users of the stringly-typed linter.
7
+ StringlyTypedConfig for configuration and StringlyTypedRule for linting. The stringly-typed
8
+ linter detects code patterns where plain strings are used instead of proper enums or typed
9
+ alternatives, helping identify potential type safety improvements. Supports cross-file
10
+ detection to find repeated string patterns across the codebase. Includes IgnoreChecker
11
+ for inline ignore directive support.
11
12
 
12
- Dependencies: .config for StringlyTypedConfig
13
+ Dependencies: .config for StringlyTypedConfig, .linter for StringlyTypedRule,
14
+ .storage for StringlyTypedStorage, .ignore_checker for IgnoreChecker
13
15
 
14
- Exports: StringlyTypedConfig dataclass
16
+ Exports: StringlyTypedConfig, StringlyTypedRule, StringlyTypedStorage, StoredPattern,
17
+ IgnoreChecker
15
18
 
16
- Interfaces: Configuration loading via StringlyTypedConfig.from_dict()
19
+ Interfaces: Configuration loading via StringlyTypedConfig.from_dict(),
20
+ StringlyTypedRule.check() and finalize() for linting, IgnoreChecker.filter_violations()
17
21
 
18
22
  Implementation: Module-level exports with __all__ definition
19
23
  """
20
24
 
21
25
  from src.linters.stringly_typed.config import StringlyTypedConfig
22
-
23
- __all__ = ["StringlyTypedConfig"]
26
+ from src.linters.stringly_typed.ignore_checker import IgnoreChecker
27
+ from src.linters.stringly_typed.linter import StringlyTypedRule
28
+ from src.linters.stringly_typed.storage import StoredPattern, StringlyTypedStorage
29
+
30
+ __all__ = [
31
+ "StringlyTypedConfig",
32
+ "IgnoreChecker",
33
+ "StringlyTypedRule",
34
+ "StringlyTypedStorage",
35
+ "StoredPattern",
36
+ ]
@@ -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
@@ -33,6 +32,23 @@ DEFAULT_MIN_OCCURRENCES = 2
33
32
  DEFAULT_MIN_VALUES_FOR_ENUM = 2
34
33
  DEFAULT_MAX_VALUES_FOR_ENUM = 6
35
34
 
35
+ # Default ignore patterns - test directories are excluded by default
36
+ # because test fixtures commonly use string literals for mocking
37
+ DEFAULT_IGNORE_PATTERNS: list[str] = [
38
+ "**/tests/**",
39
+ "**/test/**",
40
+ "**/*_test.py",
41
+ "**/*_test.ts",
42
+ "**/*.test.ts",
43
+ "**/*.test.tsx",
44
+ "**/*.spec.ts",
45
+ "**/*.spec.tsx",
46
+ "**/*.stories.ts",
47
+ "**/*.stories.tsx",
48
+ "**/conftest.py",
49
+ "**/fixtures/**",
50
+ ]
51
+
36
52
 
37
53
  @dataclass
38
54
  class StringlyTypedConfig: # pylint: disable=too-many-instance-attributes
@@ -62,7 +78,7 @@ class StringlyTypedConfig: # pylint: disable=too-many-instance-attributes
62
78
  """Whether to require cross-file occurrences to flag violations."""
63
79
 
64
80
  ignore: list[str] = field(default_factory=list)
65
- """File patterns to ignore."""
81
+ """File patterns to ignore. Defaults merged with test directories in from_dict."""
66
82
 
67
83
  allowed_string_sets: list[list[str]] = field(default_factory=list)
68
84
  """String sets that are allowed and should not be flagged."""
@@ -114,13 +130,17 @@ class StringlyTypedConfig: # pylint: disable=too-many-instance-attributes
114
130
  Returns:
115
131
  StringlyTypedConfig instance
116
132
  """
133
+ # Merge user ignore patterns with defaults
134
+ user_ignore = config.get("ignore", [])
135
+ merged_ignore = DEFAULT_IGNORE_PATTERNS.copy() + user_ignore
136
+
117
137
  return cls(
118
138
  enabled=config.get("enabled", True),
119
139
  min_occurrences=config.get("min_occurrences", DEFAULT_MIN_OCCURRENCES),
120
140
  min_values_for_enum=config.get("min_values_for_enum", DEFAULT_MIN_VALUES_FOR_ENUM),
121
141
  max_values_for_enum=config.get("max_values_for_enum", DEFAULT_MAX_VALUES_FOR_ENUM),
122
142
  require_cross_file=config.get("require_cross_file", True),
123
- ignore=config.get("ignore", []),
143
+ ignore=merged_ignore,
124
144
  allowed_string_sets=config.get("allowed_string_sets", []),
125
145
  exclude_variables=config.get("exclude_variables", []),
126
146
  )
@@ -138,6 +158,10 @@ class StringlyTypedConfig: # pylint: disable=too-many-instance-attributes
138
158
  Returns:
139
159
  StringlyTypedConfig instance with merged values
140
160
  """
161
+ # Merge user ignore patterns with defaults
162
+ user_ignore = lang_config.get("ignore", base_config.get("ignore", []))
163
+ merged_ignore = DEFAULT_IGNORE_PATTERNS.copy() + user_ignore
164
+
141
165
  return cls(
142
166
  enabled=lang_config.get("enabled", base_config.get("enabled", True)),
143
167
  min_occurrences=lang_config.get(
@@ -155,7 +179,7 @@ class StringlyTypedConfig: # pylint: disable=too-many-instance-attributes
155
179
  require_cross_file=lang_config.get(
156
180
  "require_cross_file", base_config.get("require_cross_file", True)
157
181
  ),
158
- ignore=lang_config.get("ignore", base_config.get("ignore", [])),
182
+ ignore=merged_ignore,
159
183
  allowed_string_sets=lang_config.get(
160
184
  "allowed_string_sets", base_config.get("allowed_string_sets", [])
161
185
  ),