thailint 0.1.5__py3-none-any.whl → 0.5.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- src/__init__.py +7 -2
- src/analyzers/__init__.py +23 -0
- src/analyzers/typescript_base.py +148 -0
- src/api.py +1 -1
- src/cli.py +1111 -144
- src/config.py +12 -33
- src/core/base.py +102 -5
- src/core/cli_utils.py +206 -0
- src/core/config_parser.py +126 -0
- src/core/linter_utils.py +168 -0
- src/core/registry.py +17 -92
- src/core/rule_discovery.py +132 -0
- src/core/violation_builder.py +122 -0
- src/linter_config/ignore.py +112 -40
- src/linter_config/loader.py +3 -13
- src/linters/dry/__init__.py +23 -0
- src/linters/dry/base_token_analyzer.py +76 -0
- src/linters/dry/block_filter.py +265 -0
- src/linters/dry/block_grouper.py +59 -0
- src/linters/dry/cache.py +172 -0
- src/linters/dry/cache_query.py +61 -0
- src/linters/dry/config.py +134 -0
- src/linters/dry/config_loader.py +44 -0
- src/linters/dry/deduplicator.py +120 -0
- src/linters/dry/duplicate_storage.py +63 -0
- src/linters/dry/file_analyzer.py +90 -0
- src/linters/dry/inline_ignore.py +140 -0
- src/linters/dry/linter.py +163 -0
- src/linters/dry/python_analyzer.py +668 -0
- src/linters/dry/storage_initializer.py +42 -0
- src/linters/dry/token_hasher.py +169 -0
- src/linters/dry/typescript_analyzer.py +592 -0
- src/linters/dry/violation_builder.py +74 -0
- src/linters/dry/violation_filter.py +94 -0
- src/linters/dry/violation_generator.py +174 -0
- src/linters/file_header/__init__.py +24 -0
- src/linters/file_header/atemporal_detector.py +87 -0
- src/linters/file_header/config.py +66 -0
- src/linters/file_header/field_validator.py +69 -0
- src/linters/file_header/linter.py +313 -0
- src/linters/file_header/python_parser.py +86 -0
- src/linters/file_header/violation_builder.py +78 -0
- src/linters/file_placement/config_loader.py +86 -0
- src/linters/file_placement/directory_matcher.py +80 -0
- src/linters/file_placement/linter.py +262 -471
- src/linters/file_placement/path_resolver.py +61 -0
- src/linters/file_placement/pattern_matcher.py +55 -0
- src/linters/file_placement/pattern_validator.py +106 -0
- src/linters/file_placement/rule_checker.py +229 -0
- src/linters/file_placement/violation_factory.py +177 -0
- src/linters/magic_numbers/__init__.py +48 -0
- src/linters/magic_numbers/config.py +82 -0
- src/linters/magic_numbers/context_analyzer.py +247 -0
- src/linters/magic_numbers/linter.py +516 -0
- src/linters/magic_numbers/python_analyzer.py +76 -0
- src/linters/magic_numbers/typescript_analyzer.py +218 -0
- src/linters/magic_numbers/violation_builder.py +98 -0
- src/linters/nesting/__init__.py +6 -2
- src/linters/nesting/config.py +17 -4
- src/linters/nesting/linter.py +81 -168
- src/linters/nesting/typescript_analyzer.py +39 -102
- src/linters/nesting/typescript_function_extractor.py +130 -0
- src/linters/nesting/violation_builder.py +139 -0
- src/linters/print_statements/__init__.py +53 -0
- src/linters/print_statements/config.py +83 -0
- src/linters/print_statements/linter.py +430 -0
- src/linters/print_statements/python_analyzer.py +155 -0
- src/linters/print_statements/typescript_analyzer.py +135 -0
- src/linters/print_statements/violation_builder.py +98 -0
- src/linters/srp/__init__.py +99 -0
- src/linters/srp/class_analyzer.py +113 -0
- src/linters/srp/config.py +82 -0
- src/linters/srp/heuristics.py +89 -0
- src/linters/srp/linter.py +234 -0
- src/linters/srp/metrics_evaluator.py +47 -0
- src/linters/srp/python_analyzer.py +72 -0
- src/linters/srp/typescript_analyzer.py +75 -0
- src/linters/srp/typescript_metrics_calculator.py +90 -0
- src/linters/srp/violation_builder.py +117 -0
- src/orchestrator/core.py +54 -9
- src/templates/thailint_config_template.yaml +158 -0
- src/utils/__init__.py +4 -0
- src/utils/project_root.py +203 -0
- thailint-0.5.0.dist-info/METADATA +1286 -0
- thailint-0.5.0.dist-info/RECORD +96 -0
- {thailint-0.1.5.dist-info → thailint-0.5.0.dist-info}/WHEEL +1 -1
- src/.ai/layout.yaml +0 -48
- thailint-0.1.5.dist-info/METADATA +0 -629
- thailint-0.1.5.dist-info/RECORD +0 -28
- {thailint-0.1.5.dist-info → thailint-0.5.0.dist-info}/entry_points.txt +0 -0
- {thailint-0.1.5.dist-info → thailint-0.5.0.dist-info/licenses}/LICENSE +0 -0
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Purpose: Violation overlap filtering
|
|
3
|
+
|
|
4
|
+
Scope: Filters overlapping violations within same file
|
|
5
|
+
|
|
6
|
+
Overview: Filters overlapping violations by comparing line ranges. When violations are close together
|
|
7
|
+
(within 3 lines), only the first one is kept. Used by ViolationDeduplicator to remove duplicate
|
|
8
|
+
reports from rolling hash windows.
|
|
9
|
+
|
|
10
|
+
Dependencies: Violation
|
|
11
|
+
|
|
12
|
+
Exports: ViolationFilter class
|
|
13
|
+
|
|
14
|
+
Interfaces: ViolationFilter.filter_overlapping(sorted_violations)
|
|
15
|
+
|
|
16
|
+
Implementation: Iterates through sorted violations, keeps first of each overlapping group
|
|
17
|
+
"""
|
|
18
|
+
|
|
19
|
+
from src.core.types import Violation
|
|
20
|
+
|
|
21
|
+
# Default fallback for line count when parsing fails
|
|
22
|
+
DEFAULT_FALLBACK_LINE_COUNT = 5
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
class ViolationFilter:
|
|
26
|
+
"""Filters overlapping violations."""
|
|
27
|
+
|
|
28
|
+
def filter_overlapping(self, sorted_violations: list[Violation]) -> list[Violation]:
|
|
29
|
+
"""Filter overlapping violations, keeping first occurrence.
|
|
30
|
+
|
|
31
|
+
Args:
|
|
32
|
+
sorted_violations: Violations sorted by line number
|
|
33
|
+
|
|
34
|
+
Returns:
|
|
35
|
+
Filtered list with overlaps removed
|
|
36
|
+
"""
|
|
37
|
+
kept: list[Violation] = []
|
|
38
|
+
for violation in sorted_violations:
|
|
39
|
+
if not self._overlaps_any(violation, kept):
|
|
40
|
+
kept.append(violation)
|
|
41
|
+
return kept
|
|
42
|
+
|
|
43
|
+
def _overlaps_any(self, violation: Violation, kept_violations: list[Violation]) -> bool:
|
|
44
|
+
"""Check if violation overlaps with any kept violations.
|
|
45
|
+
|
|
46
|
+
Args:
|
|
47
|
+
violation: Violation to check
|
|
48
|
+
kept_violations: Previously kept violations
|
|
49
|
+
|
|
50
|
+
Returns:
|
|
51
|
+
True if violation overlaps with any kept violation
|
|
52
|
+
"""
|
|
53
|
+
for kept in kept_violations:
|
|
54
|
+
if self._overlaps(violation, kept):
|
|
55
|
+
return True
|
|
56
|
+
return False
|
|
57
|
+
|
|
58
|
+
def _overlaps(self, v1: Violation, v2: Violation) -> bool:
|
|
59
|
+
"""Check if two violations overlap.
|
|
60
|
+
|
|
61
|
+
Args:
|
|
62
|
+
v1: First violation (later line number)
|
|
63
|
+
v2: Second violation (earlier line number)
|
|
64
|
+
|
|
65
|
+
Returns:
|
|
66
|
+
True if violations overlap based on code block size
|
|
67
|
+
"""
|
|
68
|
+
line1 = v1.line or 0
|
|
69
|
+
line2 = v2.line or 0
|
|
70
|
+
|
|
71
|
+
# Extract line count from message format: "Duplicate code (N lines, ...)"
|
|
72
|
+
line_count = self._extract_line_count(v1.message)
|
|
73
|
+
|
|
74
|
+
# Blocks overlap if their line ranges intersect
|
|
75
|
+
# Block at line2 covers [line2, line2 + line_count - 1]
|
|
76
|
+
# Block at line1 overlaps if line1 < line2 + line_count
|
|
77
|
+
return line1 < line2 + line_count
|
|
78
|
+
|
|
79
|
+
def _extract_line_count(self, message: str) -> int:
|
|
80
|
+
"""Extract line count from violation message.
|
|
81
|
+
|
|
82
|
+
Args:
|
|
83
|
+
message: Violation message containing line count
|
|
84
|
+
|
|
85
|
+
Returns:
|
|
86
|
+
Number of lines in the duplicate code block (default 5 if not found)
|
|
87
|
+
"""
|
|
88
|
+
# Message format: "Duplicate code (5 lines, 2 occurrences)..."
|
|
89
|
+
try:
|
|
90
|
+
start = message.index("(") + 1
|
|
91
|
+
end = message.index(" lines")
|
|
92
|
+
return int(message[start:end])
|
|
93
|
+
except (ValueError, IndexError):
|
|
94
|
+
return DEFAULT_FALLBACK_LINE_COUNT # Default fallback
|
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Purpose: Violation generation from duplicate code blocks
|
|
3
|
+
|
|
4
|
+
Scope: Generates violations from duplicate hashes
|
|
5
|
+
|
|
6
|
+
Overview: Handles violation generation for duplicate code blocks. Queries storage for duplicate
|
|
7
|
+
hashes, retrieves blocks for each hash, deduplicates overlapping blocks, builds violations
|
|
8
|
+
using ViolationBuilder, and filters violations based on ignore patterns. Separates violation
|
|
9
|
+
generation logic from main linter rule to maintain SRP compliance.
|
|
10
|
+
|
|
11
|
+
Dependencies: DuplicateStorage, ViolationDeduplicator, DRYViolationBuilder, Violation, DRYConfig
|
|
12
|
+
|
|
13
|
+
Exports: ViolationGenerator class
|
|
14
|
+
|
|
15
|
+
Interfaces: ViolationGenerator.generate_violations(storage, rule_id, config) -> list[Violation]
|
|
16
|
+
|
|
17
|
+
Implementation: Queries storage, deduplicates blocks, builds violations, filters by ignore patterns
|
|
18
|
+
"""
|
|
19
|
+
|
|
20
|
+
from pathlib import Path
|
|
21
|
+
|
|
22
|
+
from src.core.types import Violation
|
|
23
|
+
from src.orchestrator.language_detector import detect_language
|
|
24
|
+
|
|
25
|
+
from .config import DRYConfig
|
|
26
|
+
from .deduplicator import ViolationDeduplicator
|
|
27
|
+
from .duplicate_storage import DuplicateStorage
|
|
28
|
+
from .inline_ignore import InlineIgnoreParser
|
|
29
|
+
from .violation_builder import DRYViolationBuilder
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
class ViolationGenerator:
|
|
33
|
+
"""Generates violations from duplicate code blocks."""
|
|
34
|
+
|
|
35
|
+
def __init__(self) -> None:
|
|
36
|
+
"""Initialize with deduplicator and violation builder."""
|
|
37
|
+
self._deduplicator = ViolationDeduplicator()
|
|
38
|
+
self._violation_builder = DRYViolationBuilder()
|
|
39
|
+
|
|
40
|
+
def generate_violations(
|
|
41
|
+
self,
|
|
42
|
+
storage: DuplicateStorage,
|
|
43
|
+
rule_id: str,
|
|
44
|
+
config: DRYConfig,
|
|
45
|
+
inline_ignore: InlineIgnoreParser,
|
|
46
|
+
) -> list[Violation]:
|
|
47
|
+
"""Generate violations from storage.
|
|
48
|
+
|
|
49
|
+
Args:
|
|
50
|
+
storage: Duplicate storage instance
|
|
51
|
+
rule_id: Rule identifier for violations
|
|
52
|
+
config: DRY configuration with ignore patterns
|
|
53
|
+
inline_ignore: Parser with inline ignore directives
|
|
54
|
+
|
|
55
|
+
Returns:
|
|
56
|
+
List of violations filtered by ignore patterns and inline directives
|
|
57
|
+
"""
|
|
58
|
+
duplicate_hashes = storage.get_duplicate_hashes()
|
|
59
|
+
violations = []
|
|
60
|
+
|
|
61
|
+
for hash_value in duplicate_hashes:
|
|
62
|
+
blocks = storage.get_blocks_for_hash(hash_value)
|
|
63
|
+
dedup_blocks = self._deduplicator.deduplicate_blocks(blocks)
|
|
64
|
+
|
|
65
|
+
# Check min_occurrences threshold (language-aware)
|
|
66
|
+
if not self._meets_min_occurrences(dedup_blocks, config):
|
|
67
|
+
continue
|
|
68
|
+
|
|
69
|
+
for block in dedup_blocks:
|
|
70
|
+
violation = self._violation_builder.build_violation(block, dedup_blocks, rule_id)
|
|
71
|
+
violations.append(violation)
|
|
72
|
+
|
|
73
|
+
deduplicated = self._deduplicator.deduplicate_violations(violations)
|
|
74
|
+
pattern_filtered = self._filter_ignored(deduplicated, config.ignore_patterns)
|
|
75
|
+
return self._filter_inline_ignored(pattern_filtered, inline_ignore)
|
|
76
|
+
|
|
77
|
+
def _meets_min_occurrences(self, blocks: list, config: DRYConfig) -> bool:
|
|
78
|
+
"""Check if blocks meet minimum occurrence threshold for the language.
|
|
79
|
+
|
|
80
|
+
Args:
|
|
81
|
+
blocks: List of duplicate code blocks
|
|
82
|
+
config: DRY configuration with min_occurrences settings
|
|
83
|
+
|
|
84
|
+
Returns:
|
|
85
|
+
True if blocks meet or exceed minimum occurrence threshold
|
|
86
|
+
"""
|
|
87
|
+
if len(blocks) == 0:
|
|
88
|
+
return False
|
|
89
|
+
|
|
90
|
+
# Get language from first block's file extension
|
|
91
|
+
first_block = blocks[0]
|
|
92
|
+
language = detect_language(first_block.file_path)
|
|
93
|
+
|
|
94
|
+
# Get language-specific threshold
|
|
95
|
+
min_occurrences = config.get_min_occurrences_for_language(language)
|
|
96
|
+
|
|
97
|
+
return len(blocks) >= min_occurrences
|
|
98
|
+
|
|
99
|
+
def _filter_ignored(
|
|
100
|
+
self, violations: list[Violation], ignore_patterns: list[str]
|
|
101
|
+
) -> list[Violation]:
|
|
102
|
+
"""Filter violations based on ignore patterns.
|
|
103
|
+
|
|
104
|
+
Args:
|
|
105
|
+
violations: List of violations to filter
|
|
106
|
+
ignore_patterns: List of path patterns to ignore
|
|
107
|
+
|
|
108
|
+
Returns:
|
|
109
|
+
Filtered list of violations
|
|
110
|
+
"""
|
|
111
|
+
if not ignore_patterns:
|
|
112
|
+
return violations
|
|
113
|
+
|
|
114
|
+
filtered = []
|
|
115
|
+
for violation in violations:
|
|
116
|
+
if not self._is_ignored(violation.file_path, ignore_patterns):
|
|
117
|
+
filtered.append(violation)
|
|
118
|
+
return filtered
|
|
119
|
+
|
|
120
|
+
def _is_ignored(self, file_path: str, ignore_patterns: list[str]) -> bool:
|
|
121
|
+
"""Check if file path matches any ignore pattern.
|
|
122
|
+
|
|
123
|
+
Args:
|
|
124
|
+
file_path: Path to check
|
|
125
|
+
ignore_patterns: List of patterns to match against
|
|
126
|
+
|
|
127
|
+
Returns:
|
|
128
|
+
True if file should be ignored
|
|
129
|
+
"""
|
|
130
|
+
path_str = str(Path(file_path))
|
|
131
|
+
for pattern in ignore_patterns:
|
|
132
|
+
if pattern in path_str:
|
|
133
|
+
return True
|
|
134
|
+
return False
|
|
135
|
+
|
|
136
|
+
def _filter_inline_ignored(
|
|
137
|
+
self, violations: list[Violation], inline_ignore: InlineIgnoreParser
|
|
138
|
+
) -> list[Violation]:
|
|
139
|
+
"""Filter violations based on inline ignore directives.
|
|
140
|
+
|
|
141
|
+
Args:
|
|
142
|
+
violations: List of violations to filter
|
|
143
|
+
inline_ignore: Parser with inline ignore directives
|
|
144
|
+
|
|
145
|
+
Returns:
|
|
146
|
+
Filtered list of violations
|
|
147
|
+
"""
|
|
148
|
+
filtered = []
|
|
149
|
+
for violation in violations:
|
|
150
|
+
start_line = violation.line or 0
|
|
151
|
+
# Extract line count from message to calculate end_line
|
|
152
|
+
line_count = self._extract_line_count(violation.message)
|
|
153
|
+
end_line = start_line + line_count - 1
|
|
154
|
+
|
|
155
|
+
if not inline_ignore.should_ignore(violation.file_path, start_line, end_line):
|
|
156
|
+
filtered.append(violation)
|
|
157
|
+
return filtered
|
|
158
|
+
|
|
159
|
+
def _extract_line_count(self, message: str) -> int:
|
|
160
|
+
"""Extract line count from violation message.
|
|
161
|
+
|
|
162
|
+
Args:
|
|
163
|
+
message: Violation message
|
|
164
|
+
|
|
165
|
+
Returns:
|
|
166
|
+
Number of lines (default 1)
|
|
167
|
+
"""
|
|
168
|
+
# Message format: "Duplicate code (N lines, ...)"
|
|
169
|
+
try:
|
|
170
|
+
start = message.index("(") + 1
|
|
171
|
+
end = message.index(" lines")
|
|
172
|
+
return int(message[start:end])
|
|
173
|
+
except (ValueError, IndexError):
|
|
174
|
+
return 1
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
"""
|
|
2
|
+
File: src/linters/file_header/__init__.py
|
|
3
|
+
Purpose: File header linter module initialization
|
|
4
|
+
Exports: FileHeaderRule
|
|
5
|
+
Depends: linter.FileHeaderRule
|
|
6
|
+
Implements: Module-level exports for clean API
|
|
7
|
+
Related: linter.py for main rule implementation
|
|
8
|
+
|
|
9
|
+
Overview:
|
|
10
|
+
Initializes the file header linter module providing multi-language file header
|
|
11
|
+
validation with mandatory field checking, atemporal language detection, and configuration
|
|
12
|
+
support. Main entry point for file header linting functionality.
|
|
13
|
+
|
|
14
|
+
Usage:
|
|
15
|
+
from src.linters.file_header import FileHeaderRule
|
|
16
|
+
rule = FileHeaderRule()
|
|
17
|
+
violations = rule.check(context)
|
|
18
|
+
|
|
19
|
+
Notes: Follows standard Python module initialization pattern with __all__ export control
|
|
20
|
+
"""
|
|
21
|
+
|
|
22
|
+
from .linter import FileHeaderRule
|
|
23
|
+
|
|
24
|
+
__all__ = ["FileHeaderRule"]
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
"""
|
|
2
|
+
File: src/linters/file_header/atemporal_detector.py
|
|
3
|
+
Purpose: Detects temporal language patterns in file headers
|
|
4
|
+
Exports: AtemporalDetector class
|
|
5
|
+
Depends: re module for regex matching
|
|
6
|
+
Implements: Regex-based pattern matching with configurable patterns
|
|
7
|
+
Related: linter.py for detector usage, violation_builder.py for violation creation
|
|
8
|
+
|
|
9
|
+
Overview:
|
|
10
|
+
Implements pattern-based detection of temporal language that violates atemporal
|
|
11
|
+
documentation requirements. Detects dates, temporal qualifiers, state change language,
|
|
12
|
+
and future references using regex patterns. Provides violation details for each pattern match.
|
|
13
|
+
|
|
14
|
+
Usage:
|
|
15
|
+
detector = AtemporalDetector()
|
|
16
|
+
violations = detector.detect_violations(header_text)
|
|
17
|
+
|
|
18
|
+
Notes: Four pattern categories - dates, temporal qualifiers, state changes, future references
|
|
19
|
+
"""
|
|
20
|
+
|
|
21
|
+
import re
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
class AtemporalDetector:
|
|
25
|
+
"""Detects temporal language patterns in text."""
|
|
26
|
+
|
|
27
|
+
# Date patterns
|
|
28
|
+
DATE_PATTERNS = [
|
|
29
|
+
(r"\d{4}-\d{2}-\d{2}", "ISO date format (YYYY-MM-DD)"),
|
|
30
|
+
(
|
|
31
|
+
r"(?:January|February|March|April|May|June|July|August|September|October|November|December)\s+\d{4}",
|
|
32
|
+
"Month Year format",
|
|
33
|
+
),
|
|
34
|
+
(r"(?:Created|Updated|Modified):\s*\d{4}", "Date metadata"),
|
|
35
|
+
]
|
|
36
|
+
|
|
37
|
+
# Temporal qualifiers
|
|
38
|
+
TEMPORAL_QUALIFIERS = [
|
|
39
|
+
(r"\bcurrently\b", 'temporal qualifier "currently"'),
|
|
40
|
+
(r"\bnow\b", 'temporal qualifier "now"'),
|
|
41
|
+
(r"\brecently\b", 'temporal qualifier "recently"'),
|
|
42
|
+
(r"\bsoon\b", 'temporal qualifier "soon"'),
|
|
43
|
+
(r"\bfor now\b", 'temporal qualifier "for now"'),
|
|
44
|
+
]
|
|
45
|
+
|
|
46
|
+
# State change language
|
|
47
|
+
STATE_CHANGE = [
|
|
48
|
+
(r"\breplaces?\b", 'state change "replaces"'),
|
|
49
|
+
(r"\bmigrated from\b", 'state change "migrated from"'),
|
|
50
|
+
(r"\bformerly\b", 'state change "formerly"'),
|
|
51
|
+
(r"\bold implementation\b", 'state change "old"'),
|
|
52
|
+
(r"\bnew implementation\b", 'state change "new"'),
|
|
53
|
+
]
|
|
54
|
+
|
|
55
|
+
# Future references
|
|
56
|
+
FUTURE_REFS = [
|
|
57
|
+
(r"\bwill be\b", 'future reference "will be"'),
|
|
58
|
+
(r"\bplanned\b", 'future reference "planned"'),
|
|
59
|
+
(r"\bto be added\b", 'future reference "to be added"'),
|
|
60
|
+
(r"\bcoming soon\b", 'future reference "coming soon"'),
|
|
61
|
+
]
|
|
62
|
+
|
|
63
|
+
def detect_violations( # thailint: ignore[nesting]
|
|
64
|
+
self, text: str
|
|
65
|
+
) -> list[tuple[str, str, int]]:
|
|
66
|
+
"""Detect all temporal language violations in text.
|
|
67
|
+
|
|
68
|
+
Args:
|
|
69
|
+
text: Text to check for temporal language
|
|
70
|
+
|
|
71
|
+
Returns:
|
|
72
|
+
List of (pattern, description, line_number) tuples for each violation
|
|
73
|
+
"""
|
|
74
|
+
violations = []
|
|
75
|
+
|
|
76
|
+
# Check all pattern categories
|
|
77
|
+
all_patterns = (
|
|
78
|
+
self.DATE_PATTERNS + self.TEMPORAL_QUALIFIERS + self.STATE_CHANGE + self.FUTURE_REFS
|
|
79
|
+
)
|
|
80
|
+
|
|
81
|
+
lines = text.split("\n")
|
|
82
|
+
for line_num, line in enumerate(lines, start=1):
|
|
83
|
+
for pattern, description in all_patterns:
|
|
84
|
+
if re.search(pattern, line, re.IGNORECASE):
|
|
85
|
+
violations.append((pattern, description, line_num))
|
|
86
|
+
|
|
87
|
+
return violations
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
"""
|
|
2
|
+
File: src/linters/file_header/config.py
|
|
3
|
+
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
|
+
|
|
9
|
+
Overview:
|
|
10
|
+
Defines configuration structure for file header linter including required fields
|
|
11
|
+
per language, ignore patterns, and validation options. Provides defaults matching
|
|
12
|
+
ai-doc-standard.md requirements and supports loading from .thailint.yaml configuration.
|
|
13
|
+
|
|
14
|
+
Usage:
|
|
15
|
+
config = FileHeaderConfig()
|
|
16
|
+
config = FileHeaderConfig.from_dict(config_dict, "python")
|
|
17
|
+
|
|
18
|
+
Notes: Dataclass with validation and language-specific defaults
|
|
19
|
+
"""
|
|
20
|
+
|
|
21
|
+
from dataclasses import dataclass, field
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
@dataclass
|
|
25
|
+
class FileHeaderConfig:
|
|
26
|
+
"""Configuration for file header linting."""
|
|
27
|
+
|
|
28
|
+
# Required fields by language
|
|
29
|
+
required_fields_python: list[str] = field(
|
|
30
|
+
default_factory=lambda: [
|
|
31
|
+
"Purpose",
|
|
32
|
+
"Scope",
|
|
33
|
+
"Overview",
|
|
34
|
+
"Dependencies",
|
|
35
|
+
"Exports",
|
|
36
|
+
"Interfaces",
|
|
37
|
+
"Implementation",
|
|
38
|
+
]
|
|
39
|
+
)
|
|
40
|
+
|
|
41
|
+
# Enforce atemporal language checking
|
|
42
|
+
enforce_atemporal: bool = True
|
|
43
|
+
|
|
44
|
+
# Patterns to ignore (file paths)
|
|
45
|
+
ignore: list[str] = field(
|
|
46
|
+
default_factory=lambda: ["test/**", "**/migrations/**", "**/__init__.py"]
|
|
47
|
+
)
|
|
48
|
+
|
|
49
|
+
@classmethod
|
|
50
|
+
def from_dict(cls, config_dict: dict, language: str) -> "FileHeaderConfig":
|
|
51
|
+
"""Create config from dictionary.
|
|
52
|
+
|
|
53
|
+
Args:
|
|
54
|
+
config_dict: Dictionary of configuration values
|
|
55
|
+
language: Programming language for language-specific config
|
|
56
|
+
|
|
57
|
+
Returns:
|
|
58
|
+
FileHeaderConfig instance with values from dictionary
|
|
59
|
+
"""
|
|
60
|
+
return cls(
|
|
61
|
+
required_fields_python=config_dict.get("required_fields", {}).get(
|
|
62
|
+
"python", cls().required_fields_python
|
|
63
|
+
),
|
|
64
|
+
enforce_atemporal=config_dict.get("enforce_atemporal", True),
|
|
65
|
+
ignore=config_dict.get("ignore", cls().ignore),
|
|
66
|
+
)
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
"""
|
|
2
|
+
File: src/linters/file_header/field_validator.py
|
|
3
|
+
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
|
+
|
|
9
|
+
Overview:
|
|
10
|
+
Validates presence and quality of mandatory header fields. Checks that all
|
|
11
|
+
required fields are present, non-empty, and meet minimum content requirements.
|
|
12
|
+
Supports language-specific required fields and provides detailed violation messages.
|
|
13
|
+
|
|
14
|
+
Usage:
|
|
15
|
+
validator = FieldValidator(config)
|
|
16
|
+
violations = validator.validate_fields(fields, "python")
|
|
17
|
+
|
|
18
|
+
Notes: Language-specific field requirements defined in config
|
|
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( # thailint: ignore[nesting]
|
|
36
|
+
self, fields: dict[str, str], language: str
|
|
37
|
+
) -> list[tuple[str, str]]:
|
|
38
|
+
"""Validate all required fields are present.
|
|
39
|
+
|
|
40
|
+
Args:
|
|
41
|
+
fields: Dictionary of parsed header fields
|
|
42
|
+
language: File language (python, typescript, etc.)
|
|
43
|
+
|
|
44
|
+
Returns:
|
|
45
|
+
List of (field_name, error_message) tuples for missing/invalid fields
|
|
46
|
+
"""
|
|
47
|
+
violations = []
|
|
48
|
+
required_fields = self._get_required_fields(language)
|
|
49
|
+
|
|
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}"))
|
|
55
|
+
|
|
56
|
+
return violations
|
|
57
|
+
|
|
58
|
+
def _get_required_fields(self, language: str) -> list[str]:
|
|
59
|
+
"""Get required fields for language.
|
|
60
|
+
|
|
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
|