thailint 0.2.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 (214) 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 +44 -27
  23. src/core/base.py +95 -5
  24. src/core/cli_utils.py +19 -2
  25. src/core/config_parser.py +36 -6
  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 +125 -22
  65. src/linters/dry/block_grouper.py +4 -0
  66. src/linters/dry/cache.py +142 -94
  67. src/linters/dry/cache_query.py +4 -0
  68. src/linters/dry/config.py +68 -21
  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 +20 -82
  73. src/linters/dry/file_analyzer.py +15 -50
  74. src/linters/dry/inline_ignore.py +7 -16
  75. src/linters/dry/linter.py +182 -54
  76. src/linters/dry/python_analyzer.py +108 -336
  77. src/linters/dry/python_constant_extractor.py +100 -0
  78. src/linters/dry/single_statement_detector.py +417 -0
  79. src/linters/dry/storage_initializer.py +9 -18
  80. src/linters/dry/token_hasher.py +129 -71
  81. src/linters/dry/typescript_analyzer.py +68 -380
  82. src/linters/dry/typescript_constant_extractor.py +138 -0
  83. src/linters/dry/typescript_statement_detector.py +255 -0
  84. src/linters/dry/typescript_value_extractor.py +70 -0
  85. src/linters/dry/violation_builder.py +4 -0
  86. src/linters/dry/violation_filter.py +9 -5
  87. src/linters/dry/violation_generator.py +71 -14
  88. src/linters/file_header/__init__.py +24 -0
  89. src/linters/file_header/atemporal_detector.py +105 -0
  90. src/linters/file_header/base_parser.py +93 -0
  91. src/linters/file_header/bash_parser.py +66 -0
  92. src/linters/file_header/config.py +140 -0
  93. src/linters/file_header/css_parser.py +70 -0
  94. src/linters/file_header/field_validator.py +72 -0
  95. src/linters/file_header/linter.py +309 -0
  96. src/linters/file_header/markdown_parser.py +130 -0
  97. src/linters/file_header/python_parser.py +42 -0
  98. src/linters/file_header/typescript_parser.py +73 -0
  99. src/linters/file_header/violation_builder.py +79 -0
  100. src/linters/file_placement/config_loader.py +3 -1
  101. src/linters/file_placement/directory_matcher.py +4 -0
  102. src/linters/file_placement/linter.py +74 -31
  103. src/linters/file_placement/pattern_matcher.py +41 -6
  104. src/linters/file_placement/pattern_validator.py +31 -12
  105. src/linters/file_placement/rule_checker.py +12 -7
  106. src/linters/lazy_ignores/__init__.py +43 -0
  107. src/linters/lazy_ignores/config.py +74 -0
  108. src/linters/lazy_ignores/directive_utils.py +164 -0
  109. src/linters/lazy_ignores/header_parser.py +177 -0
  110. src/linters/lazy_ignores/linter.py +158 -0
  111. src/linters/lazy_ignores/matcher.py +168 -0
  112. src/linters/lazy_ignores/python_analyzer.py +209 -0
  113. src/linters/lazy_ignores/rule_id_utils.py +180 -0
  114. src/linters/lazy_ignores/skip_detector.py +298 -0
  115. src/linters/lazy_ignores/types.py +71 -0
  116. src/linters/lazy_ignores/typescript_analyzer.py +146 -0
  117. src/linters/lazy_ignores/violation_builder.py +135 -0
  118. src/linters/lbyl/__init__.py +31 -0
  119. src/linters/lbyl/config.py +63 -0
  120. src/linters/lbyl/linter.py +67 -0
  121. src/linters/lbyl/pattern_detectors/__init__.py +53 -0
  122. src/linters/lbyl/pattern_detectors/base.py +63 -0
  123. src/linters/lbyl/pattern_detectors/dict_key_detector.py +107 -0
  124. src/linters/lbyl/pattern_detectors/division_check_detector.py +232 -0
  125. src/linters/lbyl/pattern_detectors/file_exists_detector.py +220 -0
  126. src/linters/lbyl/pattern_detectors/hasattr_detector.py +119 -0
  127. src/linters/lbyl/pattern_detectors/isinstance_detector.py +119 -0
  128. src/linters/lbyl/pattern_detectors/len_check_detector.py +173 -0
  129. src/linters/lbyl/pattern_detectors/none_check_detector.py +146 -0
  130. src/linters/lbyl/pattern_detectors/string_validator_detector.py +145 -0
  131. src/linters/lbyl/python_analyzer.py +215 -0
  132. src/linters/lbyl/violation_builder.py +354 -0
  133. src/linters/magic_numbers/__init__.py +48 -0
  134. src/linters/magic_numbers/config.py +82 -0
  135. src/linters/magic_numbers/context_analyzer.py +249 -0
  136. src/linters/magic_numbers/linter.py +462 -0
  137. src/linters/magic_numbers/python_analyzer.py +64 -0
  138. src/linters/magic_numbers/typescript_analyzer.py +215 -0
  139. src/linters/magic_numbers/typescript_ignore_checker.py +81 -0
  140. src/linters/magic_numbers/violation_builder.py +98 -0
  141. src/linters/method_property/__init__.py +49 -0
  142. src/linters/method_property/config.py +138 -0
  143. src/linters/method_property/linter.py +414 -0
  144. src/linters/method_property/python_analyzer.py +473 -0
  145. src/linters/method_property/violation_builder.py +119 -0
  146. src/linters/nesting/__init__.py +6 -2
  147. src/linters/nesting/config.py +6 -3
  148. src/linters/nesting/linter.py +31 -34
  149. src/linters/nesting/python_analyzer.py +4 -0
  150. src/linters/nesting/typescript_analyzer.py +6 -11
  151. src/linters/nesting/violation_builder.py +1 -0
  152. src/linters/performance/__init__.py +91 -0
  153. src/linters/performance/config.py +43 -0
  154. src/linters/performance/constants.py +49 -0
  155. src/linters/performance/linter.py +149 -0
  156. src/linters/performance/python_analyzer.py +365 -0
  157. src/linters/performance/regex_analyzer.py +312 -0
  158. src/linters/performance/regex_linter.py +139 -0
  159. src/linters/performance/typescript_analyzer.py +236 -0
  160. src/linters/performance/violation_builder.py +160 -0
  161. src/linters/print_statements/__init__.py +53 -0
  162. src/linters/print_statements/config.py +78 -0
  163. src/linters/print_statements/linter.py +413 -0
  164. src/linters/print_statements/python_analyzer.py +153 -0
  165. src/linters/print_statements/typescript_analyzer.py +125 -0
  166. src/linters/print_statements/violation_builder.py +96 -0
  167. src/linters/srp/__init__.py +3 -3
  168. src/linters/srp/class_analyzer.py +11 -7
  169. src/linters/srp/config.py +12 -6
  170. src/linters/srp/heuristics.py +56 -22
  171. src/linters/srp/linter.py +47 -39
  172. src/linters/srp/python_analyzer.py +55 -20
  173. src/linters/srp/typescript_metrics_calculator.py +110 -50
  174. src/linters/stateless_class/__init__.py +25 -0
  175. src/linters/stateless_class/config.py +58 -0
  176. src/linters/stateless_class/linter.py +349 -0
  177. src/linters/stateless_class/python_analyzer.py +290 -0
  178. src/linters/stringly_typed/__init__.py +36 -0
  179. src/linters/stringly_typed/config.py +189 -0
  180. src/linters/stringly_typed/context_filter.py +451 -0
  181. src/linters/stringly_typed/function_call_violation_builder.py +135 -0
  182. src/linters/stringly_typed/ignore_checker.py +100 -0
  183. src/linters/stringly_typed/ignore_utils.py +51 -0
  184. src/linters/stringly_typed/linter.py +376 -0
  185. src/linters/stringly_typed/python/__init__.py +33 -0
  186. src/linters/stringly_typed/python/analyzer.py +348 -0
  187. src/linters/stringly_typed/python/call_tracker.py +175 -0
  188. src/linters/stringly_typed/python/comparison_tracker.py +257 -0
  189. src/linters/stringly_typed/python/condition_extractor.py +134 -0
  190. src/linters/stringly_typed/python/conditional_detector.py +179 -0
  191. src/linters/stringly_typed/python/constants.py +21 -0
  192. src/linters/stringly_typed/python/match_analyzer.py +94 -0
  193. src/linters/stringly_typed/python/validation_detector.py +189 -0
  194. src/linters/stringly_typed/python/variable_extractor.py +96 -0
  195. src/linters/stringly_typed/storage.py +620 -0
  196. src/linters/stringly_typed/storage_initializer.py +45 -0
  197. src/linters/stringly_typed/typescript/__init__.py +28 -0
  198. src/linters/stringly_typed/typescript/analyzer.py +157 -0
  199. src/linters/stringly_typed/typescript/call_tracker.py +335 -0
  200. src/linters/stringly_typed/typescript/comparison_tracker.py +378 -0
  201. src/linters/stringly_typed/violation_generator.py +419 -0
  202. src/orchestrator/core.py +264 -16
  203. src/orchestrator/language_detector.py +5 -3
  204. src/templates/thailint_config_template.yaml +354 -0
  205. src/utils/project_root.py +138 -16
  206. thailint-0.15.3.dist-info/METADATA +187 -0
  207. thailint-0.15.3.dist-info/RECORD +226 -0
  208. {thailint-0.2.0.dist-info → thailint-0.15.3.dist-info}/WHEEL +1 -1
  209. thailint-0.15.3.dist-info/entry_points.txt +4 -0
  210. src/cli.py +0 -1055
  211. thailint-0.2.0.dist-info/METADATA +0 -980
  212. thailint-0.2.0.dist-info/RECORD +0 -75
  213. thailint-0.2.0.dist-info/entry_points.txt +0 -4
  214. {thailint-0.2.0.dist-info → thailint-0.15.3.dist-info/licenses}/LICENSE +0 -0
