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.
Files changed (37) hide show
  1. src/cli.py +236 -2
  2. src/core/cli_utils.py +16 -1
  3. src/core/registry.py +1 -1
  4. src/formatters/__init__.py +22 -0
  5. src/formatters/sarif.py +202 -0
  6. src/linter_config/loader.py +5 -4
  7. src/linters/dry/block_filter.py +11 -8
  8. src/linters/dry/cache.py +3 -2
  9. src/linters/dry/duplicate_storage.py +5 -4
  10. src/linters/dry/violation_generator.py +1 -1
  11. src/linters/file_header/atemporal_detector.py +11 -11
  12. src/linters/file_header/base_parser.py +89 -0
  13. src/linters/file_header/bash_parser.py +58 -0
  14. src/linters/file_header/config.py +76 -16
  15. src/linters/file_header/css_parser.py +70 -0
  16. src/linters/file_header/field_validator.py +35 -29
  17. src/linters/file_header/linter.py +113 -121
  18. src/linters/file_header/markdown_parser.py +124 -0
  19. src/linters/file_header/python_parser.py +14 -58
  20. src/linters/file_header/typescript_parser.py +73 -0
  21. src/linters/file_header/violation_builder.py +13 -12
  22. src/linters/file_placement/linter.py +9 -11
  23. src/linters/method_property/__init__.py +49 -0
  24. src/linters/method_property/config.py +135 -0
  25. src/linters/method_property/linter.py +419 -0
  26. src/linters/method_property/python_analyzer.py +472 -0
  27. src/linters/method_property/violation_builder.py +116 -0
  28. src/linters/print_statements/config.py +7 -12
  29. src/linters/print_statements/linter.py +13 -15
  30. src/linters/print_statements/python_analyzer.py +8 -14
  31. src/linters/print_statements/typescript_analyzer.py +9 -14
  32. src/linters/print_statements/violation_builder.py +12 -14
  33. {thailint-0.5.0.dist-info → thailint-0.8.0.dist-info}/METADATA +155 -3
  34. {thailint-0.5.0.dist-info → thailint-0.8.0.dist-info}/RECORD +37 -25
  35. {thailint-0.5.0.dist-info → thailint-0.8.0.dist-info}/WHEEL +0 -0
  36. {thailint-0.5.0.dist-info → thailint-0.8.0.dist-info}/entry_points.txt +0 -0
  37. {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
- 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)
@@ -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
- 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)
@@ -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
- Overview:
10
- Creates formatted violation messages for file header validation failures.
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
- Usage:
15
- builder = ViolationBuilder("file-header.validation")
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
- Notes: Follows standard violation format with rule_id, message, location, severity, suggestion
19
+ Implementation: Builder pattern with message templates for different violation types
19
20
  """
20
21
 
21
22
  from src.core.types import Severity, Violation