thailint 0.5.0__py3-none-any.whl → 0.15.3__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (204) hide show
  1. src/__init__.py +1 -0
  2. src/analyzers/__init__.py +4 -3
  3. src/analyzers/ast_utils.py +54 -0
  4. src/analyzers/rust_base.py +155 -0
  5. src/analyzers/rust_context.py +141 -0
  6. src/analyzers/typescript_base.py +4 -0
  7. src/cli/__init__.py +30 -0
  8. src/cli/__main__.py +22 -0
  9. src/cli/config.py +480 -0
  10. src/cli/config_merge.py +241 -0
  11. src/cli/linters/__init__.py +67 -0
  12. src/cli/linters/code_patterns.py +270 -0
  13. src/cli/linters/code_smells.py +342 -0
  14. src/cli/linters/documentation.py +83 -0
  15. src/cli/linters/performance.py +287 -0
  16. src/cli/linters/shared.py +331 -0
  17. src/cli/linters/structure.py +327 -0
  18. src/cli/linters/structure_quality.py +328 -0
  19. src/cli/main.py +120 -0
  20. src/cli/utils.py +395 -0
  21. src/cli_main.py +37 -0
  22. src/config.py +38 -25
  23. src/core/base.py +7 -2
  24. src/core/cli_utils.py +19 -2
  25. src/core/config_parser.py +5 -2
  26. src/core/constants.py +54 -0
  27. src/core/linter_utils.py +95 -6
  28. src/core/python_lint_rule.py +101 -0
  29. src/core/registry.py +1 -1
  30. src/core/rule_discovery.py +147 -84
  31. src/core/types.py +13 -0
  32. src/core/violation_builder.py +78 -15
  33. src/core/violation_utils.py +69 -0
  34. src/formatters/__init__.py +22 -0
  35. src/formatters/sarif.py +202 -0
  36. src/linter_config/directive_markers.py +109 -0
  37. src/linter_config/ignore.py +254 -395
  38. src/linter_config/loader.py +45 -12
  39. src/linter_config/pattern_utils.py +65 -0
  40. src/linter_config/rule_matcher.py +89 -0
  41. src/linters/collection_pipeline/__init__.py +90 -0
  42. src/linters/collection_pipeline/any_all_analyzer.py +281 -0
  43. src/linters/collection_pipeline/ast_utils.py +40 -0
  44. src/linters/collection_pipeline/config.py +75 -0
  45. src/linters/collection_pipeline/continue_analyzer.py +94 -0
  46. src/linters/collection_pipeline/detector.py +360 -0
  47. src/linters/collection_pipeline/filter_map_analyzer.py +402 -0
  48. src/linters/collection_pipeline/linter.py +420 -0
  49. src/linters/collection_pipeline/suggestion_builder.py +130 -0
  50. src/linters/cqs/__init__.py +54 -0
  51. src/linters/cqs/config.py +55 -0
  52. src/linters/cqs/function_analyzer.py +201 -0
  53. src/linters/cqs/input_detector.py +139 -0
  54. src/linters/cqs/linter.py +159 -0
  55. src/linters/cqs/output_detector.py +84 -0
  56. src/linters/cqs/python_analyzer.py +54 -0
  57. src/linters/cqs/types.py +82 -0
  58. src/linters/cqs/typescript_cqs_analyzer.py +61 -0
  59. src/linters/cqs/typescript_function_analyzer.py +192 -0
  60. src/linters/cqs/typescript_input_detector.py +203 -0
  61. src/linters/cqs/typescript_output_detector.py +117 -0
  62. src/linters/cqs/violation_builder.py +94 -0
  63. src/linters/dry/base_token_analyzer.py +16 -9
  64. src/linters/dry/block_filter.py +120 -20
  65. src/linters/dry/block_grouper.py +4 -0
  66. src/linters/dry/cache.py +104 -10
  67. src/linters/dry/cache_query.py +4 -0
  68. src/linters/dry/config.py +54 -11
  69. src/linters/dry/constant.py +92 -0
  70. src/linters/dry/constant_matcher.py +223 -0
  71. src/linters/dry/constant_violation_builder.py +98 -0
  72. src/linters/dry/duplicate_storage.py +5 -4
  73. src/linters/dry/file_analyzer.py +4 -2
  74. src/linters/dry/inline_ignore.py +7 -16
  75. src/linters/dry/linter.py +183 -48
  76. src/linters/dry/python_analyzer.py +60 -439
  77. src/linters/dry/python_constant_extractor.py +100 -0
  78. src/linters/dry/single_statement_detector.py +417 -0
  79. src/linters/dry/token_hasher.py +116 -112
  80. src/linters/dry/typescript_analyzer.py +68 -382
  81. src/linters/dry/typescript_constant_extractor.py +138 -0
  82. src/linters/dry/typescript_statement_detector.py +255 -0
  83. src/linters/dry/typescript_value_extractor.py +70 -0
  84. src/linters/dry/violation_builder.py +4 -0
  85. src/linters/dry/violation_filter.py +5 -4
  86. src/linters/dry/violation_generator.py +71 -14
  87. src/linters/file_header/atemporal_detector.py +68 -50
  88. src/linters/file_header/base_parser.py +93 -0
  89. src/linters/file_header/bash_parser.py +66 -0
  90. src/linters/file_header/config.py +90 -16
  91. src/linters/file_header/css_parser.py +70 -0
  92. src/linters/file_header/field_validator.py +36 -33
  93. src/linters/file_header/linter.py +140 -144
  94. src/linters/file_header/markdown_parser.py +130 -0
  95. src/linters/file_header/python_parser.py +14 -58
  96. src/linters/file_header/typescript_parser.py +73 -0
  97. src/linters/file_header/violation_builder.py +13 -12
  98. src/linters/file_placement/config_loader.py +3 -1
  99. src/linters/file_placement/directory_matcher.py +4 -0
  100. src/linters/file_placement/linter.py +66 -34
  101. src/linters/file_placement/pattern_matcher.py +41 -6
  102. src/linters/file_placement/pattern_validator.py +31 -12
  103. src/linters/file_placement/rule_checker.py +12 -7
  104. src/linters/lazy_ignores/__init__.py +43 -0
  105. src/linters/lazy_ignores/config.py +74 -0
  106. src/linters/lazy_ignores/directive_utils.py +164 -0
  107. src/linters/lazy_ignores/header_parser.py +177 -0
  108. src/linters/lazy_ignores/linter.py +158 -0
  109. src/linters/lazy_ignores/matcher.py +168 -0
  110. src/linters/lazy_ignores/python_analyzer.py +209 -0
  111. src/linters/lazy_ignores/rule_id_utils.py +180 -0
  112. src/linters/lazy_ignores/skip_detector.py +298 -0
  113. src/linters/lazy_ignores/types.py +71 -0
  114. src/linters/lazy_ignores/typescript_analyzer.py +146 -0
  115. src/linters/lazy_ignores/violation_builder.py +135 -0
  116. src/linters/lbyl/__init__.py +31 -0
  117. src/linters/lbyl/config.py +63 -0
  118. src/linters/lbyl/linter.py +67 -0
  119. src/linters/lbyl/pattern_detectors/__init__.py +53 -0
  120. src/linters/lbyl/pattern_detectors/base.py +63 -0
  121. src/linters/lbyl/pattern_detectors/dict_key_detector.py +107 -0
  122. src/linters/lbyl/pattern_detectors/division_check_detector.py +232 -0
  123. src/linters/lbyl/pattern_detectors/file_exists_detector.py +220 -0
  124. src/linters/lbyl/pattern_detectors/hasattr_detector.py +119 -0
  125. src/linters/lbyl/pattern_detectors/isinstance_detector.py +119 -0
  126. src/linters/lbyl/pattern_detectors/len_check_detector.py +173 -0
  127. src/linters/lbyl/pattern_detectors/none_check_detector.py +146 -0
  128. src/linters/lbyl/pattern_detectors/string_validator_detector.py +145 -0
  129. src/linters/lbyl/python_analyzer.py +215 -0
  130. src/linters/lbyl/violation_builder.py +354 -0
  131. src/linters/magic_numbers/context_analyzer.py +227 -225
  132. src/linters/magic_numbers/linter.py +28 -82
  133. src/linters/magic_numbers/python_analyzer.py +4 -16
  134. src/linters/magic_numbers/typescript_analyzer.py +9 -12
  135. src/linters/magic_numbers/typescript_ignore_checker.py +81 -0
  136. src/linters/method_property/__init__.py +49 -0
  137. src/linters/method_property/config.py +138 -0
  138. src/linters/method_property/linter.py +414 -0
  139. src/linters/method_property/python_analyzer.py +473 -0
  140. src/linters/method_property/violation_builder.py +119 -0
  141. src/linters/nesting/linter.py +24 -16
  142. src/linters/nesting/python_analyzer.py +4 -0
  143. src/linters/nesting/typescript_analyzer.py +6 -12
  144. src/linters/nesting/violation_builder.py +1 -0
  145. src/linters/performance/__init__.py +91 -0
  146. src/linters/performance/config.py +43 -0
  147. src/linters/performance/constants.py +49 -0
  148. src/linters/performance/linter.py +149 -0
  149. src/linters/performance/python_analyzer.py +365 -0
  150. src/linters/performance/regex_analyzer.py +312 -0
  151. src/linters/performance/regex_linter.py +139 -0
  152. src/linters/performance/typescript_analyzer.py +236 -0
  153. src/linters/performance/violation_builder.py +160 -0
  154. src/linters/print_statements/config.py +7 -12
  155. src/linters/print_statements/linter.py +26 -43
  156. src/linters/print_statements/python_analyzer.py +91 -93
  157. src/linters/print_statements/typescript_analyzer.py +15 -25
  158. src/linters/print_statements/violation_builder.py +12 -14
  159. src/linters/srp/class_analyzer.py +11 -7
  160. src/linters/srp/heuristics.py +56 -22
  161. src/linters/srp/linter.py +15 -16
  162. src/linters/srp/python_analyzer.py +55 -20
  163. src/linters/srp/typescript_metrics_calculator.py +110 -50
  164. src/linters/stateless_class/__init__.py +25 -0
  165. src/linters/stateless_class/config.py +58 -0
  166. src/linters/stateless_class/linter.py +349 -0
  167. src/linters/stateless_class/python_analyzer.py +290 -0
  168. src/linters/stringly_typed/__init__.py +36 -0
  169. src/linters/stringly_typed/config.py +189 -0
  170. src/linters/stringly_typed/context_filter.py +451 -0
  171. src/linters/stringly_typed/function_call_violation_builder.py +135 -0
  172. src/linters/stringly_typed/ignore_checker.py +100 -0
  173. src/linters/stringly_typed/ignore_utils.py +51 -0
  174. src/linters/stringly_typed/linter.py +376 -0
  175. src/linters/stringly_typed/python/__init__.py +33 -0
  176. src/linters/stringly_typed/python/analyzer.py +348 -0
  177. src/linters/stringly_typed/python/call_tracker.py +175 -0
  178. src/linters/stringly_typed/python/comparison_tracker.py +257 -0
  179. src/linters/stringly_typed/python/condition_extractor.py +134 -0
  180. src/linters/stringly_typed/python/conditional_detector.py +179 -0
  181. src/linters/stringly_typed/python/constants.py +21 -0
  182. src/linters/stringly_typed/python/match_analyzer.py +94 -0
  183. src/linters/stringly_typed/python/validation_detector.py +189 -0
  184. src/linters/stringly_typed/python/variable_extractor.py +96 -0
  185. src/linters/stringly_typed/storage.py +620 -0
  186. src/linters/stringly_typed/storage_initializer.py +45 -0
  187. src/linters/stringly_typed/typescript/__init__.py +28 -0
  188. src/linters/stringly_typed/typescript/analyzer.py +157 -0
  189. src/linters/stringly_typed/typescript/call_tracker.py +335 -0
  190. src/linters/stringly_typed/typescript/comparison_tracker.py +378 -0
  191. src/linters/stringly_typed/violation_generator.py +419 -0
  192. src/orchestrator/core.py +252 -14
  193. src/orchestrator/language_detector.py +5 -3
  194. src/templates/thailint_config_template.yaml +196 -0
  195. src/utils/project_root.py +3 -0
  196. thailint-0.15.3.dist-info/METADATA +187 -0
  197. thailint-0.15.3.dist-info/RECORD +226 -0
  198. thailint-0.15.3.dist-info/entry_points.txt +4 -0
  199. src/cli.py +0 -1665
  200. thailint-0.5.0.dist-info/METADATA +0 -1286
  201. thailint-0.5.0.dist-info/RECORD +0 -96
  202. thailint-0.5.0.dist-info/entry_points.txt +0 -4
  203. {thailint-0.5.0.dist-info → thailint-0.15.3.dist-info}/WHEEL +0 -0
  204. {thailint-0.5.0.dist-info → thailint-0.15.3.dist-info}/licenses/LICENSE +0 -0