src/core/base.py CHANGED
@@ -8,14 +8,17 @@ Overview: Establishes the contract that all linting plugins must follow through
8
8
  Defines BaseLintRule which all concrete linting rules inherit from, specifying required
9
9
  properties (rule_id, rule_name, description) and the check() method for violation detection.
10
10
  Provides BaseLintContext as the interface for accessing file information during analysis,
11
- exposing file_path, file_content, and language properties. These abstractions enable the
12
- rule registry to discover and instantiate rules dynamically without tight coupling, supporting
13
- the extensible plugin system where new rules can be added by simply placing them in the
14
- appropriate directory structure.
11
+ exposing file_path, file_content, and language properties. Includes MultiLanguageLintRule
12
+ intermediate class implementing template method pattern for language dispatch, eliminating
13
+ code duplication across multi-language linters (nesting, srp, magic_numbers). These
14
+ abstractions enable the rule registry to discover and instantiate rules dynamically without
15
+ tight coupling, supporting the extensible plugin system where new rules can be added by
16
+ simply placing them in the appropriate directory structure.
15
17
 
16
18
  Dependencies: abc for abstract base class support, pathlib for Path types, Violation from types
17
19
 
18
- Exports: BaseLintRule (abstract rule interface), BaseLintContext (abstract context interface)
20
+ Exports: BaseLintRule (abstract rule interface), BaseLintContext (abstract context interface),
21
+ MultiLanguageLintRule (template method base for multi-language linters)
19
22
 
