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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (214) hide show
  1. src/__init__.py +1 -0
  2. src/analyzers/__init__.py +4 -3
  3. src/analyzers/ast_utils.py +54 -0
  4. src/analyzers/rust_base.py +155 -0
  5. src/analyzers/rust_context.py +141 -0
  6. src/analyzers/typescript_base.py +4 -0
  7. src/cli/__init__.py +30 -0
  8. src/cli/__main__.py +22 -0
  9. src/cli/config.py +480 -0
  10. src/cli/config_merge.py +241 -0
  11. src/cli/linters/__init__.py +67 -0
  12. src/cli/linters/code_patterns.py +270 -0
  13. src/cli/linters/code_smells.py +342 -0
  14. src/cli/linters/documentation.py +83 -0
  15. src/cli/linters/performance.py +287 -0
  16. src/cli/linters/shared.py +331 -0
  17. src/cli/linters/structure.py +327 -0
  18. src/cli/linters/structure_quality.py +328 -0
  19. src/cli/main.py +120 -0
  20. src/cli/utils.py +395 -0
  21. src/cli_main.py +37 -0
  22. src/config.py +44 -27
  23. src/core/base.py +95 -5
  24. src/core/cli_utils.py +19 -2
  25. src/core/config_parser.py +36 -6
  26. src/core/constants.py +54 -0
  27. src/core/linter_utils.py +95 -6
  28. src/core/python_lint_rule.py +101 -0
  29. src/core/registry.py +1 -1
  30. src/core/rule_discovery.py +147 -84
  31. src/core/types.py +13 -0
  32. src/core/violation_builder.py +78 -15
  33. src/core/violation_utils.py +69 -0
  34. src/formatters/__init__.py +22 -0
  35. src/formatters/sarif.py +202 -0
  36. src/linter_config/directive_markers.py +109 -0
  37. src/linter_config/ignore.py +254 -395
  38. src/linter_config/loader.py +45 -12
  39. src/linter_config/pattern_utils.py +65 -0
  40. src/linter_config/rule_matcher.py +89 -0
  41. src/linters/collection_pipeline/__init__.py +90 -0
  42. src/linters/collection_pipeline/any_all_analyzer.py +281 -0
  43. src/linters/collection_pipeline/ast_utils.py +40 -0
  44. src/linters/collection_pipeline/config.py +75 -0
  45. src/linters/collection_pipeline/continue_analyzer.py +94 -0
  46. src/linters/collection_pipeline/detector.py +360 -0
  47. src/linters/collection_pipeline/filter_map_analyzer.py +402 -0
  48. src/linters/collection_pipeline/linter.py +420 -0
  49. src/linters/collection_pipeline/suggestion_builder.py +130 -0
  50. src/linters/cqs/__init__.py +54 -0
  51. src/linters/cqs/config.py +55 -0
  52. src/linters/cqs/function_analyzer.py +201 -0
  53. src/linters/cqs/input_detector.py +139 -0
  54. src/linters/cqs/linter.py +159 -0
  55. src/linters/cqs/output_detector.py +84 -0
  56. src/linters/cqs/python_analyzer.py +54 -0
  57. src/linters/cqs/types.py +82 -0
  58. src/linters/cqs/typescript_cqs_analyzer.py +61 -0
  59. src/linters/cqs/typescript_function_analyzer.py +192 -0
  60. src/linters/cqs/typescript_input_detector.py +203 -0
  61. src/linters/cqs/typescript_output_detector.py +117 -0
  62. src/linters/cqs/violation_builder.py +94 -0
  63. src/linters/dry/base_token_analyzer.py +16 -9
  64. src/linters/dry/block_filter.py +125 -22
  65. src/linters/dry/block_grouper.py +4 -0
  66. src/linters/dry/cache.py +142 -94
  67. src/linters/dry/cache_query.py +4 -0
  68. src/linters/dry/config.py +68 -21
  69. src/linters/dry/constant.py +92 -0
  70. src/linters/dry/constant_matcher.py +223 -0
  71. src/linters/dry/constant_violation_builder.py +98 -0
  72. src/linters/dry/duplicate_storage.py +20 -82
  73. src/linters/dry/file_analyzer.py +15 -50
  74. src/linters/dry/inline_ignore.py +7 -16
  75. src/linters/dry/linter.py +182 -54
  76. src/linters/dry/python_analyzer.py +108 -336
  77. src/linters/dry/python_constant_extractor.py +100 -0
  78. src/linters/dry/single_statement_detector.py +417 -0
  79. src/linters/dry/storage_initializer.py +9 -18
  80. src/linters/dry/token_hasher.py +129 -71
  81. src/linters/dry/typescript_analyzer.py +68 -380
  82. src/linters/dry/typescript_constant_extractor.py +138 -0
  83. src/linters/dry/typescript_statement_detector.py +255 -0
  84. src/linters/dry/typescript_value_extractor.py +70 -0
  85. src/linters/dry/violation_builder.py +4 -0
  86. src/linters/dry/violation_filter.py +9 -5
  87. src/linters/dry/violation_generator.py +71 -14
  88. src/linters/file_header/__init__.py +24 -0
  89. src/linters/file_header/atemporal_detector.py +105 -0
  90. src/linters/file_header/base_parser.py +93 -0
  91. src/linters/file_header/bash_parser.py +66 -0
  92. src/linters/file_header/config.py +140 -0
  93. src/linters/file_header/css_parser.py +70 -0
  94. src/linters/file_header/field_validator.py +72 -0
  95. src/linters/file_header/linter.py +309 -0
  96. src/linters/file_header/markdown_parser.py +130 -0
  97. src/linters/file_header/python_parser.py +42 -0
  98. src/linters/file_header/typescript_parser.py +73 -0
  99. src/linters/file_header/violation_builder.py +79 -0
  100. src/linters/file_placement/config_loader.py +3 -1
  101. src/linters/file_placement/directory_matcher.py +4 -0
  102. src/linters/file_placement/linter.py +74 -31
  103. src/linters/file_placement/pattern_matcher.py +41 -6
  104. src/linters/file_placement/pattern_validator.py +31 -12
  105. src/linters/file_placement/rule_checker.py +12 -7
  106. src/linters/lazy_ignores/__init__.py +43 -0
  107. src/linters/lazy_ignores/config.py +74 -0
  108. src/linters/lazy_ignores/directive_utils.py +164 -0
  109. src/linters/lazy_ignores/header_parser.py +177 -0
  110. src/linters/lazy_ignores/linter.py +158 -0
  111. src/linters/lazy_ignores/matcher.py +168 -0
  112. src/linters/lazy_ignores/python_analyzer.py +209 -0
  113. src/linters/lazy_ignores/rule_id_utils.py +180 -0
  114. src/linters/lazy_ignores/skip_detector.py +298 -0
  115. src/linters/lazy_ignores/types.py +71 -0
  116. src/linters/lazy_ignores/typescript_analyzer.py +146 -0
  117. src/linters/lazy_ignores/violation_builder.py +135 -0
  118. src/linters/lbyl/__init__.py +31 -0
  119. src/linters/lbyl/config.py +63 -0
  120. src/linters/lbyl/linter.py +67 -0
  121. src/linters/lbyl/pattern_detectors/__init__.py +53 -0
  122. src/linters/lbyl/pattern_detectors/base.py +63 -0
  123. src/linters/lbyl/pattern_detectors/dict_key_detector.py +107 -0
  124. src/linters/lbyl/pattern_detectors/division_check_detector.py +232 -0
  125. src/linters/lbyl/pattern_detectors/file_exists_detector.py +220 -0
  126. src/linters/lbyl/pattern_detectors/hasattr_detector.py +119 -0
  127. src/linters/lbyl/pattern_detectors/isinstance_detector.py +119 -0
  128. src/linters/lbyl/pattern_detectors/len_check_detector.py +173 -0
  129. src/linters/lbyl/pattern_detectors/none_check_detector.py +146 -0
  130. src/linters/lbyl/pattern_detectors/string_validator_detector.py +145 -0
  131. src/linters/lbyl/python_analyzer.py +215 -0
  132. src/linters/lbyl/violation_builder.py +354 -0
  133. src/linters/magic_numbers/__init__.py +48 -0
  134. src/linters/magic_numbers/config.py +82 -0
  135. src/linters/magic_numbers/context_analyzer.py +249 -0
  136. src/linters/magic_numbers/linter.py +462 -0
  137. src/linters/magic_numbers/python_analyzer.py +64 -0
  138. src/linters/magic_numbers/typescript_analyzer.py +215 -0
  139. src/linters/magic_numbers/typescript_ignore_checker.py +81 -0
  140. src/linters/magic_numbers/violation_builder.py +98 -0
  141. src/linters/method_property/__init__.py +49 -0
  142. src/linters/method_property/config.py +138 -0
  143. src/linters/method_property/linter.py +414 -0
  144. src/linters/method_property/python_analyzer.py +473 -0
  145. src/linters/method_property/violation_builder.py +119 -0
  146. src/linters/nesting/__init__.py +6 -2
  147. src/linters/nesting/config.py +6 -3
  148. src/linters/nesting/linter.py +31 -34
  149. src/linters/nesting/python_analyzer.py +4 -0
  150. src/linters/nesting/typescript_analyzer.py +6 -11
  151. src/linters/nesting/violation_builder.py +1 -0
  152. src/linters/performance/__init__.py +91 -0
  153. src/linters/performance/config.py +43 -0
  154. src/linters/performance/constants.py +49 -0
  155. src/linters/performance/linter.py +149 -0
  156. src/linters/performance/python_analyzer.py +365 -0
  157. src/linters/performance/regex_analyzer.py +312 -0
  158. src/linters/performance/regex_linter.py +139 -0
  159. src/linters/performance/typescript_analyzer.py +236 -0
  160. src/linters/performance/violation_builder.py +160 -0
  161. src/linters/print_statements/__init__.py +53 -0
  162. src/linters/print_statements/config.py +78 -0
  163. src/linters/print_statements/linter.py +413 -0
  164. src/linters/print_statements/python_analyzer.py +153 -0
  165. src/linters/print_statements/typescript_analyzer.py +125 -0
  166. src/linters/print_statements/violation_builder.py +96 -0
  167. src/linters/srp/__init__.py +3 -3
  168. src/linters/srp/class_analyzer.py +11 -7
  169. src/linters/srp/config.py +12 -6
  170. src/linters/srp/heuristics.py +56 -22
  171. src/linters/srp/linter.py +47 -39
  172. src/linters/srp/python_analyzer.py +55 -20
  173. src/linters/srp/typescript_metrics_calculator.py +110 -50
  174. src/linters/stateless_class/__init__.py +25 -0
  175. src/linters/stateless_class/config.py +58 -0
  176. src/linters/stateless_class/linter.py +349 -0
  177. src/linters/stateless_class/python_analyzer.py +290 -0
  178. src/linters/stringly_typed/__init__.py +36 -0
  179. src/linters/stringly_typed/config.py +189 -0
  180. src/linters/stringly_typed/context_filter.py +451 -0
  181. src/linters/stringly_typed/function_call_violation_builder.py +135 -0
  182. src/linters/stringly_typed/ignore_checker.py +100 -0
  183. src/linters/stringly_typed/ignore_utils.py +51 -0
  184. src/linters/stringly_typed/linter.py +376 -0
  185. src/linters/stringly_typed/python/__init__.py +33 -0
  186. src/linters/stringly_typed/python/analyzer.py +348 -0
  187. src/linters/stringly_typed/python/call_tracker.py +175 -0
  188. src/linters/stringly_typed/python/comparison_tracker.py +257 -0
  189. src/linters/stringly_typed/python/condition_extractor.py +134 -0
  190. src/linters/stringly_typed/python/conditional_detector.py +179 -0
  191. src/linters/stringly_typed/python/constants.py +21 -0
  192. src/linters/stringly_typed/python/match_analyzer.py +94 -0
  193. src/linters/stringly_typed/python/validation_detector.py +189 -0
  194. src/linters/stringly_typed/python/variable_extractor.py +96 -0
  195. src/linters/stringly_typed/storage.py +620 -0
  196. src/linters/stringly_typed/storage_initializer.py +45 -0
  197. src/linters/stringly_typed/typescript/__init__.py +28 -0
  198. src/linters/stringly_typed/typescript/analyzer.py +157 -0
  199. src/linters/stringly_typed/typescript/call_tracker.py +335 -0
  200. src/linters/stringly_typed/typescript/comparison_tracker.py +378 -0
  201. src/linters/stringly_typed/violation_generator.py +419 -0
  202. src/orchestrator/core.py +264 -16
  203. src/orchestrator/language_detector.py +5 -3
  204. src/templates/thailint_config_template.yaml +354 -0
  205. src/utils/project_root.py +138 -16
  206. thailint-0.15.3.dist-info/METADATA +187 -0
  207. thailint-0.15.3.dist-info/RECORD +226 -0
  208. {thailint-0.2.0.dist-info → thailint-0.15.3.dist-info}/WHEEL +1 -1
  209. thailint-0.15.3.dist-info/entry_points.txt +4 -0
  210. src/cli.py +0 -1055
  211. thailint-0.2.0.dist-info/METADATA +0 -980
  212. thailint-0.2.0.dist-info/RECORD +0 -75
  213. thailint-0.2.0.dist-info/entry_points.txt +0 -4
  214. {thailint-0.2.0.dist-info → thailint-0.15.3.dist-info/licenses}/LICENSE +0 -0