@@ -1,38 +1,64 @@
1
1
  """
2
- File: src/linters/file_header/linter.py
3
2
  Purpose: Main file header linter rule implementation
4
- Exports: FileHeaderRule class
5
- Depends: BaseLintRule, PythonHeaderParser, FieldValidator, AtemporalDetector, ViolationBuilder
6
- Implements: Composition pattern with helper classes, AST-based Python parsing
7
- Related: config.py for configuration, python_parser.py for extraction
8
-
9
- Overview:
10
- Orchestrates file header validation for Python files using focused helper classes.
11
- Coordinates docstring extraction, field validation, atemporal language detection, and
12
- violation building. Supports configuration from .thailint.yaml and ignore directives.
13
- Validates headers against mandatory field requirements and atemporal language standards.
14
-
15
- Usage:
16
- rule = FileHeaderRule()
17
- violations = rule.check(context)
18
-
19
- Notes: Follows composition pattern from magic_numbers linter for maintainability
3
+
4
+ Scope: File header validation for Python, TypeScript, JavaScript, Bash, Markdown, and CSS files
5
+
6
+ Overview: Orchestrates file header validation for multiple languages using focused helper classes.
7
+ Coordinates header extraction, field validation, atemporal language detection, and
8
+ violation building. Supports configuration from .thailint.yaml and ignore directives
9
+ including file-level and line-level ignore markers. Validates headers against mandatory
10
+ field requirements and atemporal language standards. Handles language-specific parsing
11
+ and special Markdown prose field extraction for atemporal checking.
12
+
13
+ Dependencies: BaseLintRule and BaseLintContext from core, language-specific parsers,
14
+ FieldValidator, AtemporalDetector, ViolationBuilder
15
+
16
+ Exports: FileHeaderRule class implementing BaseLintRule interface
17
+
18
+ Interfaces: check(context) -> list[Violation] for rule validation, standard rule properties
19
+ (rule_id, rule_name, description)
20
+
21
+ Implementation: Composition pattern with helper classes for parsing, validation,
22
+ and violation building
23
+
24
+ Suppressions:
25
+ - type:ignore[type-var]: Protocol pattern with generic type matching
26
+ - srp: Rule class coordinates parsing, validation, and violation building for multiple
27
+ languages. Methods support single responsibility of file header validation.
20
28
  """