20
23
  Interfaces: BaseLintRule.check(context) -> list[Violation], BaseLintContext properties
21
24
  (file_path, file_content, language), all abstract methods must be implemented by subclasses
@@ -26,7 +29,9 @@ Implementation: ABC-based interface definitions with @abstractmethod decorators,
26
29
 
27
30
  from abc import ABC, abstractmethod
28
31
  from pathlib import Path
32
+ from typing import Any
29
33
 
34
+ from .constants import Language
30
35
  from .types import Violation
31
36
 
32
37
 
@@ -132,3 +137,88 @@ class BaseLintRule(ABC):
132
137
  List of violations found during finalization. Empty list by default.
133
138
  """
134
139
  return []
140
+
141
+
142
+ class MultiLanguageLintRule(BaseLintRule):
143
+ """Base class for linting rules that support multiple programming languages.
144
+
145
+ Provides language dispatch pattern to eliminate code duplication across multi-language
146
+ linters. Subclasses implement language-specific checking methods rather than handling
147
+ dispatch logic themselves.
148
+
149
+ Subclasses must implement:
150
+ - _check_python(context, config) for Python language support
151
+ - _check_typescript(context, config) for TypeScript/JavaScript support
152
+ - _load_config(context) for configuration loading
153
+ """
154
+
155
+ def __init__(self) -> None:
156
+ """Initialize the multi-language lint rule."""
157
+ pass # Base class for multi-language linters
158
+
159
+ def check(self, context: BaseLintContext) -> list[Violation]:
160
+ """Check for violations with automatic language dispatch.
161
+
162
+ Dispatches to language-specific checking methods based on context.language.
163
+ Handles common patterns like file content validation and config loading.
164
+
165
+ Args:
166
+ context: Lint context with file information
167
+
168
+ Returns:
169
+ List of violations found
170
+ """
171
+ from .linter_utils import has_file_content
172
+
173
+ if not has_file_content(context):
174
+ return []
175
+
176
+ config = self._load_config(context)
177
+ if not config.enabled:
178
+ return []
179
+
180
+ if context.language == Language.PYTHON:
181
+ return self._check_python(context, config)
182
+
183
+ if context.language in (Language.TYPESCRIPT, Language.JAVASCRIPT):
184
+ return self._check_typescript(context, config)
185
+
186
+ return []
187
+
188
+ @abstractmethod
189
+ def _load_config(self, context: BaseLintContext) -> Any:
190
+ """Load configuration from context.
191
+
192
+ Args:
193
+ context: Lint context
194
+
195
+ Returns:
196
+ Configuration object with at minimum an 'enabled' attribute
197
+ """
198
+ raise NotImplementedError("Subclasses must implement _load_config")
199
+
200
+ @abstractmethod
201
+ def _check_python(self, context: BaseLintContext, config: Any) -> list[Violation]:
202
+ """Check Python code for violations.
203
+
204
+ Args:
205
+ context: Lint context with Python file information
206
+ config: Loaded configuration
207
+
208
+ Returns:
209
+ List of violations found in Python code
210
+ """
211
+ raise NotImplementedError("Subclasses must implement _check_python")
212
+
213
+ @abstractmethod
214
+ def _check_typescript(self, context: BaseLintContext, config: Any) -> list[Violation]:
215
+ """Check TypeScript/JavaScript code for violations.
216
+
217
+ Args:
218
+ context: Lint context with TypeScript/JavaScript file information
219
+ config: Loaded configuration
220
+
221
+ Returns:
222
+ List of violations found in TypeScript/JavaScript code
223
+ """
224
+ raise NotImplementedError("Subclasses must implement _check_typescript")
src/core/cli_utils.py CHANGED
@@ -26,6 +26,8 @@ from typing import Any
26
26
 
27
27
  import click
28
28
 
29
+ from src.core.constants import CONFIG_EXTENSIONS
30
+
29
31
 
30
32
  def common_linter_options(func: Callable) -> Callable:
31
33
  """Add common linter CLI options to command.
