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.
@@ -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
- Overview:
10
- Defines configuration structure for file header linter including required fields
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
- ai-doc-standard.md requirements and supports loading from .thailint.yaml configuration.
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
- Usage:
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
- Notes: Dataclass with validation and language-specific defaults
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=config_dict.get("required_fields", {}).get(
62
- "python", cls().required_fields_python
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", cls().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
- Overview:
10
- Validates presence and quality of mandatory header fields. Checks that all
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
- Usage:
15
- validator = FieldValidator(config)
16
- violations = validator.validate_fields(fields, "python")
14
+ Exports: FieldValidator class
15
+
16
+ Interfaces: validate_fields(fields, language) -> list[tuple[str, str]] returns field violations
17
17
 
18
- Notes: Language-specific field requirements defined in config
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( # thailint: ignore[nesting]
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
- if field_name not in fields:
52
- violations.append((field_name, f"Missing mandatory field: {field_name}"))
53
- elif not fields[field_name] or len(fields[field_name].strip()) == 0:
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 _get_required_fields(self, language: str) -> list[str]:
59
- """Get required fields for language.
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
- Args:
62
- language: Programming language
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
- Returns:
65
- List of required field names for the language
66
- """
67
- if language == "python":
68
- return self.config.required_fields_python
69
- return [] # Other languages in PR5
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
- 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
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
- # Extract and validate header
102
- return self._check_python_header(context, config)
103
+ return self._check_language_header(context, config)
103
104
 
104
- def _has_file_ignore(self, context: BaseLintContext) -> bool:
105
- """Check if file has file-level ignore directive.
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
- Args:
108
- context: Lint context
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
- Returns:
111
- True if file has ignore-file directive
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] # Convert to 0-indexed
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)