21
29
 
30
+ from contextlib import suppress
22
31
  from pathlib import Path
32
+ from typing import Protocol
23
33
 
24
34
  from src.core.base import BaseLintContext, BaseLintRule
35
+ from src.core.constants import HEADER_SCAN_LINES, Language
25
36
  from src.core.linter_utils import load_linter_config
26
37
  from src.core.types import Violation
27
- from src.linter_config.ignore import IgnoreDirectiveParser
38
+ from src.linter_config.directive_markers import check_general_ignore, has_ignore_directive_marker
39
+ from src.linter_config.ignore import _check_specific_rule_ignore, get_ignore_parser
28
40
 
29
41
  from .atemporal_detector import AtemporalDetector
42
+ from .bash_parser import BashHeaderParser
30
43
  from .config import FileHeaderConfig
44
+ from .css_parser import CssHeaderParser
31
45
  from .field_validator import FieldValidator
46
+ from .markdown_parser import MarkdownHeaderParser
32
47
  from .python_parser import PythonHeaderParser
48
+ from .typescript_parser import TypeScriptHeaderParser
33
49
  from .violation_builder import ViolationBuilder
34
50
 
35
51
 
52
+ class HeaderParser(Protocol):
53
+ """Protocol for header parsers."""
54
+
55
+ def extract_header(self, code: str) -> str | None:
56
+ """Extract header from source code."""
57
+
58
+ def parse_fields(self, header: str) -> dict[str, str]:
59
+ """Parse fields from header."""
60
+
61
+
36
62
  class FileHeaderRule(BaseLintRule): # thailint: ignore[srp]