@@ -103,7 +105,7 @@ def _load_config_by_format(config_file: Path) -> dict[str, Any]:
103
105
  Returns:
104
106
  Loaded configuration dictionary
105
107
  """
106
- if config_file.suffix in {".yaml", ".yml"}:
108
+ if config_file.suffix in CONFIG_EXTENSIONS:
107
109
  return _load_yaml_config(config_file)
108
110
  if config_file.suffix == ".json":
109
111
  return _load_json_config(config_file)
@@ -146,10 +148,12 @@ def format_violations(violations: list, output_format: str) -> None:
146
148
 
147
149
  Args:
148
150
  violations: List of violation objects with rule_id, file_path, line, column, message, severity
149
- output_format: Output format ("text" or "json")
151
+ output_format: Output format ("text", "json", or "sarif")
150
152
  """
151
153
  if output_format == "json":
152
154
  _output_json(violations)
155
+ elif output_format == "sarif":
156
+ _output_sarif(violations)
153
157
  else:
154
158
  _output_text(violations)
155
159
 
@@ -177,6 +181,19 @@ def _output_json(violations: list) -> None:
177
181
  click.echo(json.dumps(output, indent=2))
178
182
 
179
183
 
184
+ def _output_sarif(violations: list) -> None:
185
+ """Output violations in SARIF v2.1.0 format.
186
+
187
+ Args:
188
+ violations: List of violation objects
189
+ """
190
+ from src.formatters.sarif import SarifFormatter
191
+
192
+ formatter = SarifFormatter()
193
+ sarif_doc = formatter.format(violations)
194
+ click.echo(json.dumps(sarif_doc, indent=2))
195
+
196
+
180
197
  def _output_text(violations: list) -> None:
