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.
- src/__init__.py +1 -0
- src/analyzers/__init__.py +4 -3
- src/analyzers/ast_utils.py +54 -0
- src/analyzers/rust_base.py +155 -0
- src/analyzers/rust_context.py +141 -0
- src/analyzers/typescript_base.py +4 -0
- src/cli/__init__.py +30 -0
- src/cli/__main__.py +22 -0
- src/cli/config.py +480 -0
- src/cli/config_merge.py +241 -0
- src/cli/linters/__init__.py +67 -0
- src/cli/linters/code_patterns.py +270 -0
- src/cli/linters/code_smells.py +342 -0
- src/cli/linters/documentation.py +83 -0
- src/cli/linters/performance.py +287 -0
- src/cli/linters/shared.py +331 -0
- src/cli/linters/structure.py +327 -0
- src/cli/linters/structure_quality.py +328 -0
- src/cli/main.py +120 -0
- src/cli/utils.py +395 -0
- src/cli_main.py +37 -0
- src/config.py +38 -25
- src/core/base.py +7 -2
- src/core/cli_utils.py +19 -2
- src/core/config_parser.py +5 -2
- src/core/constants.py +54 -0
- src/core/linter_utils.py +95 -6
- src/core/python_lint_rule.py +101 -0
- src/core/registry.py +1 -1
- src/core/rule_discovery.py +147 -84
- src/core/types.py +13 -0
- src/core/violation_builder.py +78 -15
- src/core/violation_utils.py +69 -0
- src/formatters/__init__.py +22 -0
- src/formatters/sarif.py +202 -0
- src/linter_config/directive_markers.py +109 -0
- src/linter_config/ignore.py +254 -395
- src/linter_config/loader.py +45 -12
- src/linter_config/pattern_utils.py +65 -0
- src/linter_config/rule_matcher.py +89 -0
- src/linters/collection_pipeline/__init__.py +90 -0
- src/linters/collection_pipeline/any_all_analyzer.py +281 -0
- src/linters/collection_pipeline/ast_utils.py +40 -0
- src/linters/collection_pipeline/config.py +75 -0
- src/linters/collection_pipeline/continue_analyzer.py +94 -0
- src/linters/collection_pipeline/detector.py +360 -0
- src/linters/collection_pipeline/filter_map_analyzer.py +402 -0
- src/linters/collection_pipeline/linter.py +420 -0
- src/linters/collection_pipeline/suggestion_builder.py +130 -0
- src/linters/cqs/__init__.py +54 -0
- src/linters/cqs/config.py +55 -0
- src/linters/cqs/function_analyzer.py +201 -0
- src/linters/cqs/input_detector.py +139 -0
- src/linters/cqs/linter.py +159 -0
- src/linters/cqs/output_detector.py +84 -0
- src/linters/cqs/python_analyzer.py +54 -0
- src/linters/cqs/types.py +82 -0
- src/linters/cqs/typescript_cqs_analyzer.py +61 -0
- src/linters/cqs/typescript_function_analyzer.py +192 -0
- src/linters/cqs/typescript_input_detector.py +203 -0
- src/linters/cqs/typescript_output_detector.py +117 -0
- src/linters/cqs/violation_builder.py +94 -0
- src/linters/dry/base_token_analyzer.py +16 -9
- src/linters/dry/block_filter.py +120 -20
- src/linters/dry/block_grouper.py +4 -0
- src/linters/dry/cache.py +104 -10
- src/linters/dry/cache_query.py +4 -0
- src/linters/dry/config.py +54 -11
- src/linters/dry/constant.py +92 -0
- src/linters/dry/constant_matcher.py +223 -0
- src/linters/dry/constant_violation_builder.py +98 -0
- src/linters/dry/duplicate_storage.py +5 -4
- src/linters/dry/file_analyzer.py +4 -2
- src/linters/dry/inline_ignore.py +7 -16
- src/linters/dry/linter.py +183 -48
- src/linters/dry/python_analyzer.py +60 -439
- src/linters/dry/python_constant_extractor.py +100 -0
- src/linters/dry/single_statement_detector.py +417 -0
- src/linters/dry/token_hasher.py +116 -112
- src/linters/dry/typescript_analyzer.py +68 -382
- src/linters/dry/typescript_constant_extractor.py +138 -0
- src/linters/dry/typescript_statement_detector.py +255 -0
- src/linters/dry/typescript_value_extractor.py +70 -0
- src/linters/dry/violation_builder.py +4 -0
- src/linters/dry/violation_filter.py +5 -4
- src/linters/dry/violation_generator.py +71 -14
- src/linters/file_header/atemporal_detector.py +68 -50
- src/linters/file_header/base_parser.py +93 -0
- src/linters/file_header/bash_parser.py +66 -0
- src/linters/file_header/config.py +90 -16
- src/linters/file_header/css_parser.py +70 -0
- src/linters/file_header/field_validator.py +36 -33
- src/linters/file_header/linter.py +140 -144
- src/linters/file_header/markdown_parser.py +130 -0
- src/linters/file_header/python_parser.py +14 -58
- src/linters/file_header/typescript_parser.py +73 -0
- src/linters/file_header/violation_builder.py +13 -12
- src/linters/file_placement/config_loader.py +3 -1
- src/linters/file_placement/directory_matcher.py +4 -0
- src/linters/file_placement/linter.py +66 -34
- src/linters/file_placement/pattern_matcher.py +41 -6
- src/linters/file_placement/pattern_validator.py +31 -12
- src/linters/file_placement/rule_checker.py +12 -7
- src/linters/lazy_ignores/__init__.py +43 -0
- src/linters/lazy_ignores/config.py +74 -0
- src/linters/lazy_ignores/directive_utils.py +164 -0
- src/linters/lazy_ignores/header_parser.py +177 -0
- src/linters/lazy_ignores/linter.py +158 -0
- src/linters/lazy_ignores/matcher.py +168 -0
- src/linters/lazy_ignores/python_analyzer.py +209 -0
- src/linters/lazy_ignores/rule_id_utils.py +180 -0
- src/linters/lazy_ignores/skip_detector.py +298 -0
- src/linters/lazy_ignores/types.py +71 -0
- src/linters/lazy_ignores/typescript_analyzer.py +146 -0
- src/linters/lazy_ignores/violation_builder.py +135 -0
- src/linters/lbyl/__init__.py +31 -0
- src/linters/lbyl/config.py +63 -0
- src/linters/lbyl/linter.py +67 -0
- src/linters/lbyl/pattern_detectors/__init__.py +53 -0
- src/linters/lbyl/pattern_detectors/base.py +63 -0
- src/linters/lbyl/pattern_detectors/dict_key_detector.py +107 -0
- src/linters/lbyl/pattern_detectors/division_check_detector.py +232 -0
- src/linters/lbyl/pattern_detectors/file_exists_detector.py +220 -0
- src/linters/lbyl/pattern_detectors/hasattr_detector.py +119 -0
- src/linters/lbyl/pattern_detectors/isinstance_detector.py +119 -0
- src/linters/lbyl/pattern_detectors/len_check_detector.py +173 -0
- src/linters/lbyl/pattern_detectors/none_check_detector.py +146 -0
- src/linters/lbyl/pattern_detectors/string_validator_detector.py +145 -0
- src/linters/lbyl/python_analyzer.py +215 -0
- src/linters/lbyl/violation_builder.py +354 -0
- src/linters/magic_numbers/context_analyzer.py +227 -225
- src/linters/magic_numbers/linter.py +28 -82
- src/linters/magic_numbers/python_analyzer.py +4 -16
- src/linters/magic_numbers/typescript_analyzer.py +9 -12
- src/linters/magic_numbers/typescript_ignore_checker.py +81 -0
- src/linters/method_property/__init__.py +49 -0
- src/linters/method_property/config.py +138 -0
- src/linters/method_property/linter.py +414 -0
- src/linters/method_property/python_analyzer.py +473 -0
- src/linters/method_property/violation_builder.py +119 -0
- src/linters/nesting/linter.py +24 -16
- src/linters/nesting/python_analyzer.py +4 -0
- src/linters/nesting/typescript_analyzer.py +6 -12
- src/linters/nesting/violation_builder.py +1 -0
- src/linters/performance/__init__.py +91 -0
- src/linters/performance/config.py +43 -0
- src/linters/performance/constants.py +49 -0
- src/linters/performance/linter.py +149 -0
- src/linters/performance/python_analyzer.py +365 -0
- src/linters/performance/regex_analyzer.py +312 -0
- src/linters/performance/regex_linter.py +139 -0
- src/linters/performance/typescript_analyzer.py +236 -0
- src/linters/performance/violation_builder.py +160 -0
- src/linters/print_statements/config.py +7 -12
- src/linters/print_statements/linter.py +26 -43
- src/linters/print_statements/python_analyzer.py +91 -93
- src/linters/print_statements/typescript_analyzer.py +15 -25
- src/linters/print_statements/violation_builder.py +12 -14
- src/linters/srp/class_analyzer.py +11 -7
- src/linters/srp/heuristics.py +56 -22
- src/linters/srp/linter.py +15 -16
- src/linters/srp/python_analyzer.py +55 -20
- src/linters/srp/typescript_metrics_calculator.py +110 -50
- src/linters/stateless_class/__init__.py +25 -0
- src/linters/stateless_class/config.py +58 -0
- src/linters/stateless_class/linter.py +349 -0
- src/linters/stateless_class/python_analyzer.py +290 -0
- src/linters/stringly_typed/__init__.py +36 -0
- src/linters/stringly_typed/config.py +189 -0
- src/linters/stringly_typed/context_filter.py +451 -0
- src/linters/stringly_typed/function_call_violation_builder.py +135 -0
- src/linters/stringly_typed/ignore_checker.py +100 -0
- src/linters/stringly_typed/ignore_utils.py +51 -0
- src/linters/stringly_typed/linter.py +376 -0
- src/linters/stringly_typed/python/__init__.py +33 -0
- src/linters/stringly_typed/python/analyzer.py +348 -0
- src/linters/stringly_typed/python/call_tracker.py +175 -0
- src/linters/stringly_typed/python/comparison_tracker.py +257 -0
- src/linters/stringly_typed/python/condition_extractor.py +134 -0
- src/linters/stringly_typed/python/conditional_detector.py +179 -0
- src/linters/stringly_typed/python/constants.py +21 -0
- src/linters/stringly_typed/python/match_analyzer.py +94 -0
- src/linters/stringly_typed/python/validation_detector.py +189 -0
- src/linters/stringly_typed/python/variable_extractor.py +96 -0
- src/linters/stringly_typed/storage.py +620 -0
- src/linters/stringly_typed/storage_initializer.py +45 -0
- src/linters/stringly_typed/typescript/__init__.py +28 -0
- src/linters/stringly_typed/typescript/analyzer.py +157 -0
- src/linters/stringly_typed/typescript/call_tracker.py +335 -0
- src/linters/stringly_typed/typescript/comparison_tracker.py +378 -0
- src/linters/stringly_typed/violation_generator.py +419 -0
- src/orchestrator/core.py +252 -14
- src/orchestrator/language_detector.py +5 -3
- src/templates/thailint_config_template.yaml +196 -0
- src/utils/project_root.py +3 -0
- thailint-0.15.3.dist-info/METADATA +187 -0
- thailint-0.15.3.dist-info/RECORD +226 -0
- thailint-0.15.3.dist-info/entry_points.txt +4 -0
- src/cli.py +0 -1665
- thailint-0.5.0.dist-info/METADATA +0 -1286
- thailint-0.5.0.dist-info/RECORD +0 -96
- thailint-0.5.0.dist-info/entry_points.txt +0 -4
- {thailint-0.5.0.dist-info → thailint-0.15.3.dist-info}/WHEEL +0 -0
- {thailint-0.5.0.dist-info → thailint-0.15.3.dist-info}/licenses/LICENSE +0 -0
|
@@ -1,38 +1,64 @@
|
|
|
1
1
|
"""
|
|
2
|
-
File: src/linters/file_header/linter.py
|
|
3
2
|
Purpose: Main file header linter rule implementation
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
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
|
|
23
|
+
|
|
24
|
+
Suppressions:
|
|
25
|
+
- type:ignore[type-var]: Protocol pattern with generic type matching
|
|
26
|
+
- srp: Rule class coordinates parsing, validation, and violation building for multiple
|
|
27
|
+
languages. Methods support single responsibility of file header validation.
|
|
20
28
|
"""
|
|
21
29
|
|
|
30
|
+
from contextlib import suppress
|
|
22
31
|
from pathlib import Path
|
|
32
|
+
from typing import Protocol
|
|
23
33
|
|
|
24
34
|
from src.core.base import BaseLintContext, BaseLintRule
|
|
35
|
+
from src.core.constants import HEADER_SCAN_LINES, Language
|
|
25
36
|
from src.core.linter_utils import load_linter_config
|
|
26
37
|
from src.core.types import Violation
|
|
27
|
-
from src.linter_config.
|
|
38
|
+
from src.linter_config.directive_markers import check_general_ignore, has_ignore_directive_marker
|
|
39
|
+
from src.linter_config.ignore import _check_specific_rule_ignore, get_ignore_parser
|
|
28
40
|
|
|
29
41
|
from .atemporal_detector import AtemporalDetector
|
|
42
|
+
from .bash_parser import BashHeaderParser
|
|
30
43
|
from .config import FileHeaderConfig
|
|
44
|
+
from .css_parser import CssHeaderParser
|
|
31
45
|
from .field_validator import FieldValidator
|
|
46
|
+
from .markdown_parser import MarkdownHeaderParser
|
|
32
47
|
from .python_parser import PythonHeaderParser
|
|
48
|
+
from .typescript_parser import TypeScriptHeaderParser
|
|
33
49
|
from .violation_builder import ViolationBuilder
|
|
34
50
|
|
|
35
51
|
|
|
52
|
+
class HeaderParser(Protocol):
|
|
53
|
+
"""Protocol for header parsers."""
|
|
54
|
+
|
|
55
|
+
def extract_header(self, code: str) -> str | None:
|
|
56
|
+
"""Extract header from source code."""
|
|
57
|
+
|
|
58
|
+
def parse_fields(self, header: str) -> dict[str, str]:
|
|
59
|
+
"""Parse fields from header."""
|
|
60
|
+
|
|
61
|
+
|
|
36
62
|
class FileHeaderRule(BaseLintRule): # thailint: ignore[srp]
|
|
37
63
|
"""Validates file headers for mandatory fields and atemporal language.
|
|
38
64
|
|
|
@@ -42,74 +68,97 @@ class FileHeaderRule(BaseLintRule): # thailint: ignore[srp]
|
|
|
42
68
|
pattern with focused helper classes (parser, validator, detector, builder).
|
|
43
69
|
"""
|
|
44
70
|
|
|
71
|
+
# Parser instances for each language
|
|
72
|
+
_parsers: dict[str, HeaderParser] = {
|
|
73
|
+
"python": PythonHeaderParser(),
|
|
74
|
+
"typescript": TypeScriptHeaderParser(),
|
|
75
|
+
"javascript": TypeScriptHeaderParser(),
|
|
76
|
+
"bash": BashHeaderParser(),
|
|
77
|
+
"markdown": MarkdownHeaderParser(),
|
|
78
|
+
"css": CssHeaderParser(),
|
|
79
|
+
}
|
|
80
|
+
|
|
45
81
|
def __init__(self) -> None:
|
|
46
82
|
"""Initialize the file header rule."""
|
|
47
83
|
self._violation_builder = ViolationBuilder(self.rule_id)
|
|
48
|
-
self._ignore_parser =
|
|
84
|
+
self._ignore_parser = get_ignore_parser()
|
|
49
85
|
|
|
50
86
|
@property
|
|
51
87
|
def rule_id(self) -> str:
|
|
52
|
-
"""Unique identifier for this rule.
|
|
53
|
-
|
|
54
|
-
Returns:
|
|
55
|
-
Rule identifier string
|
|
56
|
-
"""
|
|
88
|
+
"""Unique identifier for this rule."""
|
|
57
89
|
return "file-header.validation"
|
|
58
90
|
|
|
59
91
|
@property
|
|
60
92
|
def rule_name(self) -> str:
|
|
61
|
-
"""Human-readable name for this rule.
|
|
62
|
-
|
|
63
|
-
Returns:
|
|
64
|
-
Rule name string
|
|
65
|
-
"""
|
|
93
|
+
"""Human-readable name for this rule."""
|
|
66
94
|
return "File Header Validation"
|
|
67
95
|
|
|
68
96
|
@property
|
|
69
97
|
def description(self) -> str:
|
|
70
|
-
"""Description of what this rule checks.
|
|
71
|
-
|
|
72
|
-
Returns:
|
|
73
|
-
Rule description string
|
|
74
|
-
"""
|
|
98
|
+
"""Description of what this rule checks."""
|
|
75
99
|
return "Validates file headers for mandatory fields and atemporal language"
|
|
76
100
|
|
|
77
101
|
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
|
|
102
|
+
"""Check file header for violations."""
|
|
91
103
|
if self._has_file_ignore(context):
|
|
92
104
|
return []
|
|
93
105
|
|
|
94
|
-
# Load configuration
|
|
95
106
|
config = self._load_config(context)
|
|
96
107
|
|
|
97
|
-
# Check if file should be ignored by pattern
|
|
98
108
|
if self._should_ignore_file(context, config):
|
|
99
109
|
return []
|
|
100
110
|
|
|
101
|
-
|
|
102
|
-
return self._check_python_header(context, config)
|
|
111
|
+
return self._check_language_header(context, config)
|
|
103
112
|
|
|
104
|
-
def
|
|
105
|
-
|
|
113
|
+
def _check_language_header(
|
|
114
|
+
self, context: BaseLintContext, config: FileHeaderConfig
|
|
115
|
+
) -> list[Violation]:
|
|
116
|
+
"""Dispatch to language-specific header checking."""
|
|
117
|
+
parser = self._parsers.get(context.language)
|
|
118
|
+
if not parser:
|
|
119
|
+
return []
|
|
120
|
+
|
|
121
|
+
# Markdown has special atemporal handling
|
|
122
|
+
if context.language == Language.MARKDOWN:
|
|
123
|
+
return self._check_markdown_header(parser, context, config)
|
|
124
|
+
|
|
125
|
+
return self._check_header_with_parser(parser, context, config)
|
|
126
|
+
|
|
127
|
+
def _check_header_with_parser(
|
|
128
|
+
self, parser: HeaderParser, context: BaseLintContext, config: FileHeaderConfig
|
|
129
|
+
) -> list[Violation]:
|
|
130
|
+
"""Check header using the given parser."""
|
|
131
|
+
header = parser.extract_header(context.file_content or "")
|
|
132
|
+
|
|
133
|
+
if not header:
|
|
134
|
+
return self._build_missing_header_violations(context)
|
|
106
135
|
|
|
107
|
-
|
|
108
|
-
|
|
136
|
+
fields = parser.parse_fields(header)
|
|
137
|
+
violations = self._validate_header_fields(fields, context, config)
|
|
138
|
+
violations.extend(self._check_atemporal_violations(header, context, config))
|
|
139
|
+
|
|
140
|
+
return self._filter_ignored_violations(violations, context)
|
|
109
141
|
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
142
|
+
def _check_markdown_header(
|
|
143
|
+
self, parser: HeaderParser, context: BaseLintContext, config: FileHeaderConfig
|
|
144
|
+
) -> list[Violation]:
|
|
145
|
+
"""Check Markdown file header with special prose-only atemporal checking."""
|
|
146
|
+
header = parser.extract_header(context.file_content or "")
|
|
147
|
+
|
|
148
|
+
if not header:
|
|
149
|
+
return self._build_missing_header_violations(context)
|
|
150
|
+
|
|
151
|
+
fields = parser.parse_fields(header)
|
|
152
|
+
violations = self._validate_header_fields(fields, context, config)
|
|
153
|
+
|
|
154
|
+
# For Markdown, only check atemporal language in prose fields
|
|
155
|
+
prose_content = self._extract_markdown_prose_fields(fields)
|
|
156
|
+
violations.extend(self._check_atemporal_violations(prose_content, context, config))
|
|
157
|
+
|
|
158
|
+
return self._filter_ignored_violations(violations, context)
|
|
159
|
+
|
|
160
|
+
def _has_file_ignore(self, context: BaseLintContext) -> bool:
|
|
161
|
+
"""Check if file has file-level ignore directive."""
|
|
113
162
|
file_content = context.file_content or ""
|
|
114
163
|
|
|
115
164
|
if self._has_standard_ignore(file_content):
|
|
@@ -117,21 +166,20 @@ class FileHeaderRule(BaseLintRule): # thailint: ignore[srp]
|
|
|
117
166
|
|
|
118
167
|
return self._has_custom_ignore_syntax(file_content)
|
|
119
168
|
|
|
120
|
-
def _has_standard_ignore(self, file_content: str) -> bool:
|
|
169
|
+
def _has_standard_ignore(self, file_content: str) -> bool:
|
|
121
170
|
"""Check standard ignore parser for file-level ignores."""
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
return False
|
|
171
|
+
first_lines = file_content.splitlines()[:HEADER_SCAN_LINES]
|
|
172
|
+
return any(self._line_has_matching_ignore(line) for line in first_lines)
|
|
173
|
+
|
|
174
|
+
def _line_has_matching_ignore(self, line: str) -> bool:
|
|
175
|
+
"""Check if line has matching ignore directive for this rule."""
|
|
176
|
+
if not has_ignore_directive_marker(line):
|
|
177
|
+
return False
|
|
178
|
+
return _check_specific_rule_ignore(line, self.rule_id) or check_general_ignore(line)
|
|
131
179
|
|
|
132
180
|
def _has_custom_ignore_syntax(self, file_content: str) -> bool:
|
|
133
181
|
"""Check custom file-level ignore syntax."""
|
|
134
|
-
first_lines = file_content.splitlines()[:
|
|
182
|
+
first_lines = file_content.splitlines()[:HEADER_SCAN_LINES]
|
|
135
183
|
return any(self._is_ignore_line(line) for line in first_lines)
|
|
136
184
|
|
|
137
185
|
def _is_ignore_line(self, line: str) -> bool:
|
|
@@ -140,32 +188,15 @@ class FileHeaderRule(BaseLintRule): # thailint: ignore[srp]
|
|
|
140
188
|
return "# thailint-ignore-file:" in line_lower or "# thailint-ignore" in line_lower
|
|
141
189
|
|
|
142
190
|
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
|
|
191
|
+
"""Load configuration from context."""
|
|
152
192
|
if hasattr(context, "metadata") and isinstance(context.metadata, dict):
|
|
153
193
|
if "file_header" in context.metadata:
|
|
154
194
|
return load_linter_config(context, "file_header", FileHeaderConfig) # type: ignore[type-var]
|
|
155
195
|
|
|
156
|
-
# Use defaults
|
|
157
196
|
return FileHeaderConfig()
|
|
158
197
|
|
|
159
198
|
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
|
-
"""
|
|
199
|
+
"""Check if file matches ignore patterns."""
|
|
169
200
|
if not context.file_path:
|
|
170
201
|
return False
|
|
171
202
|
|
|
@@ -200,30 +231,6 @@ class FileHeaderRule(BaseLintRule): # thailint: ignore[srp]
|
|
|
200
231
|
return file_path.name == filename_pattern or path_str.endswith(filename_pattern)
|
|
201
232
|
return False
|
|
202
233
|
|
|
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
234
|
def _build_missing_header_violations(self, context: BaseLintContext) -> list[Violation]:
|
|
228
235
|
"""Build violations for missing header."""
|
|
229
236
|
return [
|
|
@@ -270,44 +277,33 @@ class FileHeaderRule(BaseLintRule): # thailint: ignore[srp]
|
|
|
270
277
|
def _filter_ignored_violations(
|
|
271
278
|
self, violations: list[Violation], context: BaseLintContext
|
|
272
279
|
) -> 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
|
-
"""
|
|
280
|
+
"""Filter out violations that should be ignored."""
|
|
282
281
|
file_content = context.file_content or ""
|
|
283
282
|
lines = file_content.splitlines()
|
|
284
283
|
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
if self._ignore_parser.should_ignore_violation(v, file_content)
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
if self._has_line_level_ignore(lines, v):
|
|
293
|
-
continue
|
|
294
|
-
|
|
295
|
-
filtered.append(v)
|
|
296
|
-
|
|
297
|
-
return filtered
|
|
284
|
+
non_ignored = (
|
|
285
|
+
v
|
|
286
|
+
for v in violations
|
|
287
|
+
if not self._ignore_parser.should_ignore_violation(v, file_content)
|
|
288
|
+
and not self._has_line_level_ignore(lines, v)
|
|
289
|
+
)
|
|
290
|
+
return list(non_ignored)
|
|
298
291
|
|
|
299
292
|
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
|
-
"""
|
|
293
|
+
"""Check for thailint-ignore-line directive."""
|
|
309
294
|
if violation.line <= 0 or violation.line > len(lines):
|
|
310
295
|
return False
|
|
311
296
|
|
|
312
|
-
line_content = lines[violation.line - 1]
|
|
297
|
+
line_content = lines[violation.line - 1]
|
|
313
298
|
return "# thailint-ignore-line:" in line_content.lower()
|
|
299
|
+
|
|
300
|
+
def _extract_markdown_prose_fields(self, fields: dict[str, str]) -> str:
|
|
301
|
+
"""Extract prose fields from Markdown frontmatter for atemporal checking."""
|
|
302
|
+
prose_fields = ["purpose", "scope", "overview"]
|
|
303
|
+
prose_parts = []
|
|
304
|
+
|
|
305
|
+
for field_name in prose_fields:
|
|
306
|
+
with suppress(KeyError):
|
|
307
|
+
prose_parts.append(f"{field_name}: {fields[field_name]}")
|
|
308
|
+
|
|
309
|
+
return "\n".join(prose_parts)
|
|
@@ -0,0 +1,130 @@
|
|
|
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
|
+
Suppressions:
|
|
22
|
+
- BLE001: Broad exception catch for YAML parsing fallback (any exception triggers regex fallback)
|
|
23
|
+
- srp: Class coordinates YAML extraction, parsing, and field validation for Markdown.
|
|
24
|
+
Method count exceeds limit due to complexity refactoring.
|
|
25
|
+
- nesting,dry: _parse_simple_yaml uses nested loops for YAML structure traversal.
|
|
26
|
+
"""
|
|
27
|
+
|
|
28
|
+
import logging
|
|
29
|
+
import re
|
|
30
|
+
|
|
31
|
+
logger = logging.getLogger(__name__)
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
class MarkdownHeaderParser: # thailint: ignore[srp]
|
|
35
|
+
"""Extracts and parses Markdown file headers from YAML frontmatter.
|
|
36
|
+
|
|
37
|
+
Method count (10) exceeds SRP guideline (8) because proper A-grade complexity
|
|
38
|
+
refactoring requires extracting small focused helper methods. Class maintains
|
|
39
|
+
single responsibility of YAML frontmatter parsing - all methods support this
|
|
40
|
+
core purpose through either PyYAML or simple regex parsing fallback.
|
|
41
|
+
"""
|
|
42
|
+
|
|
43
|
+
# Pattern to match YAML frontmatter at start of file
|
|
44
|
+
FRONTMATTER_PATTERN = re.compile(r"^---\s*\n(.*?)\n---", re.DOTALL)
|
|
45
|
+
|
|
46
|
+
def extract_header(self, code: str) -> str | None:
|
|
47
|
+
"""Extract YAML frontmatter from Markdown file."""
|
|
48
|
+
if not code or not code.strip():
|
|
49
|
+
return None
|
|
50
|
+
|
|
51
|
+
match = self.FRONTMATTER_PATTERN.match(code)
|
|
52
|
+
return match.group(1).strip() if match else None
|
|
53
|
+
|
|
54
|
+
def parse_fields(self, header: str) -> dict[str, str]:
|
|
55
|
+
"""Parse YAML frontmatter into field dictionary."""
|
|
56
|
+
yaml_result = self._try_yaml_parse(header)
|
|
57
|
+
if yaml_result is not None:
|
|
58
|
+
return yaml_result
|
|
59
|
+
|
|
60
|
+
return self._parse_simple_yaml(header)
|
|
61
|
+
|
|
62
|
+
def _try_yaml_parse(self, header: str) -> dict[str, str] | None:
|
|
63
|
+
"""Try to parse with PyYAML, returning None if unavailable or failed."""
|
|
64
|
+
try:
|
|
65
|
+
import yaml
|
|
66
|
+
|
|
67
|
+
data = yaml.safe_load(header)
|
|
68
|
+
if isinstance(data, dict):
|
|
69
|
+
return self._flatten_yaml_dict(data)
|
|
70
|
+
except ImportError:
|
|
71
|
+
logger.debug("PyYAML not available, using simple parser")
|
|
72
|
+
except Exception: # noqa: BLE001
|
|
73
|
+
logger.debug("YAML parsing failed, falling back to simple parser")
|
|
74
|
+
return None
|
|
75
|
+
|
|
76
|
+
def _flatten_yaml_dict(self, data: dict) -> dict[str, str]:
|
|
77
|
+
"""Convert YAML dict to string values."""
|
|
78
|
+
result: dict[str, str] = {}
|
|
79
|
+
for key, value in data.items():
|
|
80
|
+
result[str(key)] = self._convert_value(value)
|
|
81
|
+
return result
|
|
82
|
+
|
|
83
|
+
def _convert_value(self, value: object) -> str:
|
|
84
|
+
"""Convert a single YAML value to string."""
|
|
85
|
+
if isinstance(value, list):
|
|
86
|
+
return ", ".join(str(v) for v in value)
|
|
87
|
+
if value is not None:
|
|
88
|
+
return str(value)
|
|
89
|
+
return ""
|
|
90
|
+
|
|
91
|
+
def _parse_simple_yaml( # thailint: ignore[nesting,dry]
|
|
92
|
+
self, header: str
|
|
93
|
+
) -> dict[str, str]:
|
|
94
|
+
"""Simple regex-based YAML parsing fallback."""
|
|
95
|
+
fields: dict[str, str] = {}
|
|
96
|
+
current_field: str | None = None
|
|
97
|
+
current_value: list[str] = []
|
|
98
|
+
|
|
99
|
+
for line in header.split("\n"):
|
|
100
|
+
if self._is_field_start(line):
|
|
101
|
+
self._save_field(fields, current_field, current_value)
|
|
102
|
+
current_field, current_value = self._start_field(line)
|
|
103
|
+
elif current_field and line.strip():
|
|
104
|
+
current_value.append(self._process_continuation(line))
|
|
105
|
+
|
|
106
|
+
self._save_field(fields, current_field, current_value)
|
|
107
|
+
return fields
|
|
108
|
+
|
|
109
|
+
def _is_field_start(self, line: str) -> bool:
|
|
110
|
+
"""Check if line starts a new field (not indented, has colon)."""
|
|
111
|
+
return not line.startswith(" ") and ":" in line
|
|
112
|
+
|
|
113
|
+
def _start_field(self, line: str) -> tuple[str, list[str]]:
|
|
114
|
+
"""Parse field start and return field name and initial value."""
|
|
115
|
+
parts = line.split(":", 1)
|
|
116
|
+
field_name = parts[0].strip()
|
|
117
|
+
value = parts[1].strip() if len(parts) > 1 else ""
|
|
118
|
+
return field_name, [value] if value else []
|
|
119
|
+
|
|
120
|
+
def _process_continuation(self, line: str) -> str:
|
|
121
|
+
"""Process a continuation line (list item or multiline value)."""
|
|
122
|
+
stripped = line.strip()
|
|
123
|
+
return stripped[2:] if stripped.startswith("- ") else stripped
|
|
124
|
+
|
|
125
|
+
def _save_field(
|
|
126
|
+
self, fields: dict[str, str], field_name: str | None, values: list[str]
|
|
127
|
+
) -> None:
|
|
128
|
+
"""Save field to dictionary if field name exists."""
|
|
129
|
+
if field_name:
|
|
130
|
+
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
|
-
|
|
10
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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)
|