37
63
  """Validates file headers for mandatory fields and atemporal language.
38
64
 
@@ -42,74 +68,97 @@ class FileHeaderRule(BaseLintRule): # thailint: ignore[srp]
42
68
  pattern with focused helper classes (parser, validator, detector, builder).
43
69
  """
44
70
 
71
+ # Parser instances for each language
72
+ _parsers: dict[str, HeaderParser] = {
73
+ "python": PythonHeaderParser(),
74
+ "typescript": TypeScriptHeaderParser(),
75
+ "javascript": TypeScriptHeaderParser(),
76
+ "bash": BashHeaderParser(),
77
+ "markdown": MarkdownHeaderParser(),
78
+ "css": CssHeaderParser(),
79
+ }
80
+
45
81
  def __init__(self) -> None:
46
82
  """Initialize the file header rule."""
47
83
  self._violation_builder = ViolationBuilder(self.rule_id)
48
- self._ignore_parser = IgnoreDirectiveParser()
84
+ self._ignore_parser = get_ignore_parser()
49
85
 
50
86
  @property
51
87
  def rule_id(self) -> str:
52
- """Unique identifier for this rule.
53
-
54
- Returns:
55
- Rule identifier string
56
- """
88
+ """Unique identifier for this rule."""
57
89
  return "file-header.validation"
58
90
 
59
91
  @property
60
92
  def rule_name(self) -> str:
61
- """Human-readable name for this rule.
62
-
63
- Returns:
64
- Rule name string
65
- """
93
+ """Human-readable name for this rule."""
66
94
  return "File Header Validation"
67
95
 
68
96
  @property
69
97
  def description(self) -> str:
70
- """Description of what this rule checks.
71
-
72
- Returns:
73
- Rule description string
74
- """
98
+ """Description of what this rule checks."""
75
99
  return "Validates file headers for mandatory fields and atemporal language"
76
100
 
77
101
  def check(self, context: BaseLintContext) -> list[Violation]:
78
- """Check file header for violations.
79
-
80
- Args:
81
- context: Lint context with file information
82
-
83
- Returns:
84
- List of violations found in file header
85
- """
86
- # Only Python for now (PR3), multi-language in PR5
87
- if context.language != "python":
88
- return []
89
-
90
- # Check for file-level ignore directives first
102
+ """Check file header for violations."""
91
103
  if self._has_file_ignore(context):
92
104
  return []
93
105
 
94
- # Load configuration
95
106
  config = self._load_config(context)
96
107
 
97
- # Check if file should be ignored by pattern
98
108
  if self._should_ignore_file(context, config):
99
109
  return []
100
110
 
101
- # Extract and validate header
102
- return self._check_python_header(context, config)
111
+ return self._check_language_header(context, config)
103
112
 