181
198
  """Output violations in human-readable text format.
182
199
 
src/core/config_parser.py CHANGED
@@ -27,6 +27,8 @@ from typing import Any, TextIO
27
27
 
28
28
  import yaml
29
29
 
30
+ from src.core.constants import CONFIG_EXTENSIONS
31
+
30
32
 
31
33
  class ConfigParseError(Exception):
32
34
  """Configuration file parsing errors."""
@@ -72,28 +74,56 @@ def parse_json(file_obj: TextIO, path: Path) -> dict[str, Any]:
72
74
  raise ConfigParseError(f"Invalid JSON in {path}: {e}") from e
73
75
 
74
76
 
77
+ def _normalize_config_keys(config: dict[str, Any]) -> dict[str, Any]:
78
+ """Normalize configuration keys from hyphens to underscores.
79
+
80
+ Converts top-level keys like "magic-numbers" to "magic_numbers" to match
81
+ internal linter expectations while maintaining backward compatibility with
82
+ both formats in config files.
83
+
84
+ Args:
85
+ config: Configuration dictionary with potentially hyphenated keys
86
+
87
+ Returns:
88
+ Configuration dictionary with normalized (underscored) keys
89
+ """
90
+ normalized = {}
91
+ for key, value in config.items():
92
+ # Replace hyphens with underscores in keys
93
+ normalized_key = key.replace("-", "_")
94
+ normalized[normalized_key] = value
95
+ return normalized
96
+
97
+
75
98
  def parse_config_file(path: Path, encoding: str = "utf-8") -> dict[str, Any]:
76
99
  """Parse configuration file based on extension.
77
100
 
78
101
  Supports .yaml, .yml, and .json formats. Automatically detects format
79
- from file extension and uses appropriate parser.
102
+ from file extension and uses appropriate parser. Normalizes hyphenated
103
+ keys (e.g., "magic-numbers") to underscored keys (e.g., "magic_numbers")
104
+ for internal consistency.
80
105
 
81
106
  Args:
82
107
  path: Path to configuration file.
83
108
  encoding: File encoding (default: utf-8).
84
109
 
85
110
  Returns:
86
- Parsed configuration dictionary.
111
+ Parsed configuration dictionary with normalized keys.
87
112
 
88
113
  Raises:
89
114
  ConfigParseError: If file format is unsupported or parsing fails.
