thailint 0.5.0__py3-none-any.whl → 0.8.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.
- src/cli.py +236 -2
- src/core/cli_utils.py +16 -1
- src/core/registry.py +1 -1
- src/formatters/__init__.py +22 -0
- src/formatters/sarif.py +202 -0
- src/linter_config/loader.py +5 -4
- src/linters/dry/block_filter.py +11 -8
- src/linters/dry/cache.py +3 -2
- src/linters/dry/duplicate_storage.py +5 -4
- src/linters/dry/violation_generator.py +1 -1
- src/linters/file_header/atemporal_detector.py +11 -11
- src/linters/file_header/base_parser.py +89 -0
- src/linters/file_header/bash_parser.py +58 -0
- src/linters/file_header/config.py +76 -16
- src/linters/file_header/css_parser.py +70 -0
- src/linters/file_header/field_validator.py +35 -29
- src/linters/file_header/linter.py +113 -121
- src/linters/file_header/markdown_parser.py +124 -0
- src/linters/file_header/python_parser.py +14 -58
- src/linters/file_header/typescript_parser.py +73 -0
- src/linters/file_header/violation_builder.py +13 -12
- src/linters/file_placement/linter.py +9 -11
- src/linters/method_property/__init__.py +49 -0
- src/linters/method_property/config.py +135 -0
- src/linters/method_property/linter.py +419 -0
- src/linters/method_property/python_analyzer.py +472 -0
- src/linters/method_property/violation_builder.py +116 -0
- src/linters/print_statements/config.py +7 -12
- src/linters/print_statements/linter.py +13 -15
- src/linters/print_statements/python_analyzer.py +8 -14
- src/linters/print_statements/typescript_analyzer.py +9 -14
- src/linters/print_statements/violation_builder.py +12 -14
- {thailint-0.5.0.dist-info → thailint-0.8.0.dist-info}/METADATA +155 -3
- {thailint-0.5.0.dist-info → thailint-0.8.0.dist-info}/RECORD +37 -25
- {thailint-0.5.0.dist-info → thailint-0.8.0.dist-info}/WHEEL +0 -0
- {thailint-0.5.0.dist-info → thailint-0.8.0.dist-info}/entry_points.txt +0 -0
- {thailint-0.5.0.dist-info → thailint-0.8.0.dist-info}/licenses/LICENSE +0 -0
|
@@ -1,25 +1,29 @@
|
|
|
1
1
|
"""
|
|
2
|
-
File: src/linters/file_header/linter.py
|
|
3
2
|
Purpose: Main file header linter rule implementation
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
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
|
|
20
23
|
"""
|
|
21
24
|
|
|
22
25
|
from pathlib import Path
|
|
26
|
+
from typing import Protocol
|
|
23
27
|
|
|
24
28
|
from src.core.base import BaseLintContext, BaseLintRule
|
|
25
29
|
from src.core.linter_utils import load_linter_config
|
|
@@ -27,12 +31,26 @@ from src.core.types import Violation
|
|
|
27
31
|
from src.linter_config.ignore import IgnoreDirectiveParser
|
|
28
32
|
|
|
29
33
|
from .atemporal_detector import AtemporalDetector
|
|
34
|
+
from .bash_parser import BashHeaderParser
|
|
30
35
|
from .config import FileHeaderConfig
|
|
36
|
+
from .css_parser import CssHeaderParser
|
|
31
37
|
from .field_validator import FieldValidator
|
|
38
|
+
from .markdown_parser import MarkdownHeaderParser
|
|
32
39
|
from .python_parser import PythonHeaderParser
|
|
40
|
+
from .typescript_parser import TypeScriptHeaderParser
|
|
33
41
|
from .violation_builder import ViolationBuilder
|
|
34
42
|
|
|
35
43
|
|
|
44
|
+
class HeaderParser(Protocol):
|
|
45
|
+
"""Protocol for header parsers."""
|
|
46
|
+
|
|
47
|
+
def extract_header(self, code: str) -> str | None:
|
|
48
|
+
"""Extract header from source code."""
|
|
49
|
+
|
|
50
|
+
def parse_fields(self, header: str) -> dict[str, str]:
|
|
51
|
+
"""Parse fields from header."""
|
|
52
|
+
|
|
53
|
+
|
|
36
54
|
class FileHeaderRule(BaseLintRule): # thailint: ignore[srp]
|
|
37
55
|
"""Validates file headers for mandatory fields and atemporal language.
|
|
38
56
|
|
|
@@ -42,6 +60,16 @@ class FileHeaderRule(BaseLintRule): # thailint: ignore[srp]
|
|
|
42
60
|
pattern with focused helper classes (parser, validator, detector, builder).
|
|
43
61
|
"""
|
|
44
62
|
|
|
63
|
+
# Parser instances for each language
|
|
64
|
+
_parsers: dict[str, HeaderParser] = {
|
|
65
|
+
"python": PythonHeaderParser(),
|
|
66
|
+
"typescript": TypeScriptHeaderParser(),
|
|
67
|
+
"javascript": TypeScriptHeaderParser(),
|
|
68
|
+
"bash": BashHeaderParser(),
|
|
69
|
+
"markdown": MarkdownHeaderParser(),
|
|
70
|
+
"css": CssHeaderParser(),
|
|
71
|
+
}
|
|
72
|
+
|
|
45
73
|
def __init__(self) -> None:
|
|
46
74
|
"""Initialize the file header rule."""
|
|
47
75
|
self._violation_builder = ViolationBuilder(self.rule_id)
|
|
@@ -49,67 +77,80 @@ class FileHeaderRule(BaseLintRule): # thailint: ignore[srp]
|
|
|
49
77
|
|
|
50
78
|
@property
|
|
51
79
|
def rule_id(self) -> str:
|
|
52
|
-
"""Unique identifier for this rule.
|
|
53
|
-
|
|
54
|
-
Returns:
|
|
55
|
-
Rule identifier string
|
|
56
|
-
"""
|
|
80
|
+
"""Unique identifier for this rule."""
|
|
57
81
|
return "file-header.validation"
|
|
58
82
|
|
|
59
83
|
@property
|
|
60
84
|
def rule_name(self) -> str:
|
|
61
|
-
"""Human-readable name for this rule.
|
|
62
|
-
|
|
63
|
-
Returns:
|
|
64
|
-
Rule name string
|
|
65
|
-
"""
|
|
85
|
+
"""Human-readable name for this rule."""
|
|
66
86
|
return "File Header Validation"
|
|
67
87
|
|
|
68
88
|
@property
|
|
69
89
|
def description(self) -> str:
|
|
70
|
-
"""Description of what this rule checks.
|
|
71
|
-
|
|
72
|
-
Returns:
|
|
73
|
-
Rule description string
|
|
74
|
-
"""
|
|
90
|
+
"""Description of what this rule checks."""
|
|
75
91
|
return "Validates file headers for mandatory fields and atemporal language"
|
|
76
92
|
|
|
77
93
|
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
|
|
94
|
+
"""Check file header for violations."""
|
|
91
95
|
if self._has_file_ignore(context):
|
|
92
96
|
return []
|
|
93
97
|
|
|
94
|
-
# Load configuration
|
|
95
98
|
config = self._load_config(context)
|
|
96
99
|
|
|
97
|
-
# Check if file should be ignored by pattern
|
|
98
100
|
if self._should_ignore_file(context, config):
|
|
99
101
|
return []
|
|
100
102
|
|
|
101
|
-
|
|
102
|
-
return self._check_python_header(context, config)
|
|
103
|
+
return self._check_language_header(context, config)
|
|
103
104
|
|
|
104
|
-
def
|
|
105
|
-
|
|
105
|
+
def _check_language_header(
|
|
106
|
+
self, context: BaseLintContext, config: FileHeaderConfig
|
|
107
|
+
) -> list[Violation]:
|
|
108
|
+
"""Dispatch to language-specific header checking."""
|
|
109
|
+
parser = self._parsers.get(context.language)
|
|
110
|
+
if not parser:
|
|
111
|
+
return []
|
|
112
|
+
|
|
113
|
+
# Markdown has special atemporal handling
|
|
114
|
+
if context.language == "markdown":
|
|
115
|
+
return self._check_markdown_header(parser, context, config)
|
|
116
|
+
|
|
117
|
+
return self._check_header_with_parser(parser, context, config)
|
|
118
|
+
|
|
119
|
+
def _check_header_with_parser(
|
|
120
|
+
self, parser: HeaderParser, context: BaseLintContext, config: FileHeaderConfig
|
|
121
|
+
) -> list[Violation]:
|
|
122
|
+
"""Check header using the given parser."""
|
|
123
|
+
header = parser.extract_header(context.file_content or "")
|
|
124
|
+
|
|
125
|
+
if not header:
|
|
126
|
+
return self._build_missing_header_violations(context)
|
|
127
|
+
|
|
128
|
+
fields = parser.parse_fields(header)
|
|
129
|
+
violations = self._validate_header_fields(fields, context, config)
|
|
130
|
+
violations.extend(self._check_atemporal_violations(header, context, config))
|
|
131
|
+
|
|
132
|
+
return self._filter_ignored_violations(violations, context)
|
|
133
|
+
|
|
134
|
+
def _check_markdown_header(
|
|
135
|
+
self, parser: HeaderParser, context: BaseLintContext, config: FileHeaderConfig
|
|
136
|
+
) -> list[Violation]:
|
|
137
|
+
"""Check Markdown file header with special prose-only atemporal checking."""
|
|
138
|
+
header = parser.extract_header(context.file_content or "")
|
|
139
|
+
|
|
140
|
+
if not header:
|
|
141
|
+
return self._build_missing_header_violations(context)
|
|
106
142
|
|
|
107
|
-
|
|
108
|
-
|
|
143
|
+
fields = parser.parse_fields(header)
|
|
144
|
+
violations = self._validate_header_fields(fields, context, config)
|
|
145
|
+
|
|
146
|
+
# For Markdown, only check atemporal language in prose fields
|
|
147
|
+
prose_content = self._extract_markdown_prose_fields(fields)
|
|
148
|
+
violations.extend(self._check_atemporal_violations(prose_content, context, config))
|
|
149
|
+
|
|
150
|
+
return self._filter_ignored_violations(violations, context)
|
|
109
151
|
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
"""
|
|
152
|
+
def _has_file_ignore(self, context: BaseLintContext) -> bool:
|
|
153
|
+
"""Check if file has file-level ignore directive."""
|
|
113
154
|
file_content = context.file_content or ""
|
|
114
155
|
|
|
115
156
|
if self._has_standard_ignore(file_content):
|
|
@@ -119,7 +160,6 @@ class FileHeaderRule(BaseLintRule): # thailint: ignore[srp]
|
|
|
119
160
|
|
|
120
161
|
def _has_standard_ignore(self, file_content: str) -> bool: # thailint: ignore[nesting]
|
|
121
162
|
"""Check standard ignore parser for file-level ignores."""
|
|
122
|
-
# Check first 10 lines for standard ignore directives
|
|
123
163
|
first_lines = file_content.splitlines()[:10]
|
|
124
164
|
for line in first_lines:
|
|
125
165
|
if self._ignore_parser._has_ignore_directive_marker(line): # pylint: disable=protected-access
|
|
@@ -140,32 +180,15 @@ class FileHeaderRule(BaseLintRule): # thailint: ignore[srp]
|
|
|
140
180
|
return "# thailint-ignore-file:" in line_lower or "# thailint-ignore" in line_lower
|
|
141
181
|
|
|
142
182
|
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
|
|
183
|
+
"""Load configuration from context."""
|
|
152
184
|
if hasattr(context, "metadata") and isinstance(context.metadata, dict):
|
|
153
185
|
if "file_header" in context.metadata:
|
|
154
186
|
return load_linter_config(context, "file_header", FileHeaderConfig) # type: ignore[type-var]
|
|
155
187
|
|
|
156
|
-
# Use defaults
|
|
157
188
|
return FileHeaderConfig()
|
|
158
189
|
|
|
159
190
|
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
|
-
"""
|
|
191
|
+
"""Check if file matches ignore patterns."""
|
|
169
192
|
if not context.file_path:
|
|
170
193
|
return False
|
|
171
194
|
|
|
@@ -200,30 +223,6 @@ class FileHeaderRule(BaseLintRule): # thailint: ignore[srp]
|
|
|
200
223
|
return file_path.name == filename_pattern or path_str.endswith(filename_pattern)
|
|
201
224
|
return False
|
|
202
225
|
|
|
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
226
|
def _build_missing_header_violations(self, context: BaseLintContext) -> list[Violation]:
|
|
228
227
|
"""Build violations for missing header."""
|
|
229
228
|
return [
|
|
@@ -270,25 +269,15 @@ class FileHeaderRule(BaseLintRule): # thailint: ignore[srp]
|
|
|
270
269
|
def _filter_ignored_violations(
|
|
271
270
|
self, violations: list[Violation], context: BaseLintContext
|
|
272
271
|
) -> 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
|
-
"""
|
|
272
|
+
"""Filter out violations that should be ignored."""
|
|
282
273
|
file_content = context.file_content or ""
|
|
283
274
|
lines = file_content.splitlines()
|
|
284
275
|
|
|
285
276
|
filtered = []
|
|
286
277
|
for v in violations:
|
|
287
|
-
# Check standard ignore directives
|
|
288
278
|
if self._ignore_parser.should_ignore_violation(v, file_content):
|
|
289
279
|
continue
|
|
290
280
|
|
|
291
|
-
# Check custom line-level ignore syntax: # thailint-ignore-line:
|
|
292
281
|
if self._has_line_level_ignore(lines, v):
|
|
293
282
|
continue
|
|
294
283
|
|
|
@@ -297,17 +286,20 @@ class FileHeaderRule(BaseLintRule): # thailint: ignore[srp]
|
|
|
297
286
|
return filtered
|
|
298
287
|
|
|
299
288
|
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
|
-
"""
|
|
289
|
+
"""Check for thailint-ignore-line directive."""
|
|
309
290
|
if violation.line <= 0 or violation.line > len(lines):
|
|
310
291
|
return False
|
|
311
292
|
|
|
312
|
-
line_content = lines[violation.line - 1]
|
|
293
|
+
line_content = lines[violation.line - 1]
|
|
313
294
|
return "# thailint-ignore-line:" in line_content.lower()
|
|
295
|
+
|
|
296
|
+
def _extract_markdown_prose_fields(self, fields: dict[str, str]) -> str:
|
|
297
|
+
"""Extract prose fields from Markdown frontmatter for atemporal checking."""
|
|
298
|
+
prose_fields = ["purpose", "scope", "overview"]
|
|
299
|
+
prose_parts = []
|
|
300
|
+
|
|
301
|
+
for field_name in prose_fields:
|
|
302
|
+
if field_name in fields:
|
|
303
|
+
prose_parts.append(f"{field_name}: {fields[field_name]}")
|
|
304
|
+
|
|
305
|
+
return "\n".join(prose_parts)
|
|
@@ -0,0 +1,124 @@
|
|
|
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
|
+
|
|
22
|
+
import logging
|
|
23
|
+
import re
|
|
24
|
+
|
|
25
|
+
logger = logging.getLogger(__name__)
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
class MarkdownHeaderParser: # thailint: ignore[srp]
|
|
29
|
+
"""Extracts and parses Markdown file headers from YAML frontmatter.
|
|
30
|
+
|
|
31
|
+
Method count (10) exceeds SRP guideline (8) because proper A-grade complexity
|
|
32
|
+
refactoring requires extracting small focused helper methods. Class maintains
|
|
33
|
+
single responsibility of YAML frontmatter parsing - all methods support this
|
|
34
|
+
core purpose through either PyYAML or simple regex parsing fallback.
|
|
35
|
+
"""
|
|
36
|
+
|
|
37
|
+
# Pattern to match YAML frontmatter at start of file
|
|
38
|
+
FRONTMATTER_PATTERN = re.compile(r"^---\s*\n(.*?)\n---", re.DOTALL)
|
|
39
|
+
|
|
40
|
+
def extract_header(self, code: str) -> str | None:
|
|
41
|
+
"""Extract YAML frontmatter from Markdown file."""
|
|
42
|
+
if not code or not code.strip():
|
|
43
|
+
return None
|
|
44
|
+
|
|
45
|
+
match = self.FRONTMATTER_PATTERN.match(code)
|
|
46
|
+
return match.group(1).strip() if match else None
|
|
47
|
+
|
|
48
|
+
def parse_fields(self, header: str) -> dict[str, str]:
|
|
49
|
+
"""Parse YAML frontmatter into field dictionary."""
|
|
50
|
+
yaml_result = self._try_yaml_parse(header)
|
|
51
|
+
if yaml_result is not None:
|
|
52
|
+
return yaml_result
|
|
53
|
+
|
|
54
|
+
return self._parse_simple_yaml(header)
|
|
55
|
+
|
|
56
|
+
def _try_yaml_parse(self, header: str) -> dict[str, str] | None:
|
|
57
|
+
"""Try to parse with PyYAML, returning None if unavailable or failed."""
|
|
58
|
+
try:
|
|
59
|
+
import yaml
|
|
60
|
+
|
|
61
|
+
data = yaml.safe_load(header)
|
|
62
|
+
if isinstance(data, dict):
|
|
63
|
+
return self._flatten_yaml_dict(data)
|
|
64
|
+
except ImportError:
|
|
65
|
+
logger.debug("PyYAML not available, using simple parser")
|
|
66
|
+
except Exception: # noqa: BLE001
|
|
67
|
+
logger.debug("YAML parsing failed, falling back to simple parser")
|
|
68
|
+
return None
|
|
69
|
+
|
|
70
|
+
def _flatten_yaml_dict(self, data: dict) -> dict[str, str]:
|
|
71
|
+
"""Convert YAML dict to string values."""
|
|
72
|
+
result: dict[str, str] = {}
|
|
73
|
+
for key, value in data.items():
|
|
74
|
+
result[str(key)] = self._convert_value(value)
|
|
75
|
+
return result
|
|
76
|
+
|
|
77
|
+
def _convert_value(self, value: object) -> str:
|
|
78
|
+
"""Convert a single YAML value to string."""
|
|
79
|
+
if isinstance(value, list):
|
|
80
|
+
return ", ".join(str(v) for v in value)
|
|
81
|
+
if value is not None:
|
|
82
|
+
return str(value)
|
|
83
|
+
return ""
|
|
84
|
+
|
|
85
|
+
def _parse_simple_yaml( # thailint: ignore[nesting,dry]
|
|
86
|
+
self, header: str
|
|
87
|
+
) -> dict[str, str]:
|
|
88
|
+
"""Simple regex-based YAML parsing fallback."""
|
|
89
|
+
fields: dict[str, str] = {}
|
|
90
|
+
current_field: str | None = None
|
|
91
|
+
current_value: list[str] = []
|
|
92
|
+
|
|
93
|
+
for line in header.split("\n"):
|
|
94
|
+
if self._is_field_start(line):
|
|
95
|
+
self._save_field(fields, current_field, current_value)
|
|
96
|
+
current_field, current_value = self._start_field(line)
|
|
97
|
+
elif current_field and line.strip():
|
|
98
|
+
current_value.append(self._process_continuation(line))
|
|
99
|
+
|
|
100
|
+
self._save_field(fields, current_field, current_value)
|
|
101
|
+
return fields
|
|
102
|
+
|
|
103
|
+
def _is_field_start(self, line: str) -> bool:
|
|
104
|
+
"""Check if line starts a new field (not indented, has colon)."""
|
|
105
|
+
return not line.startswith(" ") and ":" in line
|
|
106
|
+
|
|
107
|
+
def _start_field(self, line: str) -> tuple[str, list[str]]:
|
|
108
|
+
"""Parse field start and return field name and initial value."""
|
|
109
|
+
parts = line.split(":", 1)
|
|
110
|
+
field_name = parts[0].strip()
|
|
111
|
+
value = parts[1].strip() if len(parts) > 1 else ""
|
|
112
|
+
return field_name, [value] if value else []
|
|
113
|
+
|
|
114
|
+
def _process_continuation(self, line: str) -> str:
|
|
115
|
+
"""Process a continuation line (list item or multiline value)."""
|
|
116
|
+
stripped = line.strip()
|
|
117
|
+
return stripped[2:] if stripped.startswith("- ") else stripped
|
|
118
|
+
|
|
119
|
+
def _save_field(
|
|
120
|
+
self, fields: dict[str, str], field_name: str | None, values: list[str]
|
|
121
|
+
) -> None:
|
|
122
|
+
"""Save field to dictionary if field name exists."""
|
|
123
|
+
if field_name:
|
|
124
|
+
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
|
-
|
|
10
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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)
|
|
@@ -1,21 +1,22 @@
|
|
|
1
1
|
"""
|
|
2
|
-
File: src/linters/file_header/violation_builder.py
|
|
3
2
|
Purpose: Builds violation messages for file header linter
|
|
4
|
-
Exports: ViolationBuilder class
|
|
5
|
-
Depends: Violation type from core
|
|
6
|
-
Implements: Message templates with context-specific details
|
|
7
|
-
Related: linter.py for builder usage, atemporal_detector.py for temporal violations
|
|
8
3
|
|
|
9
|
-
|
|
10
|
-
|
|
4
|
+
Scope: Violation message creation for file header validation failures
|
|
5
|
+
|
|
6
|
+
Overview: Creates formatted violation messages for file header validation failures.
|
|
11
7
|
Handles missing fields, atemporal language, and other header issues with clear,
|
|
12
|
-
actionable messages. Provides consistent violation format across all validation types
|
|
8
|
+
actionable messages. Provides consistent violation format across all validation types
|
|
9
|
+
including rule_id, message, location, severity, and helpful suggestions. Supports
|
|
10
|
+
multiple violation types with appropriate error messages and remediation guidance.
|
|
11
|
+
|
|
12
|
+
Dependencies: Violation and Severity types from core.types module
|
|
13
|
+
|
|
14
|
+
Exports: ViolationBuilder class
|
|
13
15
|
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
violation = builder.build_missing_field("Purpose", "test.py", 1)
|
|
16
|
+
Interfaces: build_missing_field(field_name, file_path, line) -> Violation,
|
|
17
|
+
build_atemporal_violation(pattern, description, file_path, line) -> Violation
|
|
17
18
|
|
|
18
|
-
|
|
19
|
+
Implementation: Builder pattern with message templates for different violation types
|
|
19
20
|
"""
|
|
20
21
|
|
|
21
22
|
from src.core.types import Severity, Violation
|