104
- def _has_file_ignore(self, context: BaseLintContext) -> bool:
105
- """Check if file has file-level ignore directive.
113
+ def _check_language_header(
114
+ self, context: BaseLintContext, config: FileHeaderConfig
115
+ ) -> list[Violation]:
116
+ """Dispatch to language-specific header checking."""
117
+ parser = self._parsers.get(context.language)
118
+ if not parser:
119
+ return []
120
+
121
+ # Markdown has special atemporal handling
122
+ if context.language == Language.MARKDOWN:
123
+ return self._check_markdown_header(parser, context, config)
124
+
125
+ return self._check_header_with_parser(parser, context, config)
126
+
127
+ def _check_header_with_parser(
128
+ self, parser: HeaderParser, context: BaseLintContext, config: FileHeaderConfig
129
+ ) -> list[Violation]:
130
+ """Check header using the given parser."""
131
+ header = parser.extract_header(context.file_content or "")
132
+
133
+ if not header:
134
+ return self._build_missing_header_violations(context)
106
135
 
107
- Args:
108
- context: Lint context
136
+ fields = parser.parse_fields(header)
137
+ violations = self._validate_header_fields(fields, context, config)
138
+ violations.extend(self._check_atemporal_violations(header, context, config))
139
+
140
+ return self._filter_ignored_violations(violations, context)
109
141
 
110
- Returns:
111
- True if file has ignore-file directive
112
- """
142
+ def _check_markdown_header(
143
+ self, parser: HeaderParser, context: BaseLintContext, config: FileHeaderConfig
144
+ ) -> list[Violation]:
145
+ """Check Markdown file header with special prose-only atemporal checking."""
146
+ header = parser.extract_header(context.file_content or "")
147
+
148
+ if not header:
149
+ return self._build_missing_header_violations(context)
150
+
151
+ fields = parser.parse_fields(header)
152
+ violations = self._validate_header_fields(fields, context, config)
153
+
154
+ # For Markdown, only check atemporal language in prose fields
155
+ prose_content = self._extract_markdown_prose_fields(fields)
156
+ violations.extend(self._check_atemporal_violations(prose_content, context, config))
157
+
158
+ return self._filter_ignored_violations(violations, context)
159
+
160
+ def _has_file_ignore(self, context: BaseLintContext) -> bool:
161
+ """Check if file has file-level ignore directive."""
113
162
  file_content = context.file_content or ""
114
163
 
115
164
  if self._has_standard_ignore(file_content):
@@ -117,21 +166,20 @@ class FileHeaderRule(BaseLintRule): # thailint: ignore[srp]
117
166
 
118
167
  return self._has_custom_ignore_syntax(file_content)
119
168
 
120
- def _has_standard_ignore(self, file_content: str) -> bool: # thailint: ignore[nesting]
169
+ def _has_standard_ignore(self, file_content: str) -> bool:
121
170
  """Check standard ignore parser for file-level ignores."""
122
- # Check first 10 lines for standard ignore directives
123
- first_lines = file_content.splitlines()[:10]
124
- for line in first_lines:
125
- if self._ignore_parser._has_ignore_directive_marker(line): # pylint: disable=protected-access
126
- if self._ignore_parser._check_specific_rule_ignore(line, self.rule_id): # pylint: disable=protected-access
127
- return True
128
- if self._ignore_parser._check_general_ignore(line): # pylint: disable=protected-access
129
- return True
130
- return False
171
+ first_lines = file_content.splitlines()[:HEADER_SCAN_LINES]
172
+ return any(self._line_has_matching_ignore(line) for line in first_lines)
173
+
174
+ def _line_has_matching_ignore(self, line: str) -> bool:
175
+ """Check if line has matching ignore directive for this rule."""
176
+ if not has_ignore_directive_marker(line):
177
+ return False
178
+ return _check_specific_rule_ignore(line, self.rule_id) or check_general_ignore(line)
131
179
 
132
180
  def _has_custom_ignore_syntax(self, file_content: str) -> bool:
133
181
  """Check custom file-level ignore syntax."""
134
- first_lines = file_content.splitlines()[:10]
182
+ first_lines = file_content.splitlines()[:HEADER_SCAN_LINES]
135
183
  return any(self._is_ignore_line(line) for line in first_lines)
136
184
 
137
185
  def _is_ignore_line(self, line: str) -> bool:
@@ -140,32 +188,15 @@ class FileHeaderRule(BaseLintRule): # thailint: ignore[srp]
140
188
  return "# thailint-ignore-file:" in line_lower or "# thailint-ignore" in line_lower
141
189
 
142
190
  def _load_config(self, context: BaseLintContext) -> FileHeaderConfig:
143
- """Load configuration from context.
144
-
145
- Args:
146
- context: Lint context
147
-
148
- Returns:
149
- FileHeaderConfig with loaded or default values
150
- """
151
- # Try production config first
191
+ """Load configuration from context."""
152
192
  if hasattr(context, "metadata") and isinstance(context.metadata, dict):
153
193
  if "file_header" in context.metadata:
154
194
  return load_linter_config(context, "file_header", FileHeaderConfig) # type: ignore[type-var]
155
195
 
156
- # Use defaults
157
196
  return FileHeaderConfig()
158
197
 
159
198
  def _should_ignore_file(self, context: BaseLintContext, config: FileHeaderConfig) -> bool:
160
- """Check if file matches ignore patterns.
161
-
162
- Args:
163
- context: Lint context
164
- config: File header configuration
165
-
166
- Returns:
167
- True if file should be ignored
168
- """
199
+ """Check if file matches ignore patterns."""
169
200
  if not context.file_path:
170
201
  return False
171
202
 
@@ -200,30 +231,6 @@ class FileHeaderRule(BaseLintRule): # thailint: ignore[srp]
200
231
  return file_path.name == filename_pattern or path_str.endswith(filename_pattern)
201
232
  return False
202
233
 