90
115
  """
91
116
  suffix = path.suffix.lower()
92
117
 
93
- if suffix not in [".yaml", ".yml", ".json"]:
118
+ valid_suffixes = (*CONFIG_EXTENSIONS, ".json")
119
+ if suffix not in valid_suffixes:
94
120
  raise ConfigParseError(f"Unsupported config format: {suffix}")
95
121
 
96
122
  with path.open(encoding=encoding) as f:
97
- if suffix in [".yaml", ".yml"]:
98
- return parse_yaml(f, path)
99
- return parse_json(f, path)
123
+ if suffix in CONFIG_EXTENSIONS:
124
+ config = parse_yaml(f, path)
125
+ else:
126
+ config = parse_json(f, path)
127
+
128
+ # Normalize keys from hyphens to underscores
129
+ return _normalize_config_keys(config)
src/core/constants.py ADDED
@@ -0,0 +1,54 @@
1
+ """
2
+ Purpose: Core constants and enums used across the thai-lint codebase
3
+
4
+ Scope: Centralized definitions for language names, storage modes, config extensions
5
+
6
+ Overview: Provides type-safe enums and constants for consistent stringly-typed patterns
7
+ across the codebase. Includes Language enum for programming language detection,
8
+ StorageMode for cache storage options, and CONFIG_EXTENSIONS for config file
9
+ discovery. Using enums ensures compile-time safety and IDE autocompletion.
10
+
11
+ Dependencies: enum module
12
+
13
+ Exports: Language enum, StorageMode enum, CONFIG_EXTENSIONS, IgnoreDirective enum,
14
+ HEADER_SCAN_LINES, MAX_ATTRIBUTE_CHAIN_DEPTH
15
+
16
+ Interfaces: Use enum values instead of string literals throughout codebase
17
+
18
+ Implementation: Standard Python enums with string values for compatibility
19
+ """
20
+
21
+ from enum import Enum
22
+
23
+
24
+ class Language(str, Enum):
25
+ """Supported programming languages for linting."""
26
+
27
+ PYTHON = "python"
28
+ TYPESCRIPT = "typescript"
29
+ JAVASCRIPT = "javascript"
30
+ MARKDOWN = "markdown"
31
+
32
+
33
+ class StorageMode(str, Enum):
34
+ """Storage modes for DRY linter cache."""
35
+
36
+ MEMORY = "memory"
37
+ TEMPFILE = "tempfile"
38
+
39
+
40
+ class IgnoreDirective(str, Enum):
41
+ """Inline ignore directive types."""
42
+
43
+ IGNORE = "ignore"
44
+ IGNORE_FILE = "ignore-file"
45
+
46
+
47
+ # Valid config file extensions
48
+ CONFIG_EXTENSIONS: tuple[str, str] = (".yaml", ".yml")
49
+
50
+ # Number of lines to scan at file start for ignore directives and headers
51
+ HEADER_SCAN_LINES: int = 10
52
+
53
+ # Maximum depth for attribute chain traversal (e.g., obj.attr.attr2.attr3)
54
+ MAX_ATTRIBUTE_CHAIN_DEPTH: int = 3
src/core/linter_utils.py CHANGED
@@ -1,26 +1,48 @@
1
1
  """
2
2
  Purpose: Shared utility functions for linter framework patterns
3
3
 
4
- Scope: Common config loading, metadata access, and context validation utilities for all linters
4
+ Scope: Common config loading, metadata access, context validation, and AST parsing utilities
5
5
 
6
6
  Overview: Provides reusable helper functions to eliminate duplication across linter implementations.
7
7
  Includes utilities for loading configuration from context metadata with language-specific overrides,
8
- extracting metadata fields safely with type validation, and validating context state. Standardizes
9
- common patterns used by srp, nesting, dry, and file_placement linters. Reduces boilerplate code
10
- while maintaining type safety and proper error handling.
8
+ extracting metadata fields safely with type validation, validating context state, and parsing
9
+ Python AST with syntax error handling. Standardizes common patterns used by srp, nesting, dry,
10
+ performance, and file_placement linters. Reduces boilerplate code while maintaining type safety
11
+ and proper error handling.
11
12
 
12
- Dependencies: BaseLintContext from src.core.base
13
+ Dependencies: BaseLintContext from src.core.base, ast for Python parsing
13
14
 
14
- Exports: get_metadata, get_metadata_value, load_linter_config, has_file_content
15
+ Exports: get_metadata, get_metadata_value, load_linter_config, has_file_content, parse_python_ast,
16
+ with_parsed_python
15
17
 
16
18
  Interfaces: All functions take BaseLintContext and return typed values (dict, str, bool, Any)
17
19
 
18
20
  Implementation: Type-safe metadata access with fallbacks, generic config loading with language support
