thailint 0.5.0__py3-none-any.whl → 0.7.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 +118 -1
- 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/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/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.7.0.dist-info}/METADATA +148 -2
- {thailint-0.5.0.dist-info → thailint-0.7.0.dist-info}/RECORD +27 -20
- {thailint-0.5.0.dist-info → thailint-0.7.0.dist-info}/WHEEL +0 -0
- {thailint-0.5.0.dist-info → thailint-0.7.0.dist-info}/entry_points.txt +0 -0
- {thailint-0.5.0.dist-info → thailint-0.7.0.dist-info}/licenses/LICENSE +0 -0
|
@@ -1,21 +1,22 @@
|
|
|
1
1
|
"""
|
|
2
|
-
File: src/linters/file_header/config.py
|
|
3
2
|
Purpose: Configuration model for file header linter
|
|
4
|
-
Exports: FileHeaderConfig dataclass
|
|
5
|
-
Depends: dataclasses, pathlib
|
|
6
|
-
Implements: Configuration with validation and defaults
|
|
7
|
-
Related: linter.py for configuration usage
|
|
8
3
|
|
|
9
|
-
|
|
10
|
-
|
|
4
|
+
Scope: File header linter configuration for all supported languages
|
|
5
|
+
|
|
6
|
+
Overview: Defines configuration structure for file header linter including required fields
|
|
11
7
|
per language, ignore patterns, and validation options. Provides defaults matching
|
|
12
|
-
|
|
8
|
+
FILE_HEADER_STANDARDS.md requirements and supports loading from .thailint.yaml configuration.
|
|
9
|
+
Supports Python, TypeScript/JavaScript, Bash, Markdown, and CSS file types with
|
|
10
|
+
language-specific required field sets. Includes atemporal language enforcement
|
|
11
|
+
configuration and file ignore patterns.
|
|
12
|
+
|
|
13
|
+
Dependencies: dataclasses module for configuration structure
|
|
14
|
+
|
|
15
|
+
Exports: FileHeaderConfig dataclass
|
|
13
16
|
|
|
14
|
-
|
|
15
|
-
config = FileHeaderConfig()
|
|
16
|
-
config = FileHeaderConfig.from_dict(config_dict, "python")
|
|
17
|
+
Interfaces: from_dict(config_dict, language) -> FileHeaderConfig for configuration loading
|
|
17
18
|
|
|
18
|
-
|
|
19
|
+
Implementation: Dataclass with language-specific field lists and factory method for config loading
|
|
19
20
|
"""
|
|
20
21
|
|
|
21
22
|
from dataclasses import dataclass, field
|
|
@@ -25,7 +26,7 @@ from dataclasses import dataclass, field
|
|
|
25
26
|
class FileHeaderConfig:
|
|
26
27
|
"""Configuration for file header linting."""
|
|
27
28
|
|
|
28
|
-
# Required fields by language
|
|
29
|
+
# Required fields by language - Python
|
|
29
30
|
required_fields_python: list[str] = field(
|
|
30
31
|
default_factory=lambda: [
|
|
31
32
|
"Purpose",
|
|
@@ -38,6 +39,56 @@ class FileHeaderConfig:
|
|
|
38
39
|
]
|
|
39
40
|
)
|
|
40
41
|
|
|
42
|
+
# Required fields by language - TypeScript/JavaScript
|
|
43
|
+
required_fields_typescript: list[str] = field(
|
|
44
|
+
default_factory=lambda: [
|
|
45
|
+
"Purpose",
|
|
46
|
+
"Scope",
|
|
47
|
+
"Overview",
|
|
48
|
+
"Dependencies",
|
|
49
|
+
"Exports",
|
|
50
|
+
"Props/Interfaces",
|
|
51
|
+
"State/Behavior",
|
|
52
|
+
]
|
|
53
|
+
)
|
|
54
|
+
|
|
55
|
+
# Required fields by language - Bash
|
|
56
|
+
required_fields_bash: list[str] = field(
|
|
57
|
+
default_factory=lambda: [
|
|
58
|
+
"Purpose",
|
|
59
|
+
"Scope",
|
|
60
|
+
"Overview",
|
|
61
|
+
"Dependencies",
|
|
62
|
+
"Exports",
|
|
63
|
+
"Usage",
|
|
64
|
+
"Environment",
|
|
65
|
+
]
|
|
66
|
+
)
|
|
67
|
+
|
|
68
|
+
# Required fields by language - Markdown (lowercase for YAML frontmatter)
|
|
69
|
+
required_fields_markdown: list[str] = field(
|
|
70
|
+
default_factory=lambda: [
|
|
71
|
+
"purpose",
|
|
72
|
+
"scope",
|
|
73
|
+
"overview",
|
|
74
|
+
"audience",
|
|
75
|
+
"status",
|
|
76
|
+
]
|
|
77
|
+
)
|
|
78
|
+
|
|
79
|
+
# Required fields by language - CSS
|
|
80
|
+
required_fields_css: list[str] = field(
|
|
81
|
+
default_factory=lambda: [
|
|
82
|
+
"Purpose",
|
|
83
|
+
"Scope",
|
|
84
|
+
"Overview",
|
|
85
|
+
"Dependencies",
|
|
86
|
+
"Exports",
|
|
87
|
+
"Interfaces",
|
|
88
|
+
"Environment",
|
|
89
|
+
]
|
|
90
|
+
)
|
|
91
|
+
|
|
41
92
|
# Enforce atemporal language checking
|
|
42
93
|
enforce_atemporal: bool = True
|
|
43
94
|
|
|
@@ -57,10 +108,19 @@ class FileHeaderConfig:
|
|
|
57
108
|
Returns:
|
|
58
109
|
FileHeaderConfig instance with values from dictionary
|
|
59
110
|
"""
|
|
111
|
+
defaults = cls()
|
|
112
|
+
required_fields = config_dict.get("required_fields", {})
|
|
113
|
+
|
|
60
114
|
return cls(
|
|
61
|
-
required_fields_python=
|
|
62
|
-
|
|
115
|
+
required_fields_python=required_fields.get("python", defaults.required_fields_python),
|
|
116
|
+
required_fields_typescript=required_fields.get(
|
|
117
|
+
"typescript", defaults.required_fields_typescript
|
|
118
|
+
),
|
|
119
|
+
required_fields_bash=required_fields.get("bash", defaults.required_fields_bash),
|
|
120
|
+
required_fields_markdown=required_fields.get(
|
|
121
|
+
"markdown", defaults.required_fields_markdown
|
|
63
122
|
),
|
|
123
|
+
required_fields_css=required_fields.get("css", defaults.required_fields_css),
|
|
64
124
|
enforce_atemporal=config_dict.get("enforce_atemporal", True),
|
|
65
|
-
ignore=config_dict.get("ignore",
|
|
125
|
+
ignore=config_dict.get("ignore", defaults.ignore),
|
|
66
126
|
)
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Purpose: CSS block comment header extraction and parsing
|
|
3
|
+
|
|
4
|
+
Scope: CSS and SCSS file header parsing
|
|
5
|
+
|
|
6
|
+
Overview: Extracts JSDoc-style block comments (/** ... */) from CSS and SCSS files.
|
|
7
|
+
Handles @charset declarations by allowing them before the header comment.
|
|
8
|
+
Parses structured header fields from comment content and cleans formatting
|
|
9
|
+
characters. Requires JSDoc-style comment (/**) not regular block comment (/*).
|
|
10
|
+
Processes multi-line comments and removes leading asterisks from content.
|
|
11
|
+
|
|
12
|
+
Dependencies: re module for regex pattern matching, base_parser.BaseHeaderParser for field parsing
|
|
13
|
+
|
|
14
|
+
Exports: CssHeaderParser class
|
|
15
|
+
|
|
16
|
+
Interfaces: extract_header(code) -> str | None for JSDoc comment extraction, parse_fields(header) inherited from base
|
|
17
|
+
|
|
18
|
+
Implementation: Regex-based JSDoc comment 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 CssHeaderParser(BaseHeaderParser):
|
|
27
|
+
"""Extracts and parses CSS file headers from block comments."""
|
|
28
|
+
|
|
29
|
+
# Pattern to match JSDoc-style comment, allowing @charset before
|
|
30
|
+
JSDOC_PATTERN = re.compile(r'^(?:@charset\s+"[^"]+"\s*;\s*)?\s*/\*\*\s*(.*?)\s*\*/', re.DOTALL)
|
|
31
|
+
|
|
32
|
+
def extract_header(self, code: str) -> str | None:
|
|
33
|
+
"""Extract JSDoc-style comment from CSS code.
|
|
34
|
+
|
|
35
|
+
Args:
|
|
36
|
+
code: CSS/SCSS source code
|
|
37
|
+
|
|
38
|
+
Returns:
|
|
39
|
+
Comment 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 and clean the content
|
|
49
|
+
comment_content = match.group(1)
|
|
50
|
+
return self._clean_comment_content(comment_content)
|
|
51
|
+
|
|
52
|
+
def _clean_comment_content(self, content: str) -> str:
|
|
53
|
+
"""Remove comment formatting (leading asterisks) from content.
|
|
54
|
+
|
|
55
|
+
Args:
|
|
56
|
+
content: Raw comment content
|
|
57
|
+
|
|
58
|
+
Returns:
|
|
59
|
+
Cleaned content without leading asterisks
|
|
60
|
+
"""
|
|
61
|
+
lines = content.split("\n")
|
|
62
|
+
cleaned_lines = []
|
|
63
|
+
|
|
64
|
+
for line in lines:
|
|
65
|
+
stripped = line.strip()
|
|
66
|
+
if stripped.startswith("*"):
|
|
67
|
+
stripped = stripped[1:].strip()
|
|
68
|
+
cleaned_lines.append(stripped)
|
|
69
|
+
|
|
70
|
+
return "\n".join(cleaned_lines)
|
|
@@ -1,21 +1,21 @@
|
|
|
1
1
|
"""
|
|
2
|
-
File: src/linters/file_header/field_validator.py
|
|
3
2
|
Purpose: Validates mandatory fields in file headers
|
|
4
|
-
Exports: FieldValidator class
|
|
5
|
-
Depends: FileHeaderConfig for field requirements
|
|
6
|
-
Implements: Configuration-driven validation with field presence checking
|
|
7
|
-
Related: linter.py for validator usage, config.py for configuration
|
|
8
3
|
|
|
9
|
-
|
|
10
|
-
|
|
4
|
+
Scope: File header field validation for all supported languages
|
|
5
|
+
|
|
6
|
+
Overview: Validates presence and quality of mandatory header fields. Checks that all
|
|
11
7
|
required fields are present, non-empty, and meet minimum content requirements.
|
|
12
|
-
Supports language-specific required fields and provides detailed violation messages
|
|
8
|
+
Supports language-specific required fields and provides detailed violation messages
|
|
9
|
+
for missing or empty fields. Uses configuration-driven validation to support
|
|
10
|
+
different field requirements per language type.
|
|
11
|
+
|
|
12
|
+
Dependencies: FileHeaderConfig for language-specific field requirements
|
|
13
13
|
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
14
|
+
Exports: FieldValidator class
|
|
15
|
+
|
|
16
|
+
Interfaces: validate_fields(fields, language) -> list[tuple[str, str]] returns field violations
|
|
17
17
|
|
|
18
|
-
|
|
18
|
+
Implementation: Configuration-driven validation with field presence and emptiness checking
|
|
19
19
|
"""
|
|
20
20
|
|
|
21
21
|
from .config import FileHeaderConfig
|
|
@@ -32,9 +32,7 @@ class FieldValidator:
|
|
|
32
32
|
"""
|
|
33
33
|
self.config = config
|
|
34
34
|
|
|
35
|
-
def validate_fields(
|
|
36
|
-
self, fields: dict[str, str], language: str
|
|
37
|
-
) -> list[tuple[str, str]]:
|
|
35
|
+
def validate_fields(self, fields: dict[str, str], language: str) -> list[tuple[str, str]]:
|
|
38
36
|
"""Validate all required fields are present.
|
|
39
37
|
|
|
40
38
|
Args:
|
|
@@ -48,22 +46,30 @@ class FieldValidator:
|
|
|
48
46
|
required_fields = self._get_required_fields(language)
|
|
49
47
|
|
|
50
48
|
for field_name in required_fields:
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
violations.append((field_name, f"Empty mandatory field: {field_name}"))
|
|
49
|
+
error = self._check_field(fields, field_name)
|
|
50
|
+
if error:
|
|
51
|
+
violations.append(error)
|
|
55
52
|
|
|
56
53
|
return violations
|
|
57
54
|
|
|
58
|
-
def
|
|
59
|
-
"""
|
|
55
|
+
def _check_field(self, fields: dict[str, str], field_name: str) -> tuple[str, str] | None:
|
|
56
|
+
"""Check a single field for presence and content."""
|
|
57
|
+
if field_name not in fields:
|
|
58
|
+
return (field_name, f"Missing mandatory field: {field_name}")
|
|
60
59
|
|
|
61
|
-
|
|
62
|
-
|
|
60
|
+
if not fields[field_name] or not fields[field_name].strip():
|
|
61
|
+
return (field_name, f"Empty mandatory field: {field_name}")
|
|
63
62
|
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
63
|
+
return None
|
|
64
|
+
|
|
65
|
+
def _get_required_fields(self, language: str) -> list[str]:
|
|
66
|
+
"""Get required fields for language using dictionary lookup."""
|
|
67
|
+
language_fields = {
|
|
68
|
+
"python": self.config.required_fields_python,
|
|
69
|
+
"typescript": self.config.required_fields_typescript,
|
|
70
|
+
"javascript": self.config.required_fields_typescript,
|
|
71
|
+
"bash": self.config.required_fields_bash,
|
|
72
|
+
"markdown": self.config.required_fields_markdown,
|
|
73
|
+
"css": self.config.required_fields_css,
|
|
74
|
+
}
|
|
75
|
+
return language_fields.get(language, [])
|
|
@@ -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)
|