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.
Files changed (91) hide show
  1. src/__init__.py +7 -2
  2. src/analyzers/__init__.py +23 -0
  3. src/analyzers/typescript_base.py +148 -0
  4. src/api.py +1 -1
  5. src/cli.py +1111 -144
  6. src/config.py +12 -33
  7. src/core/base.py +102 -5
  8. src/core/cli_utils.py +206 -0
  9. src/core/config_parser.py +126 -0
  10. src/core/linter_utils.py +168 -0
  11. src/core/registry.py +17 -92
  12. src/core/rule_discovery.py +132 -0
  13. src/core/violation_builder.py +122 -0
  14. src/linter_config/ignore.py +112 -40
  15. src/linter_config/loader.py +3 -13
  16. src/linters/dry/__init__.py +23 -0
  17. src/linters/dry/base_token_analyzer.py +76 -0
  18. src/linters/dry/block_filter.py +265 -0
  19. src/linters/dry/block_grouper.py +59 -0
  20. src/linters/dry/cache.py +172 -0
  21. src/linters/dry/cache_query.py +61 -0
  22. src/linters/dry/config.py +134 -0
  23. src/linters/dry/config_loader.py +44 -0
  24. src/linters/dry/deduplicator.py +120 -0
  25. src/linters/dry/duplicate_storage.py +63 -0
  26. src/linters/dry/file_analyzer.py +90 -0
  27. src/linters/dry/inline_ignore.py +140 -0
  28. src/linters/dry/linter.py +163 -0
  29. src/linters/dry/python_analyzer.py +668 -0
  30. src/linters/dry/storage_initializer.py +42 -0
  31. src/linters/dry/token_hasher.py +169 -0
  32. src/linters/dry/typescript_analyzer.py +592 -0
  33. src/linters/dry/violation_builder.py +74 -0
  34. src/linters/dry/violation_filter.py +94 -0
  35. src/linters/dry/violation_generator.py +174 -0
  36. src/linters/file_header/__init__.py +24 -0
  37. src/linters/file_header/atemporal_detector.py +87 -0
  38. src/linters/file_header/config.py +66 -0
  39. src/linters/file_header/field_validator.py +69 -0
  40. src/linters/file_header/linter.py +313 -0
  41. src/linters/file_header/python_parser.py +86 -0
  42. src/linters/file_header/violation_builder.py +78 -0
  43. src/linters/file_placement/config_loader.py +86 -0
  44. src/linters/file_placement/directory_matcher.py +80 -0
  45. src/linters/file_placement/linter.py +262 -471
  46. src/linters/file_placement/path_resolver.py +61 -0
  47. src/linters/file_placement/pattern_matcher.py +55 -0
  48. src/linters/file_placement/pattern_validator.py +106 -0
  49. src/linters/file_placement/rule_checker.py +229 -0
  50. src/linters/file_placement/violation_factory.py +177 -0
  51. src/linters/magic_numbers/__init__.py +48 -0
  52. src/linters/magic_numbers/config.py +82 -0
  53. src/linters/magic_numbers/context_analyzer.py +247 -0
  54. src/linters/magic_numbers/linter.py +516 -0
  55. src/linters/magic_numbers/python_analyzer.py +76 -0
  56. src/linters/magic_numbers/typescript_analyzer.py +218 -0
  57. src/linters/magic_numbers/violation_builder.py +98 -0
  58. src/linters/nesting/__init__.py +6 -2
  59. src/linters/nesting/config.py +17 -4
  60. src/linters/nesting/linter.py +81 -168
  61. src/linters/nesting/typescript_analyzer.py +39 -102
  62. src/linters/nesting/typescript_function_extractor.py +130 -0
  63. src/linters/nesting/violation_builder.py +139 -0
  64. src/linters/print_statements/__init__.py +53 -0
  65. src/linters/print_statements/config.py +83 -0
  66. src/linters/print_statements/linter.py +430 -0
  67. src/linters/print_statements/python_analyzer.py +155 -0
  68. src/linters/print_statements/typescript_analyzer.py +135 -0
  69. src/linters/print_statements/violation_builder.py +98 -0
  70. src/linters/srp/__init__.py +99 -0
  71. src/linters/srp/class_analyzer.py +113 -0
  72. src/linters/srp/config.py +82 -0
  73. src/linters/srp/heuristics.py +89 -0
  74. src/linters/srp/linter.py +234 -0
  75. src/linters/srp/metrics_evaluator.py +47 -0
  76. src/linters/srp/python_analyzer.py +72 -0
  77. src/linters/srp/typescript_analyzer.py +75 -0
  78. src/linters/srp/typescript_metrics_calculator.py +90 -0
  79. src/linters/srp/violation_builder.py +117 -0
  80. src/orchestrator/core.py +54 -9
  81. src/templates/thailint_config_template.yaml +158 -0
  82. src/utils/__init__.py +4 -0
  83. src/utils/project_root.py +203 -0
  84. thailint-0.5.0.dist-info/METADATA +1286 -0
  85. thailint-0.5.0.dist-info/RECORD +96 -0
  86. {thailint-0.1.5.dist-info → thailint-0.5.0.dist-info}/WHEEL +1 -1
  87. src/.ai/layout.yaml +0 -48
  88. thailint-0.1.5.dist-info/METADATA +0 -629
  89. thailint-0.1.5.dist-info/RECORD +0 -28
  90. {thailint-0.1.5.dist-info → thailint-0.5.0.dist-info}/entry_points.txt +0 -0
  91. {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