21
+
22
+ Suppressions:
23
+ - invalid-name: T type variable follows Python generic naming convention
24
+ - type:ignore[return-value]: Generic config factory with runtime type checking
25
+ - unnecessary-ellipsis: Protocol method bodies use ellipsis per PEP 544
26
+ - B101: Assert used to narrow type after parse_python_ast returns non-None tree
19
27
  """
20
28
 
29
+ import ast
30
+ from collections.abc import Callable
21
31
  from typing import Any, Protocol, TypeVar
22
32
 
23
33
  from src.core.base import BaseLintContext
34
+ from src.core.types import Violation
35
+
36
+
37
+ # Protocol for violation builders that support syntax error handling
38
+ class SyntaxErrorViolationBuilder(Protocol):
39
+ """Protocol for violation builders that can create syntax error violations."""
40
+
41
+ def create_syntax_error_violation(
42
+ self, error: SyntaxError, context: BaseLintContext
43
+ ) -> Violation:
44
+ """Create a violation for a syntax error."""
45
+ ... # pylint: disable=unnecessary-ellipsis
24
46
 
25
47
 
26
48
  # Protocol for config classes that support from_dict
@@ -166,3 +188,70 @@ def should_process_file(context: BaseLintContext) -> bool:
166
188
  True if file has both content and path available
167
189
  """
168
190
  return has_file_content(context) and has_file_path(context)