@@ -0,0 +1,93 @@
1
+ """
2
+ Purpose: Base class for file header parsers with common field parsing logic
3
+
4
+ Scope: File header parsing infrastructure for all language-specific parsers
5
+
6
+ Overview: Provides common field parsing functionality shared across all language-specific
7
+ header parsers. Implements the parse_fields method and helper methods for
8
+ detecting field lines and saving fields. Uses template method pattern where subclasses
9
+ implement extract_header for language-specific header extraction while this base class
10
+ handles field parsing logic. Supports multi-line field values and field continuation.
11
+
12
+ Dependencies: re module for field pattern matching, abc module for abstract base class
13
+
14
+ Exports: BaseHeaderParser abstract base class
15
+
16
+ Interfaces: extract_header(code) abstract method, parse_fields(header) -> dict[str, str] for field extraction
17
+
18
+ Implementation: Template method pattern with shared field parsing and language-specific extraction
19
+
20
+ Suppressions:
21
+ - nesting: parse_fields uses nested loops for multi-line field processing. Extracting
22
+ would fragment the field-building logic without improving clarity.
23
+ """
24
+
25
+ import re
26
+ from abc import ABC, abstractmethod
27
+
28
+
29
+ class BaseHeaderParser(ABC):
30
+ """Base class for file header parsers with common field parsing logic."""
31
+
32
+ # Pattern to match field names (word characters and /)
33
+ FIELD_NAME_PATTERN = re.compile(r"^[\w/]+$")
34
+
35
+ @abstractmethod
36
+ def extract_header(self, code: str) -> str | None:
37
+ """Extract header from source code.
38
+
39
+ Args:
40
+ code: Source code
41
+
42
+ Returns:
43
+ Header content or None if not found
44
+ """
45
+
46
+ def parse_fields(self, header: str) -> dict[str, str]: # thailint: ignore[nesting]
47
+ """Parse structured fields from header text.
48
+
49
+ Args:
50
+ header: Header text
51
+
52
+ Returns:
53
+ Dictionary mapping field_name -> field_value
54
+ """
55
+ fields: dict[str, str] = {}
56
+ current_field: str | None = None
57
+ current_value: list[str] = []
58
+
59
+ for line in header.split("\n"):
60
+ if self._is_field_line(line):
61
+ self._save_field(fields, current_field, current_value)
62
+ current_field, current_value = self._start_new_field(line)
63
+ elif current_field:
64
+ current_value.append(line.strip())
65
+
66
+ self._save_field(fields, current_field, current_value)
67
+ return fields
68
+
69
+ def _is_field_line(self, line: str) -> bool:
70
+ """Check if line starts a new field."""
71
+ if ":" not in line:
72
+ return False
73
+
74
+ colon_pos = line.find(":")
75
+ if colon_pos <= 0:
76
+ return False
77
+
78
+ field_name = line[:colon_pos].strip()
79
+ return bool(self.FIELD_NAME_PATTERN.match(field_name))
80
+
81
+ def _start_new_field(self, line: str) -> tuple[str, list[str]]:
82
+ """Parse a field line and start tracking its value."""
83
+ parts = line.split(":", 1)
84
+ field_name = parts[0].strip()
85
+ initial_value = parts[1].strip() if len(parts) > 1 else ""
86
+ return field_name, [initial_value] if initial_value else []
87
+
88
+ def _save_field(
89
+ self, fields: dict[str, str], field_name: str | None, value_lines: list[str]
90
+ ) -> None:
91
+ """Save accumulated field value to fields dict."""
92
+ if field_name:
93
+ fields[field_name] = "\n".join(value_lines).strip()
@@ -0,0 +1,66 @@
1
+ """
2
+ Purpose: Bash shell script comment header extraction and parsing
3
+
4
+ Scope: Bash and shell script file header parsing
5
+
6
+ Overview: Extracts hash comment headers from Bash shell scripts. Handles shebang lines
7
+ (#!/bin/bash, #!/usr/bin/env bash, etc.) by skipping them and extracting the
8
+ comment block that follows. Parses structured header fields from comment content.
9
+ Extracts contiguous comment blocks from the start of the file and processes them
10
+ into structured fields for validation.
11
+
12
+ Dependencies: base_parser.BaseHeaderParser for common field parsing functionality
13
+
14
+ Exports: BashHeaderParser class
15
+
16
+ Interfaces: extract_header(code) -> str | None for comment extraction, parse_fields(header) inherited from base
17
+
18
+ Implementation: Skips shebang and preamble, then extracts contiguous hash comment block
19
+
20
+ Suppressions:
21
+ - nesting: _skip_preamble uses conditional loops for shebang/preamble detection.
22
+ Sequential line processing requires nested state checks.
23
+ """
24
+
25
+ from src.linters.file_header.base_parser import BaseHeaderParser
26
+
27
+
28
+ class BashHeaderParser(BaseHeaderParser):
29
+ """Extracts and parses Bash file headers from comment blocks."""
30
+
31
+ def __init__(self) -> None:
32
+ """Initialize the Bash header parser."""
33
+ pass # BaseHeaderParser has no __init__, but we need this for stateless-class
34
+
35
+ def extract_header(self, code: str) -> str | None:
36
+ """Extract comment header from Bash script."""
37
+ if not code or not code.strip():
38
+ return None
39
+
40
+ lines = self._skip_preamble(code.split("\n"))
41
+ header_lines = self._extract_comment_block(lines)
42
+
43
+ return "\n".join(header_lines) if header_lines else None
44
+
45
+ def _skip_preamble(self, lines: list[str]) -> list[str]: # thailint: ignore[nesting]
46
+ """Skip shebang and leading empty lines."""
47
+ result = []
48
+ skipping = True
49
+ for line in lines:
50
+ stripped = line.strip()
51
+ if skipping:
52
+ if stripped.startswith("#!") or not stripped:
53
+ continue
54
+ skipping = False
55
+ result.append(stripped)
56
+ return result
57
+
58
+ def _extract_comment_block(self, lines: list[str]) -> list[str]:
59
+ """Extract contiguous comment lines from start of input."""
60
+ result = []
61
+ for line in lines:
62
+ if line.startswith("#"):
63
+ result.append(line[1:].strip())
64
+ else:
65
+ break
66
+ return result
@@ -0,0 +1,140 @@
1
+ """
2
+ Purpose: Configuration model for file header linter
3
+
4
+ Scope: File header linter configuration for all supported languages
5
+
6
+ Overview: Defines configuration structure for file header linter including required fields
7
+ per language, ignore patterns, and validation options. Provides defaults matching
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
16
+
17
+ Interfaces: from_dict(config_dict, language) -> FileHeaderConfig for configuration loading
18
+
19
+ Implementation: Dataclass with language-specific field lists and factory method for config loading
20
+ """
21
+
22
+ from dataclasses import dataclass, field
23
+
24
+
25
+ @dataclass
26
+ class FileHeaderConfig:
27
+ """Configuration for file header linting."""
28
+
29
+ # Required fields by language - Python
30
+ required_fields_python: list[str] = field(
31
+ default_factory=lambda: [
32
+ "Purpose",
33
+ "Scope",
34
+ "Overview",
35
+ "Dependencies",
36
+ "Exports",
37
+ "Interfaces",
38
+ "Implementation",
39
+ ]
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
+
92
+ # Enforce atemporal language checking
93
+ enforce_atemporal: bool = True
94
+
95
+ # Patterns to ignore (file paths)
96
+ ignore: list[str] = field(
97
+ default_factory=lambda: ["test/**", "**/migrations/**", "**/__init__.py"]
98
+ )
99
+
100
+ @classmethod
101
+ def from_dict(cls, config_dict: dict, language: str) -> "FileHeaderConfig":
102
+ """Create config from dictionary.
103
+
104
+ Args:
105
+ config_dict: Dictionary of configuration values
106
+ language: Programming language for language-specific config
107
+
108
+ Returns:
109
+ FileHeaderConfig instance with values from dictionary
110
+ """
111
+ defaults = cls()
112
+ required_fields = config_dict.get("required_fields", {})
113
+
114
+ # Handle both list format (applies to all languages) and dict format (language-specific)
115
+ if isinstance(required_fields, list):
116
+ # Simple list format: apply same fields to all languages
117
+ return cls(
118
+ required_fields_python=required_fields,
119
+ required_fields_typescript=required_fields,
120
+ required_fields_bash=required_fields,
121
+ required_fields_markdown=required_fields,
122
+ required_fields_css=required_fields,
123
+ enforce_atemporal=config_dict.get("enforce_atemporal", True),
124
+ ignore=config_dict.get("ignore", defaults.ignore),
125
+ )
126
+
127
+ # Dict format: language-specific fields
128
+ return cls(
129
+ required_fields_python=required_fields.get("python", defaults.required_fields_python),
130
+ required_fields_typescript=required_fields.get(
131
+ "typescript", defaults.required_fields_typescript
132
+ ),
133
+ required_fields_bash=required_fields.get("bash", defaults.required_fields_bash),
134
+ required_fields_markdown=required_fields.get(
135
+ "markdown", defaults.required_fields_markdown
136
+ ),
137
+ required_fields_css=required_fields.get("css", defaults.required_fields_css),
138
+ enforce_atemporal=config_dict.get("enforce_atemporal", True),
139
+ ignore=config_dict.get("ignore", defaults.ignore),
140
+ )
@@ -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)
@@ -0,0 +1,72 @@
1
+ """
2
+ Purpose: Validates mandatory fields in file headers
3
+
4
+ Scope: File header field validation for all supported languages
5
+
6
+ Overview: Validates presence and quality of mandatory header fields. Checks that all
7
+ required fields are present, non-empty, and meet minimum content requirements.
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
+
14
+ Exports: FieldValidator class
15
+
16
+ Interfaces: validate_fields(fields, language) -> list[tuple[str, str]] returns field violations
17
+
18
+ Implementation: Configuration-driven validation with field presence and emptiness checking
19
+ """
20
+
21
+ from .config import FileHeaderConfig
22
+
23
+
24
+ class FieldValidator:
25
+ """Validates mandatory fields in headers."""
26
+
27
+ def __init__(self, config: FileHeaderConfig):
28
+ """Initialize validator with configuration.
29
+
30
+ Args:
31
+ config: File header configuration with required fields
32
+ """
33
+ self.config = config
34
+
35
+ def validate_fields(self, fields: dict[str, str], language: str) -> list[tuple[str, str]]:
36
+ """Validate all required fields are present.
37
+
38
+ Args:
39
+ fields: Dictionary of parsed header fields
40
+ language: File language (python, typescript, etc.)
41
+
42
+ Returns:
43
+ List of (field_name, error_message) tuples for missing/invalid fields
44
+ """
45
+ required_fields = self._get_required_fields(language)
46
+ return [
47
+ error
48
+ for field_name in required_fields
49
+ if (error := self._check_field(fields, field_name))
50
+ ]
51
+
52
+ def _check_field(self, fields: dict[str, str], field_name: str) -> tuple[str, str] | None:
53
+ """Check a single field for presence and content."""
54
+ if field_name not in fields:
55
+ return (field_name, f"Missing mandatory field: {field_name}")
56
+
57
+ if not fields[field_name] or not fields[field_name].strip():
58
+ return (field_name, f"Empty mandatory field: {field_name}")
59
+
60
+ return None
61
+
62
+ def _get_required_fields(self, language: str) -> list[str]:
63
+ """Get required fields for language using dictionary lookup."""
64
+ language_fields = {
65
+ "python": self.config.required_fields_python,
66
+ "typescript": self.config.required_fields_typescript,
67
+ "javascript": self.config.required_fields_typescript,
68
+ "bash": self.config.required_fields_bash,
69
+ "markdown": self.config.required_fields_markdown,
70
+ "css": self.config.required_fields_css,
71
+ }
72
+ return language_fields.get(language, [])