203
- def _check_python_header(
204
- self, context: BaseLintContext, config: FileHeaderConfig
205
- ) -> list[Violation]:
206
- """Check Python file header.
207
-
208
- Args:
209
- context: Lint context
210
- config: Configuration
211
-
212
- Returns:
213
- List of violations filtered through ignore directives
214
- """
215
- parser = PythonHeaderParser()
216
- header = parser.extract_header(context.file_content or "")
217
-
218
- if not header:
219
- return self._build_missing_header_violations(context)
220
-
221
- fields = parser.parse_fields(header)
222
- violations = self._validate_header_fields(fields, context, config)
223
- violations.extend(self._check_atemporal_violations(header, context, config))
224
-
225
- return self._filter_ignored_violations(violations, context)
226
-
227
234
  def _build_missing_header_violations(self, context: BaseLintContext) -> list[Violation]:
228
235
  """Build violations for missing header."""
229
236
  return [
@@ -270,44 +277,33 @@ class FileHeaderRule(BaseLintRule): # thailint: ignore[srp]
270
277
  def _filter_ignored_violations(
271
278
  self, violations: list[Violation], context: BaseLintContext
272
279
  ) -> list[Violation]:
273
- """Filter out violations that should be ignored.
274
-
275
- Args:
276
- violations: List of violations to filter
277
- context: Lint context with file content
278
-
279
- Returns:
280
- Filtered list of violations
281
- """
280
+ """Filter out violations that should be ignored."""
282
281
  file_content = context.file_content or ""
283
282
  lines = file_content.splitlines()
284
283
 
285
- filtered = []
286
- for v in violations:
287
- # Check standard ignore directives
288
- if self._ignore_parser.should_ignore_violation(v, file_content):
289
- continue
290
-
291
- # Check custom line-level ignore syntax: # thailint-ignore-line:
292
- if self._has_line_level_ignore(lines, v):
293
- continue
294
-
295
- filtered.append(v)
296
-
297
- return filtered
284
+ non_ignored = (
285
+ v
286
+ for v in violations
287
+ if not self._ignore_parser.should_ignore_violation(v, file_content)
288
+ and not self._has_line_level_ignore(lines, v)
289
+ )
290
+ return list(non_ignored)
298
291
 
299
292
  def _has_line_level_ignore(self, lines: list[str], violation: Violation) -> bool:
300
- """Check for thailint-ignore-line directive.
301
-
302
- Args:
303
- lines: File content split into lines
304
- violation: Violation to check
305
-
306
- Returns:
307
- True if line has ignore directive
308
- """
293
+ """Check for thailint-ignore-line directive."""
309
294
  if violation.line <= 0 or violation.line > len(lines):
310
295
  return False
311
296
 
312
- line_content = lines[violation.line - 1] # Convert to 0-indexed
297
+ line_content = lines[violation.line - 1]
313
298
  return "# thailint-ignore-line:" in line_content.lower()
299
+
300
+ def _extract_markdown_prose_fields(self, fields: dict[str, str]) -> str:
301
+ """Extract prose fields from Markdown frontmatter for atemporal checking."""
302
+ prose_fields = ["purpose", "scope", "overview"]
303
+ prose_parts = []
304
+
305
+ for field_name in prose_fields:
306
+ with suppress(KeyError):
307
+ prose_parts.append(f"{field_name}: {fields[field_name]}")
308
+
309
+ return "\n".join(prose_parts)
@@ -0,0 +1,130 @@
1
+ """
2
+ Purpose: Markdown YAML frontmatter extraction and parsing
3
+
4
+ Scope: Markdown file header parsing from YAML frontmatter
5
+
6
+ Overview: Extracts YAML frontmatter from Markdown files. Frontmatter must be at the
7
+ start of the file, enclosed in --- markers. Parses YAML content to extract
8
+ field values using PyYAML when available, falling back to regex parsing if not.
9
+ Handles both simple key-value pairs and complex YAML structures including lists.
10
+ Flattens nested structures into string representations for field validation.
11
+
12
+ Dependencies: re module for frontmatter pattern matching, yaml module (optional) for parsing, logging module
13
+
14
+ Exports: MarkdownHeaderParser class
15
+
16
+ Interfaces: extract_header(code) -> str | None for frontmatter extraction,
17
+ parse_fields(header) -> dict[str, str] for field parsing
18
+
19
+ Implementation: YAML frontmatter extraction with PyYAML parsing and regex fallback for robustness
20
+
21
+ Suppressions:
22
+ - BLE001: Broad exception catch for YAML parsing fallback (any exception triggers regex fallback)
23
+ - srp: Class coordinates YAML extraction, parsing, and field validation for Markdown.
24
+ Method count exceeds limit due to complexity refactoring.
25
+ - nesting,dry: _parse_simple_yaml uses nested loops for YAML structure traversal.
26
+ """
27
+
28
+ import logging
29
+ import re
30
+
31
+ logger = logging.getLogger(__name__)
32
+
33
+
34
+ class MarkdownHeaderParser: # thailint: ignore[srp]
35
+ """Extracts and parses Markdown file headers from YAML frontmatter.
36
+
37
+ Method count (10) exceeds SRP guideline (8) because proper A-grade complexity
38
+ refactoring requires extracting small focused helper methods. Class maintains
39
+ single responsibility of YAML frontmatter parsing - all methods support this
40
+ core purpose through either PyYAML or simple regex parsing fallback.
41
+ """
42
+
43
+ # Pattern to match YAML frontmatter at start of file
44
+ FRONTMATTER_PATTERN = re.compile(r"^---\s*\n(.*?)\n---", re.DOTALL)
45
+
46
+ def extract_header(self, code: str) -> str | None:
47
+ """Extract YAML frontmatter from Markdown file."""
48
+ if not code or not code.strip():
49
+ return None
50
+
51
+ match = self.FRONTMATTER_PATTERN.match(code)
52
+ return match.group(1).strip() if match else None
53
+
54
+ def parse_fields(self, header: str) -> dict[str, str]:
55
+ """Parse YAML frontmatter into field dictionary."""
56
+ yaml_result = self._try_yaml_parse(header)
57
+ if yaml_result is not None:
58
+ return yaml_result
59
+
60
+ return self._parse_simple_yaml(header)
61
+
62
+ def _try_yaml_parse(self, header: str) -> dict[str, str] | None:
63
+ """Try to parse with PyYAML, returning None if unavailable or failed."""
64
+ try:
65
+ import yaml
66
+
67
+ data = yaml.safe_load(header)
68
+ if isinstance(data, dict):
69
+ return self._flatten_yaml_dict(data)
70
+ except ImportError:
71
+ logger.debug("PyYAML not available, using simple parser")
72
+ except Exception: # noqa: BLE001
73
+ logger.debug("YAML parsing failed, falling back to simple parser")
74
+ return None
75
+
76
+ def _flatten_yaml_dict(self, data: dict) -> dict[str, str]:
77
+ """Convert YAML dict to string values."""
78
+ result: dict[str, str] = {}
79
+ for key, value in data.items():
80
+ result[str(key)] = self._convert_value(value)
81
+ return result
82
+
83
+ def _convert_value(self, value: object) -> str:
84
+ """Convert a single YAML value to string."""
85
+ if isinstance(value, list):
86
+ return ", ".join(str(v) for v in value)
87
+ if value is not None:
88
+ return str(value)
89
+ return ""
90
+
91
+ def _parse_simple_yaml( # thailint: ignore[nesting,dry]
92
+ self, header: str
93
+ ) -> dict[str, str]:
94
+ """Simple regex-based YAML parsing fallback."""
95
+ fields: dict[str, str] = {}
96
+ current_field: str | None = None
97
+ current_value: list[str] = []
98
+
99
+ for line in header.split("\n"):
100
+ if self._is_field_start(line):
101
+ self._save_field(fields, current_field, current_value)
102
+ current_field, current_value = self._start_field(line)
103
+ elif current_field and line.strip():
104
+ current_value.append(self._process_continuation(line))
105
+
106
+ self._save_field(fields, current_field, current_value)
107
+ return fields
108
+
109
+ def _is_field_start(self, line: str) -> bool:
110
+ """Check if line starts a new field (not indented, has colon)."""
111
+ return not line.startswith(" ") and ":" in line
112
+
113
+ def _start_field(self, line: str) -> tuple[str, list[str]]:
114
+ """Parse field start and return field name and initial value."""
115
+ parts = line.split(":", 1)
116
+ field_name = parts[0].strip()
117
+ value = parts[1].strip() if len(parts) > 1 else ""
118
+ return field_name, [value] if value else []
119
+
120
+ def _process_continuation(self, line: str) -> str:
121
+ """Process a continuation line (list item or multiline value)."""
122
+ stripped = line.strip()
123
+ return stripped[2:] if stripped.startswith("- ") else stripped
124
+
125
+ def _save_field(
126
+ self, fields: dict[str, str], field_name: str | None, values: list[str]
127
+ ) -> None:
128
+ """Save field to dictionary if field name exists."""
129
+ if field_name:
130
+ fields[field_name] = "\n".join(values).strip()
@@ -1,29 +1,29 @@
1
1
  """
2
- File: src/linters/file_header/python_parser.py
3
2
  Purpose: Python docstring extraction and parsing for file headers
4
- Exports: PythonHeaderParser class
5
- Depends: Python ast module
6
- Implements: AST-based docstring extraction with field parsing
7
- Related: linter.py for parser usage, field_validator.py for field validation
8
3
 
9
- Overview:
10
- Extracts module-level docstrings from Python files using AST parsing.
4
+ Scope: Python file header parsing from module-level docstrings
5
+
6
+ Overview: Extracts module-level docstrings from Python files using AST parsing.
11
7
  Parses structured header fields from docstring content and handles both
12
8
  well-formed and malformed headers. Provides field extraction and validation
13
- support for FileHeaderRule.
9
+ support for FileHeaderRule. Uses ast.get_docstring() for reliable extraction
10
+ and gracefully handles syntax errors in source code.
11
+
12
+ Dependencies: Python ast module for AST parsing, base_parser.BaseHeaderParser for field parsing
13
+
14
+ Exports: PythonHeaderParser class
14
15
 
15
- Usage:
16
- parser = PythonHeaderParser()
17
- header = parser.extract_header(code)
18
- fields = parser.parse_fields(header)
16
+ Interfaces: extract_header(code) -> str | None for docstring extraction, parse_fields(header) inherited from base
19
17
 
20
- Notes: Uses ast.get_docstring() for reliable module-level docstring extraction
18
+ Implementation: AST-based docstring extraction with syntax error handling
21
19
  """
22
20
 
23
21
  import ast
24
22
 
23
+ from src.linters.file_header.base_parser import BaseHeaderParser
25
24
 
26
- class PythonHeaderParser:
25
+
26
+ class PythonHeaderParser(BaseHeaderParser):
27
27
  """Extracts and parses Python file headers from docstrings."""
28
28
 
29
29
  def extract_header(self, code: str) -> str | None:
@@ -40,47 +40,3 @@ class PythonHeaderParser:
40
40
  return ast.get_docstring(tree)
41
41
  except SyntaxError:
42
42
  return None
43
-
44
- def parse_fields(self, header: str) -> dict[str, str]: # thailint: ignore[nesting]
45
- """Parse structured fields from header text.
46
-
47
- Args:
48
- header: Header docstring text
49
-
50
- Returns:
51
- Dictionary mapping field_name -> field_value
52
- """
53
- fields: dict[str, str] = {}
54
- current_field: str | None = None
55
- current_value: list[str] = []
56
-
57
- for line in header.split("\n"):
58
- if self._is_new_field_line(line):
59
- current_field = self._save_and_start_new_field(
60
- fields, current_field, current_value, line
61
- )
62
- current_value = [line.split(":", 1)[1].strip()]
63
- elif current_field:
64
- current_value.append(line.strip())
65
-
66
- self._save_current_field(fields, current_field, current_value)
67
- return fields
68
-
69
- def _is_new_field_line(self, line: str) -> bool:
70
- """Check if line starts a new field."""
71
- return ":" in line and not line.startswith(" ")
72
-
73
- def _save_and_start_new_field(
74
- self, fields: dict[str, str], current_field: str | None, current_value: list[str], line: str
75
- ) -> str:
76
- """Save current field and start new one."""
77
- if current_field:
78
- fields[current_field] = "\n".join(current_value).strip()
79
- return line.split(":", 1)[0].strip()
80
-
81
- def _save_current_field(
82
- self, fields: dict[str, str], current_field: str | None, current_value: list[str]
83
- ) -> None:
84
- """Save the last field."""
85
- if current_field:
86
- fields[current_field] = "\n".join(current_value).strip()
@@ -0,0 +1,73 @@
1
+ """
2
+ Purpose: TypeScript/JavaScript JSDoc comment extraction and parsing
3
+
4
+ Scope: TypeScript and JavaScript file header parsing from JSDoc comments
5
+
6
+ Overview: Extracts JSDoc-style comments (/** ... */) from TypeScript and JavaScript files.
7
+ Parses structured header fields from JSDoc content and handles both single-line
8
+ and multi-line field values. Distinguishes JSDoc comments from regular block
9
+ comments (/* ... */) by requiring the double asterisk syntax. Cleans formatting
10
+ characters including leading asterisks from content lines.
11
+
12
+ Dependencies: re module for regex-based JSDoc pattern matching, base_parser.BaseHeaderParser for field parsing
13
+
14
+ Exports: TypeScriptHeaderParser class
15
+
16
+ Interfaces: extract_header(code) -> str | None for JSDoc extraction, parse_fields(header) inherited from base
17
+
18
+ Implementation: Regex-based JSDoc extraction with content cleaning and formatting removal
19
+ """
20
+
21
+ import re
22
+
23
+ from src.linters.file_header.base_parser import BaseHeaderParser
24
+
25
+
26
+ class TypeScriptHeaderParser(BaseHeaderParser):
27
+ """Extracts and parses TypeScript/JavaScript file headers from JSDoc comments."""
28
+
29
+ # Pattern to match JSDoc comment at start of file (allowing whitespace before)
30
+ JSDOC_PATTERN = re.compile(r"^\s*/\*\*\s*(.*?)\s*\*/", re.DOTALL)
31
+
32
+ def extract_header(self, code: str) -> str | None:
33
+ """Extract JSDoc comment from TypeScript/JavaScript code.
34
+
35
+ Args:
36
+ code: TypeScript/JavaScript source code
37
+
38
+ Returns:
39
+ JSDoc content or None if not found
40
+ """
41
+ if not code or not code.strip():
42
+ return None
43
+
44
+ match = self.JSDOC_PATTERN.match(code)
45
+ if not match:
46
+ return None
47
+
48
+ # Extract the content inside the JSDoc
49
+ jsdoc_content = match.group(1)
50
+
51
+ # Clean up the JSDoc content - remove leading * from each line
52
+ return self._clean_jsdoc_content(jsdoc_content)
53
+
54
+ def _clean_jsdoc_content(self, content: str) -> str:
55
+ """Remove JSDoc formatting (leading asterisks) from content.
56
+
57
+ Args:
58
+ content: Raw JSDoc content
59
+
60
+ Returns:
61
+ Cleaned content without leading asterisks
62
+ """
63
+ lines = content.split("\n")
64
+ cleaned_lines = []
65
+
66
+ for line in lines:
67
+ # Remove leading whitespace and asterisk
68
+ stripped = line.strip()
69
+ if stripped.startswith("*"):
70
+ stripped = stripped[1:].strip()
71
+ cleaned_lines.append(stripped)
72
+
73
+ return "\n".join(cleaned_lines)