191
+
192
+
193
+ def parse_python_ast(
194
+ context: BaseLintContext,
195
+ violation_builder: SyntaxErrorViolationBuilder,
196
+ ) -> tuple[ast.Module | None, list[Violation]]:
197
+ """Parse Python AST from context, handling syntax errors gracefully.
198
+
199
+ Provides a standard pattern for Python linters to parse AST and handle
200
+ syntax errors by returning a violation instead of crashing.
201
+
202
+ Args:
203
+ context: Lint context containing file content
204
+ violation_builder: Builder to create syntax error violations
205
+
206
+ Returns:
207
+ Tuple of (ast_tree, violations):
208
+ - On success: (ast.Module, [])
209
+ - On syntax error: (None, [syntax_error_violation])
210
+
211
+ Example:
212
+ tree, errors = parse_python_ast(context, self._violation_builder)
213
+ if errors:
214
+ return errors
215
+ # ... use tree for analysis
216
+ """
217
+ try:
218
+ tree = ast.parse(context.file_content or "")
219
+ return tree, []
220
+ except SyntaxError as e:
221
+ violation = violation_builder.create_syntax_error_violation(e, context)
222
+ return None, [violation]
223
+
224
+
225
+ def with_parsed_python(
226
+ context: BaseLintContext,
227
+ violation_builder: SyntaxErrorViolationBuilder,
228
+ on_success: Callable[[ast.Module], list[Violation]],
229
+ ) -> list[Violation]:
230
+ """Parse Python and call on_success with the AST, or return parse errors.
231
+
232
+ Eliminates the repeated parse-check-assert pattern across Python linters.
233
+ On parse success, calls on_success with a guaranteed non-None AST tree.
234
+ On parse failure, returns syntax error violations.
235
+
236
+ Args:
237
+ context: Lint context containing file content
238
+ violation_builder: Builder to create syntax error violations
239
+ on_success: Callback receiving the parsed AST tree, returns violations
240
+
241
+ Returns:
242
+ Violations from on_success callback, or syntax error violations
243
+
244
+ Example:
245
+ def _check_python(self, context, config):
246
+ return with_parsed_python(
247
+ context,
248
+ self._violation_builder,
249
+ lambda tree: self._analyze_tree(tree, config, context),
250
+ )
251
+ """
252
+ tree, errors = parse_python_ast(context, violation_builder)
253
+ if errors:
254
+ return errors
255
+ # tree is guaranteed non-None when errors is empty (parse_python_ast contract)
256
+ assert tree is not None # nosec B101
257
+ return on_success(tree)
@@ -0,0 +1,101 @@
1
+ """
2
+ Purpose: Base class for Python-only linters with common boilerplate
3
+
4
+ Scope: Shared infrastructure for Python-only lint rules
5
+
6
+ Overview: Provides PythonOnlyLintRule abstract base class that handles common boilerplate
7
+ for Python-only linters. Subclasses implement the abstract properties and analysis
8
+ method while the base class handles language checking, config loading, and enabled
9
+ checking. This eliminates duplicate code across Python-only linters like CQS and LBYL.
10
+
11
+ Dependencies: BaseLintRule, BaseLintContext, Language, load_linter_config, has_file_content
12
+
13
+ Exports: PythonOnlyLintRule
14
+
15
+ Interfaces: Subclasses implement _config_key, _config_class, _analyze, and rule metadata
16
+
17
+ Implementation: Template method pattern for Python linter boilerplate
18
+ """
19
+
20
+ from abc import abstractmethod
21
+ from typing import Any, Generic
22
+
23
+ from .base import BaseLintContext, BaseLintRule
24
+ from .constants import Language
25
+ from .linter_utils import ConfigType, has_file_content, load_linter_config
26
+ from .types import Violation
27
+
28
+
29
+ class PythonOnlyLintRule(BaseLintRule, Generic[ConfigType]):
30
+ """Base class for Python-only linters with common boilerplate.
31
+
32
+ Handles language checking, config loading, and enabled checking.
33
+ Subclasses provide the config key, config class, and analysis logic.
34
+ """
35
+
36
+ def __init__(self, config: ConfigType | None = None) -> None:
37
+ """Initialize with optional config override.
38
+
39
+ Args:
40
+ config: Optional configuration override for testing
41
+ """
42
+ self._config_override = config
43
+
44
+ @property
45
+ @abstractmethod
46
+ def _config_key(self) -> str:
47
+ """Configuration key in metadata (e.g., 'cqs', 'lbyl')."""
48
+ raise NotImplementedError
49
+
50
+ @property
51
+ @abstractmethod
52
+ def _config_class(self) -> type[ConfigType]:
53
+ """Configuration class type."""
54
+ raise NotImplementedError
55
+
56
+ @abstractmethod
57
+ def _analyze(self, code: str, file_path: str, config: ConfigType) -> list[Violation]:
58
+ """Perform linter-specific analysis.
59
+
60
+ Args:
61
+ code: Python source code
62
+ file_path: Path to the file
63
+ config: Loaded configuration
64
+
65
+ Returns:
66
+ List of violations found
67
+ """
68
+ raise NotImplementedError
69
+
70
+ def check(self, context: BaseLintContext) -> list[Violation]:
71
+ """Check for violations in the given context.
72
+
73
+ Args:
74
+ context: The lint context containing file information.
75
+
76
+ Returns:
77
+ List of violations found.
78
+ """
79
+ if not self._should_analyze(context):
80
+ return []
81
+
82
+ config = self._get_config(context)
83
+ if not self._is_enabled(config):
84
+ return []
85
+
86
+ file_path = str(context.file_path) if context.file_path else "unknown"
87
+ return self._analyze(context.file_content or "", file_path, config)
88
+
89
+ def _should_analyze(self, context: BaseLintContext) -> bool:
90
+ """Check if context should be analyzed."""
91
+ return context.language == Language.PYTHON and has_file_content(context)
92
+
93
+ def _get_config(self, context: BaseLintContext) -> ConfigType:
94
+ """Get configuration, using override if provided."""
95
+ if self._config_override is not None:
96
+ return self._config_override
97
+ return load_linter_config(context, self._config_key, self._config_class)
98
+
99
+ def _is_enabled(self, config: Any) -> bool:
100
+ """Check if linter is enabled in config."""
101
+ return getattr(config, "enabled", True)
src/core/registry.py CHANGED
@@ -6,7 +6,7 @@ Scope: Dynamic rule management and discovery across all linter plugin packages
6
6
  Overview: Implements rule registry that maintains a collection of registered linting rules indexed
7
7
  by rule_id. Provides methods to register individual rules, retrieve rules by identifier, list
8
8
  all available rules, and discover rules from packages using the RuleDiscovery helper. Enables
9
- the extensible plugin architecture by allowing rules to be added dynamically without framework
9
+ the extensible plugin architecture by allowing dynamic rule registration without framework
10
10
  modifications. Validates rule uniqueness and handles registration errors gracefully.
11
11
 
12
12
  Dependencies: BaseLintRule, RuleDiscovery