thailint 0.5.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 (204) 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 +38 -25
  23. src/core/base.py +7 -2
  24. src/core/cli_utils.py +19 -2
  25. src/core/config_parser.py +5 -2
  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 +120 -20
  65. src/linters/dry/block_grouper.py +4 -0
  66. src/linters/dry/cache.py +104 -10
  67. src/linters/dry/cache_query.py +4 -0
  68. src/linters/dry/config.py +54 -11
  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 +5 -4
  73. src/linters/dry/file_analyzer.py +4 -2
  74. src/linters/dry/inline_ignore.py +7 -16
  75. src/linters/dry/linter.py +183 -48
  76. src/linters/dry/python_analyzer.py +60 -439
  77. src/linters/dry/python_constant_extractor.py +100 -0
  78. src/linters/dry/single_statement_detector.py +417 -0
  79. src/linters/dry/token_hasher.py +116 -112
  80. src/linters/dry/typescript_analyzer.py +68 -382
  81. src/linters/dry/typescript_constant_extractor.py +138 -0
  82. src/linters/dry/typescript_statement_detector.py +255 -0
  83. src/linters/dry/typescript_value_extractor.py +70 -0
  84. src/linters/dry/violation_builder.py +4 -0
  85. src/linters/dry/violation_filter.py +5 -4
  86. src/linters/dry/violation_generator.py +71 -14
  87. src/linters/file_header/atemporal_detector.py +68 -50
  88. src/linters/file_header/base_parser.py +93 -0
  89. src/linters/file_header/bash_parser.py +66 -0
  90. src/linters/file_header/config.py +90 -16
  91. src/linters/file_header/css_parser.py +70 -0
  92. src/linters/file_header/field_validator.py +36 -33
  93. src/linters/file_header/linter.py +140 -144
  94. src/linters/file_header/markdown_parser.py +130 -0
  95. src/linters/file_header/python_parser.py +14 -58
  96. src/linters/file_header/typescript_parser.py +73 -0
  97. src/linters/file_header/violation_builder.py +13 -12
  98. src/linters/file_placement/config_loader.py +3 -1
  99. src/linters/file_placement/directory_matcher.py +4 -0
  100. src/linters/file_placement/linter.py +66 -34
  101. src/linters/file_placement/pattern_matcher.py +41 -6
  102. src/linters/file_placement/pattern_validator.py +31 -12
  103. src/linters/file_placement/rule_checker.py +12 -7
  104. src/linters/lazy_ignores/__init__.py +43 -0
  105. src/linters/lazy_ignores/config.py +74 -0
  106. src/linters/lazy_ignores/directive_utils.py +164 -0
  107. src/linters/lazy_ignores/header_parser.py +177 -0
  108. src/linters/lazy_ignores/linter.py +158 -0
  109. src/linters/lazy_ignores/matcher.py +168 -0
  110. src/linters/lazy_ignores/python_analyzer.py +209 -0
  111. src/linters/lazy_ignores/rule_id_utils.py +180 -0
  112. src/linters/lazy_ignores/skip_detector.py +298 -0
  113. src/linters/lazy_ignores/types.py +71 -0
  114. src/linters/lazy_ignores/typescript_analyzer.py +146 -0
  115. src/linters/lazy_ignores/violation_builder.py +135 -0
  116. src/linters/lbyl/__init__.py +31 -0
  117. src/linters/lbyl/config.py +63 -0
  118. src/linters/lbyl/linter.py +67 -0
  119. src/linters/lbyl/pattern_detectors/__init__.py +53 -0
  120. src/linters/lbyl/pattern_detectors/base.py +63 -0
  121. src/linters/lbyl/pattern_detectors/dict_key_detector.py +107 -0
  122. src/linters/lbyl/pattern_detectors/division_check_detector.py +232 -0
  123. src/linters/lbyl/pattern_detectors/file_exists_detector.py +220 -0
  124. src/linters/lbyl/pattern_detectors/hasattr_detector.py +119 -0
  125. src/linters/lbyl/pattern_detectors/isinstance_detector.py +119 -0
  126. src/linters/lbyl/pattern_detectors/len_check_detector.py +173 -0
  127. src/linters/lbyl/pattern_detectors/none_check_detector.py +146 -0
  128. src/linters/lbyl/pattern_detectors/string_validator_detector.py +145 -0
  129. src/linters/lbyl/python_analyzer.py +215 -0
  130. src/linters/lbyl/violation_builder.py +354 -0
  131. src/linters/magic_numbers/context_analyzer.py +227 -225
  132. src/linters/magic_numbers/linter.py +28 -82
  133. src/linters/magic_numbers/python_analyzer.py +4 -16
  134. src/linters/magic_numbers/typescript_analyzer.py +9 -12
  135. src/linters/magic_numbers/typescript_ignore_checker.py +81 -0
  136. src/linters/method_property/__init__.py +49 -0
  137. src/linters/method_property/config.py +138 -0
  138. src/linters/method_property/linter.py +414 -0
  139. src/linters/method_property/python_analyzer.py +473 -0
  140. src/linters/method_property/violation_builder.py +119 -0
  141. src/linters/nesting/linter.py +24 -16
  142. src/linters/nesting/python_analyzer.py +4 -0
  143. src/linters/nesting/typescript_analyzer.py +6 -12
  144. src/linters/nesting/violation_builder.py +1 -0
  145. src/linters/performance/__init__.py +91 -0
  146. src/linters/performance/config.py +43 -0
  147. src/linters/performance/constants.py +49 -0
  148. src/linters/performance/linter.py +149 -0
  149. src/linters/performance/python_analyzer.py +365 -0
  150. src/linters/performance/regex_analyzer.py +312 -0
  151. src/linters/performance/regex_linter.py +139 -0
  152. src/linters/performance/typescript_analyzer.py +236 -0
  153. src/linters/performance/violation_builder.py +160 -0
  154. src/linters/print_statements/config.py +7 -12
  155. src/linters/print_statements/linter.py +26 -43
  156. src/linters/print_statements/python_analyzer.py +91 -93
  157. src/linters/print_statements/typescript_analyzer.py +15 -25
  158. src/linters/print_statements/violation_builder.py +12 -14
  159. src/linters/srp/class_analyzer.py +11 -7
  160. src/linters/srp/heuristics.py +56 -22
  161. src/linters/srp/linter.py +15 -16
  162. src/linters/srp/python_analyzer.py +55 -20
  163. src/linters/srp/typescript_metrics_calculator.py +110 -50
  164. src/linters/stateless_class/__init__.py +25 -0
  165. src/linters/stateless_class/config.py +58 -0
  166. src/linters/stateless_class/linter.py +349 -0
  167. src/linters/stateless_class/python_analyzer.py +290 -0
  168. src/linters/stringly_typed/__init__.py +36 -0
  169. src/linters/stringly_typed/config.py +189 -0
  170. src/linters/stringly_typed/context_filter.py +451 -0
  171. src/linters/stringly_typed/function_call_violation_builder.py +135 -0
  172. src/linters/stringly_typed/ignore_checker.py +100 -0
  173. src/linters/stringly_typed/ignore_utils.py +51 -0
  174. src/linters/stringly_typed/linter.py +376 -0
  175. src/linters/stringly_typed/python/__init__.py +33 -0
  176. src/linters/stringly_typed/python/analyzer.py +348 -0
  177. src/linters/stringly_typed/python/call_tracker.py +175 -0
  178. src/linters/stringly_typed/python/comparison_tracker.py +257 -0
  179. src/linters/stringly_typed/python/condition_extractor.py +134 -0
  180. src/linters/stringly_typed/python/conditional_detector.py +179 -0
  181. src/linters/stringly_typed/python/constants.py +21 -0
  182. src/linters/stringly_typed/python/match_analyzer.py +94 -0
  183. src/linters/stringly_typed/python/validation_detector.py +189 -0
  184. src/linters/stringly_typed/python/variable_extractor.py +96 -0
  185. src/linters/stringly_typed/storage.py +620 -0
  186. src/linters/stringly_typed/storage_initializer.py +45 -0
  187. src/linters/stringly_typed/typescript/__init__.py +28 -0
  188. src/linters/stringly_typed/typescript/analyzer.py +157 -0
  189. src/linters/stringly_typed/typescript/call_tracker.py +335 -0
  190. src/linters/stringly_typed/typescript/comparison_tracker.py +378 -0
  191. src/linters/stringly_typed/violation_generator.py +419 -0
  192. src/orchestrator/core.py +252 -14
  193. src/orchestrator/language_detector.py +5 -3
  194. src/templates/thailint_config_template.yaml +196 -0
  195. src/utils/project_root.py +3 -0
  196. thailint-0.15.3.dist-info/METADATA +187 -0
  197. thailint-0.15.3.dist-info/RECORD +226 -0
  198. thailint-0.15.3.dist-info/entry_points.txt +4 -0
  199. src/cli.py +0 -1665
  200. thailint-0.5.0.dist-info/METADATA +0 -1286
  201. thailint-0.5.0.dist-info/RECORD +0 -96
  202. thailint-0.5.0.dist-info/entry_points.txt +0 -4
  203. {thailint-0.5.0.dist-info → thailint-0.15.3.dist-info}/WHEEL +0 -0
  204. {thailint-0.5.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
@@ -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,33 @@ 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
+
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
60
128
  return cls(
61
- required_fields_python=config_dict.get("required_fields", {}).get(
62
- "python", cls().required_fields_python
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
63
136
  ),
137
+ required_fields_css=required_fields.get("css", defaults.required_fields_css),
64
138
  enforce_atemporal=config_dict.get("enforce_atemporal", True),
65
- ignore=config_dict.get("ignore", cls().ignore),
139
+ ignore=config_dict.get("ignore", defaults.ignore),
66
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)
@@ -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:
@@ -44,26 +42,31 @@ class FieldValidator:
44
42
  Returns:
45
43
  List of (field_name, error_message) tuples for missing/invalid fields
46
44
  """
47
- violations = []
48
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
+ ]
49
51
 
50
- 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}"))
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}")
55
56
 
56
- return violations
57
+ if not fields[field_name] or not fields[field_name].strip():
58
+ return (field_name, f"Empty mandatory field: {field_name}")
57
59
 
58
- def _get_required_fields(self, language: str) -> list[str]:
59
- """Get required fields for language.
60
+ return None
60
61
 
61
- Args:
62
- language: Programming language
63
-
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
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, [])