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,71 @@
1
+ """
2
+ Purpose: Type definitions for lazy-ignores linter
3
+
4
+ Scope: Data structures for ignore directives and suppression entries
5
+
6
+ Overview: Defines core types for the lazy-ignores linter including IgnoreType enum for
7
+ categorizing different suppression patterns, IgnoreDirective dataclass for representing
8
+ detected ignores in code, and SuppressionEntry dataclass for representing declared
9
+ suppressions in file headers. Supports Python (noqa, type:ignore, pylint, nosec, pyright),
10
+ TypeScript (@ts-ignore, eslint-disable), thai-lint (thailint:ignore), and test skip
11
+ patterns (pytest.mark.skip, it.skip, describe.skip).
12
+
13
+ Dependencies: dataclasses, enum, pathlib
14
+
15
+ Exports: IgnoreType, IgnoreDirective, SuppressionEntry
16
+
17
+ Interfaces: Frozen dataclasses for immutable ignore representation
18
+
19
+ Implementation: Enum-based categorization with frozen dataclasses for thread safety
20
+ """
21
+
22
+ from dataclasses import dataclass
23
+ from enum import Enum
24
+ from pathlib import Path
25
+
26
+
27
+ class IgnoreType(Enum):
28
+ """Type of linting ignore directive."""
29
+
30
+ NOQA = "noqa"
31
+ TYPE_IGNORE = "type:ignore"
32
+ PYLINT_DISABLE = "pylint:disable"
33
+ NOSEC = "nosec"
34
+ PYRIGHT_IGNORE = "pyright:ignore"
35
+ TS_IGNORE = "ts-ignore"
36
+ TS_NOCHECK = "ts-nocheck"
37
+ TS_EXPECT_ERROR = "ts-expect-error"
38
+ ESLINT_DISABLE = "eslint-disable"
39
+ THAILINT_IGNORE = "thailint:ignore"
40
+ THAILINT_IGNORE_FILE = "thailint:ignore-file"
41
+ THAILINT_IGNORE_NEXT = "thailint:ignore-next-line"
42
+ THAILINT_IGNORE_BLOCK = "thailint:ignore-start"
43
+ # DRY ignore patterns
44
+ DRY_IGNORE_BLOCK = "dry:ignore-block"
45
+ # Test skip patterns
46
+ PYTEST_SKIP = "pytest:skip"
47
+ PYTEST_SKIPIF = "pytest:skipif"
48
+ JEST_SKIP = "jest:skip"
49
+ MOCHA_SKIP = "mocha:skip"
50
+
51
+
52
+ @dataclass(frozen=True)
53
+ class IgnoreDirective:
54
+ """Represents a linting ignore found in code."""
55
+
56
+ ignore_type: IgnoreType
57
+ rule_ids: tuple[str, ...] # Can have multiple: noqa: PLR0912, PLR0915
58
+ line: int
59
+ column: int
60
+ raw_text: str # Original comment text
61
+ file_path: Path
62
+ inline_justification: str | None = None # Justification after " - " delimiter
63
+
64
+
65
+ @dataclass(frozen=True)
66
+ class SuppressionEntry:
67
+ """Represents a suppression declared in file header."""
68
+
69
+ rule_id: str # Normalized rule ID
70
+ justification: str
71
+ raw_text: str # Original header line
@@ -0,0 +1,146 @@
1
+ """
2
+ Purpose: Detect TypeScript/JavaScript linting ignore directives in source code
3
+
4
+ Scope: @ts-ignore, @ts-nocheck, @ts-expect-error, eslint-disable pattern detection
5
+
6
+ Overview: Provides TypeScriptIgnoreDetector class that scans TypeScript and JavaScript source
7
+ code for common linting ignore patterns. Detects TypeScript-specific patterns (@ts-ignore,
8
+ @ts-nocheck, @ts-expect-error) and ESLint patterns (eslint-disable-next-line, eslint-disable
9
+ block comments, eslint-disable-line). Handles both single-line (//) and block (/* */)
10
+ comment styles. Returns list of IgnoreDirective objects with line/column positions for
11
+ violation reporting.
12
+
13
+ Dependencies: re for pattern matching, pathlib for file paths, types module for dataclasses
14
+
15
+ Exports: TypeScriptIgnoreDetector
16
+
17
+ Interfaces: find_ignores(code: str, file_path: Path | str | None) -> list[IgnoreDirective]
18
+
19
+ Implementation: Regex-based line-by-line scanning with pattern-specific rule ID extraction
20
+ """
21
+
22
+ import re
23
+ from pathlib import Path
24
+
25
+ from src.linters.lazy_ignores.directive_utils import create_directive, normalize_path
26
+ from src.linters.lazy_ignores.types import IgnoreDirective, IgnoreType
27
+
28
+
29
+ class TypeScriptIgnoreDetector:
30
+ """Detects TypeScript/JavaScript linting ignore directives in source code."""
31
+
32
+ # Regex patterns for each ignore type
33
+ # Single-line comment patterns (//)
34
+ SINGLE_LINE_PATTERNS: dict[IgnoreType, re.Pattern[str]] = {
35
+ IgnoreType.TS_IGNORE: re.compile(
36
+ r"//\s*@ts-ignore(?:\s|$)",
37
+ ),
38
+ IgnoreType.TS_NOCHECK: re.compile(
39
+ r"//\s*@ts-nocheck(?:\s|$)",
40
+ ),
41
+ IgnoreType.TS_EXPECT_ERROR: re.compile(
42
+ r"//\s*@ts-expect-error(?:\s|$)",
43
+ ),
44
+ IgnoreType.THAILINT_IGNORE: re.compile(
45
+ r"//\s*thailint:\s*ignore(?!-)(?:\[([^\]]+)\])?",
46
+ re.IGNORECASE,
47
+ ),
48
+ IgnoreType.THAILINT_IGNORE_FILE: re.compile(
49
+ r"//\s*thailint:\s*ignore-file(?:\[([^\]]+)\])?",
50
+ re.IGNORECASE,
51
+ ),
52
+ }
53
+
54
+ # ESLint patterns (can be single-line or inline)
55
+ ESLINT_PATTERNS: dict[str, re.Pattern[str]] = {
56
+ "next-line": re.compile(
57
+ r"//\s*eslint-disable-next-line(?:\s+([a-zA-Z0-9\-/,\s]+))?(?:\s|$)",
58
+ ),
59
+ "inline": re.compile(
60
+ r"//\s*eslint-disable-line(?:\s+([a-zA-Z0-9\-/,\s]+))?(?:\s|$)",
61
+ ),
62
+ "block-start": re.compile(
63
+ r"/\*\s*eslint-disable(?:\s+([a-zA-Z0-9\-/,\s]+))?\s*\*/",
64
+ ),
65
+ }
66
+
67
+ def find_ignores(self, code: str, file_path: Path | str | None = None) -> list[IgnoreDirective]:
68
+ """Find all TypeScript/JavaScript ignore directives in code.
69
+
70
+ Args:
71
+ code: TypeScript/JavaScript source code to scan
72
+ file_path: Optional path to the source file (Path or string)
73
+
74
+ Returns:
75
+ List of IgnoreDirective objects for each detected ignore pattern
76
+ """
77
+ directives: list[IgnoreDirective] = []
78
+ effective_path = normalize_path(file_path)
79
+
80
+ for line_num, line in enumerate(code.splitlines(), start=1):
81
+ directives.extend(self._scan_line(line, line_num, effective_path))
82
+
83
+ return directives
84
+
85
+ def _scan_line(self, line: str, line_num: int, file_path: Path) -> list[IgnoreDirective]:
86
+ """Scan a single line for ignore patterns.
87
+
88
+ Args:
89
+ line: Line of code to scan
90
+ line_num: 1-indexed line number
91
+ file_path: Path to the source file
92
+
93
+ Returns:
94
+ List of IgnoreDirective objects found on this line
95
+ """
96
+ found: list[IgnoreDirective] = []
97
+
98
+ # Check TypeScript-specific patterns
99
+ found.extend(self._scan_typescript_patterns(line, line_num, file_path))
100
+
101
+ # Check ESLint patterns
102
+ found.extend(self._scan_eslint_patterns(line, line_num, file_path))
103
+
104
+ return found
105
+
106
+ def _scan_typescript_patterns(
107
+ self, line: str, line_num: int, file_path: Path
108
+ ) -> list[IgnoreDirective]:
109
+ """Scan for TypeScript-specific ignore patterns.
110
+
111
+ Args:
112
+ line: Line of code to scan
113
+ line_num: 1-indexed line number
114
+ file_path: Path to the source file
115
+
116
+ Returns:
117
+ List of IgnoreDirective objects for TypeScript patterns
118
+ """
119
+ found: list[IgnoreDirective] = []
120
+ for ignore_type, pattern in self.SINGLE_LINE_PATTERNS.items():
121
+ match = pattern.search(line)
122
+ if match:
123
+ found.append(create_directive(match, ignore_type, line_num, file_path))
124
+ return found
125
+
126
+ def _scan_eslint_patterns(
127
+ self, line: str, line_num: int, file_path: Path
128
+ ) -> list[IgnoreDirective]:
129
+ """Scan for ESLint disable patterns.
130
+
131
+ Args:
132
+ line: Line of code to scan
133
+ line_num: 1-indexed line number
134
+ file_path: Path to the source file
135
+
136
+ Returns:
137
+ List of IgnoreDirective objects for ESLint patterns
138
+ """
139
+ found: list[IgnoreDirective] = []
140
+ for pattern in self.ESLINT_PATTERNS.values():
141
+ match = pattern.search(line)
142
+ if match:
143
+ found.append(
144
+ create_directive(match, IgnoreType.ESLINT_DISABLE, line_num, file_path)
145
+ )
146
+ return found
@@ -0,0 +1,135 @@
1
+ """
2
+ Purpose: Build agent-friendly violation messages for lazy-ignores linter
3
+
4
+ Scope: Violation construction for unjustified ignores and orphaned header suppressions
5
+
6
+ Overview: Provides functions to construct Violation objects with AI-agent-friendly error
7
+ messages. Messages include explicit guidance for adding Suppressions section entries to
8
+ file headers and emphasize the requirement for human approval before adding suppressions.
9
+ Designed to help AI coding assistants understand the proper workflow for handling linting
10
+ suppressions rather than blindly adding ignore directives.
11
+
12
+ Dependencies: src.core.types for Violation dataclass
13
+
14
+ Exports: build_unjustified_violation, build_orphaned_violation
15
+
16
+ Interfaces: Two builder functions returning Violation objects
17
+
18
+ Implementation: Template-based message construction with rule ID formatting
19
+ """
20
+
21
+ from src.core.types import Severity, Violation
22
+
23
+
24
+ def build_unjustified_violation(
25
+ file_path: str,
26
+ line: int,
27
+ column: int,
28
+ rule_id: str,
29
+ raw_text: str,
30
+ ) -> Violation:
31
+ """Create violation for an ignore directive without header justification.
32
+
33
+ Args:
34
+ file_path: Path to the file containing the violation.
35
+ line: Line number where the ignore was found (1-indexed).
36
+ column: Column number where the ignore starts (0-indexed).
37
+ rule_id: The rule ID(s) being suppressed (e.g., "PLR0912").
38
+ raw_text: The raw ignore directive text found in code.
39
+
40
+ Returns:
41
+ Violation object with agent-friendly guidance message.
42
+ """
43
+ message = (
44
+ f"Unjustified suppression found: {raw_text} "
45
+ f"(ASK PERMISSION before adding Suppressions header)"
46
+ )
47
+
48
+ suggestion = _build_unjustified_suggestion(rule_id)
49
+
50
+ return Violation(
51
+ rule_id="lazy-ignores.unjustified",
52
+ file_path=file_path,
53
+ line=line,
54
+ column=column,
55
+ message=message,
56
+ severity=Severity.ERROR,
57
+ suggestion=suggestion,
58
+ )
59
+
60
+
61
+ def _build_unjustified_suggestion(rule_id: str) -> str:
62
+ """Build the suggestion text for unjustified violations.
63
+
64
+ Args:
65
+ rule_id: The rule ID(s) being suppressed.
66
+
67
+ Returns:
68
+ Formatted suggestion string with header instructions.
69
+ """
70
+ # Handle multiple rules (e.g., "PLR0912, PLR0915")
71
+ rule_ids = [r.strip() for r in rule_id.split(",")]
72
+
73
+ suppression_entries = "\n".join(f" {rid}: [Your justification here]" for rid in rule_ids)
74
+
75
+ return f"""To fix, either:
76
+
77
+ 1. Add an inline justification (10+ chars) after the ignore directive:
78
+ # noqa: {rule_ids[0]} - [Your justification here]
79
+
80
+ 2. Or add an entry to the file header Suppressions section:
81
+ Suppressions:
82
+ {suppression_entries}
83
+
84
+ IMPORTANT: Adding suppressions requires human approval.
85
+ Do not add this entry without explicit permission from a human reviewer.
86
+ Ask first, then add if approved."""
87
+
88
+
89
+ def build_orphaned_violation(
90
+ file_path: str,
91
+ header_line: int,
92
+ rule_id: str,
93
+ justification: str,
94
+ ) -> Violation:
95
+ """Create violation for a header entry without matching code ignore.
96
+
97
+ Args:
98
+ file_path: Path to the file containing the orphaned entry.
99
+ header_line: Line number of the suppression in the header (1-indexed).
100
+ rule_id: The orphaned rule ID from the header.
101
+ justification: The justification text from the header.
102
+
103
+ Returns:
104
+ Violation object suggesting removal of the orphaned entry.
105
+ """
106
+ message = f"Orphaned suppression in header: {rule_id}: {justification}"
107
+
108
+ suggestion = _build_orphaned_suggestion(rule_id)
109
+
110
+ return Violation(
111
+ rule_id="lazy-ignores.orphaned",
112
+ file_path=file_path,
113
+ line=header_line,
114
+ column=0,
115
+ message=message,
116
+ severity=Severity.ERROR,
117
+ suggestion=suggestion,
118
+ )
119
+
120
+
121
+ def _build_orphaned_suggestion(rule_id: str) -> str:
122
+ """Build the suggestion text for orphaned violations.
123
+
124
+ Args:
125
+ rule_id: The orphaned rule ID.
126
+
127
+ Returns:
128
+ Formatted suggestion string with removal instructions.
129
+ """
130
+ return f"""This rule is declared in the Suppressions section but no matching
131
+ ignore directive was found in the code.
132
+
133
+ Either:
134
+ 1. Remove the entry for {rule_id} from the Suppressions section if the ignore was removed from code
135
+ 2. Add the ignore directive if it's missing from the code"""
@@ -0,0 +1,31 @@
1
+ """
2
+ Purpose: LBYL (Look Before You Leap) linter package exports
3
+
4
+ Scope: Detect LBYL anti-patterns in Python code and suggest EAFP alternatives
5
+
6
+ Overview: Package providing LBYL pattern detection for Python code. Identifies common
7
+ anti-patterns where explicit checks are performed before operations (e.g., if key in
8
+ dict before dict[key]) and suggests EAFP (Easier to Ask Forgiveness than Permission)
9
+ alternatives using try/except blocks. Supports 8 pattern types including dict key
10
+ checking, hasattr, isinstance, file exists, length checks, None checks, string
11
+ validation, and division safety checks.
12
+
13
+ Dependencies: ast module for Python parsing, src.core for base classes
14
+
15
+ Exports: LBYLConfig, LBYLPattern, BaseLBYLDetector, LBYLRule
16
+
17
+ Interfaces: LBYLConfig.from_dict() for YAML configuration loading
18
+
19
+ Implementation: AST-based pattern detection with configurable pattern toggles
20
+ """
21
+
22
+ from .config import LBYLConfig
23
+ from .linter import LBYLRule
24
+ from .pattern_detectors.base import BaseLBYLDetector, LBYLPattern
25
+
26
+ __all__ = [
27
+ "LBYLConfig",
28
+ "LBYLPattern",
29
+ "BaseLBYLDetector",
30
+ "LBYLRule",
31
+ ]
@@ -0,0 +1,63 @@
1
+ """
2
+ Purpose: Configuration dataclass for LBYL linter
3
+
4
+ Scope: Pattern toggles, ignore patterns, and validation
5
+
6
+ Overview: Provides LBYLConfig dataclass with pattern-specific toggles for each LBYL
7
+ pattern type (dict_key, hasattr, isinstance, file_exists, len_check, none_check,
8
+ string_validation, division_check). Some patterns like isinstance and none_check
9
+ are disabled by default due to many valid use cases. Configuration can be loaded
10
+ from dictionary (YAML) with sensible defaults.
11
+
12
+ Dependencies: dataclasses, typing
13
+
14
+ Exports: LBYLConfig
15
+
16
+ Interfaces: LBYLConfig.from_dict() for YAML configuration loading
17
+
18
+ Implementation: Dataclass with factory defaults and conservative default settings
19
+
20
+ Suppressions:
21
+ too-many-instance-attributes: Configuration dataclass requires many toggles
22
+ """
23
+
24
+ from dataclasses import dataclass, field
25
+ from typing import Any
26
+
27
+
28
+ @dataclass
29
+ class LBYLConfig: # pylint: disable=too-many-instance-attributes
30
+ """Configuration for LBYL linter."""
31
+
32
+ enabled: bool = True
33
+
34
+ # Pattern toggles
35
+ detect_dict_key: bool = True
36
+ detect_hasattr: bool = True
37
+ detect_isinstance: bool = False # Disabled - many valid uses for type narrowing
38
+ detect_file_exists: bool = True
39
+ detect_len_check: bool = True
40
+ detect_none_check: bool = False # Disabled - many valid uses
41
+ detect_string_validation: bool = True
42
+ detect_division_check: bool = True
43
+
44
+ # File patterns to ignore
45
+ ignore: list[str] = field(default_factory=list)
46
+
47
+ @classmethod
48
+ def from_dict(cls, config: dict[str, Any], language: str | None = None) -> "LBYLConfig":
49
+ """Load configuration from dictionary."""
50
+ # Language parameter reserved for future multi-language support
51
+ _ = language
52
+ return cls(
53
+ enabled=config.get("enabled", True),
54
+ detect_dict_key=config.get("detect_dict_key", True),
55
+ detect_hasattr=config.get("detect_hasattr", True),
56
+ detect_isinstance=config.get("detect_isinstance", False),
57
+ detect_file_exists=config.get("detect_file_exists", True),
58
+ detect_len_check=config.get("detect_len_check", True),
59
+ detect_none_check=config.get("detect_none_check", False),
60
+ detect_string_validation=config.get("detect_string_validation", True),
61
+ detect_division_check=config.get("detect_division_check", True),
62
+ ignore=config.get("ignore", []),
63
+ )
@@ -0,0 +1,67 @@
1
+ """
2
+ Purpose: Main LBYL linter rule implementing PythonOnlyLintRule interface
3
+
4
+ Scope: Entry point for LBYL anti-pattern detection in Python code
5
+
6
+ Overview: Provides LBYLRule class that implements the PythonOnlyLintRule interface for
7
+ detecting Look Before You Leap anti-patterns in Python code. Validates that files
8
+ are Python with content, loads configuration, and delegates analysis to
9
+ PythonLBYLAnalyzer. Returns violations with EAFP suggestions for detected patterns.
10
+ Supports disabling via configuration and pattern-specific toggles.
11
+
12
+ Dependencies: PythonOnlyLintRule, PythonLBYLAnalyzer, LBYLConfig
13
+
14
+ Exports: LBYLRule
15
+
16
+ Interfaces: check(context: BaseLintContext) -> list[Violation]
17
+
18
+ Implementation: Single-file analysis with config-driven pattern detection
19
+ """
20
+
21
+ from src.core.python_lint_rule import PythonOnlyLintRule
22
+ from src.core.types import Violation
23
+
24
+ from .config import LBYLConfig
25
+ from .python_analyzer import PythonLBYLAnalyzer
26
+
27
+
28
+ class LBYLRule(PythonOnlyLintRule[LBYLConfig]):
29
+ """Detects Look Before You Leap anti-patterns in Python code."""
30
+
31
+ def __init__(self, config: LBYLConfig | None = None) -> None:
32
+ """Initialize the LBYL rule."""
33
+ super().__init__(config)
34
+ self._analyzer = PythonLBYLAnalyzer()
35
+
36
+ @property
37
+ def rule_id(self) -> str:
38
+ """Unique identifier for this rule."""
39
+ return "lbyl"
40
+
41
+ @property
42
+ def rule_name(self) -> str:
43
+ """Human-readable name for this rule."""
44
+ return "Look Before You Leap"
45
+
46
+ @property
47
+ def description(self) -> str:
48
+ """Description of what this rule checks."""
49
+ return (
50
+ "Detects LBYL (Look Before You Leap) anti-patterns that should be "
51
+ "refactored to EAFP (Easier to Ask Forgiveness than Permission) style "
52
+ "using try/except blocks."
53
+ )
54
+
55
+ @property
56
+ def _config_key(self) -> str:
57
+ """Configuration key in metadata."""
58
+ return "lbyl"
59
+
60
+ @property
61
+ def _config_class(self) -> type[LBYLConfig]:
62
+ """Configuration class type."""
63
+ return LBYLConfig
64
+
65
+ def _analyze(self, code: str, file_path: str, config: LBYLConfig) -> list[Violation]:
66
+ """Analyze code for LBYL violations."""
67
+ return self._analyzer.analyze(code, file_path, config)
@@ -0,0 +1,53 @@
1
+ """
2
+ Purpose: Pattern detector exports for LBYL linter
3
+
4
+ Scope: All AST-based pattern detectors for LBYL anti-pattern detection
5
+
6
+ Overview: Exports pattern detector classes for the LBYL linter. Each detector is an
7
+ AST NodeVisitor that identifies specific LBYL anti-patterns. Detectors include
8
+ dict key checking, hasattr, isinstance, file exists, length checks, None checks,
9
+ string validators, and division zero-checks.
10
+
11
+ Dependencies: ast module, base detector class
12
+
13
+ Exports: BaseLBYLDetector, LBYLPattern, DictKeyDetector, DictKeyPattern, HasattrDetector,
14
+ HasattrPattern, IsinstanceDetector, IsinstancePattern, FileExistsDetector,
15
+ FileExistsPattern, LenCheckDetector, LenCheckPattern, NoneCheckDetector,
16
+ NoneCheckPattern, StringValidatorDetector, StringValidatorPattern,
17
+ DivisionCheckDetector, DivisionCheckPattern
18
+
19
+ Interfaces: find_patterns(tree: ast.AST) -> list[LBYLPattern]
20
+
21
+ Implementation: Modular detector pattern for extensible LBYL detection
22
+ """
23
+
24
+ from .base import BaseLBYLDetector, LBYLPattern
25
+ from .dict_key_detector import DictKeyDetector, DictKeyPattern
26
+ from .division_check_detector import DivisionCheckDetector, DivisionCheckPattern
27
+ from .file_exists_detector import FileExistsDetector, FileExistsPattern
28
+ from .hasattr_detector import HasattrDetector, HasattrPattern
29
+ from .isinstance_detector import IsinstanceDetector, IsinstancePattern
30
+ from .len_check_detector import LenCheckDetector, LenCheckPattern
31
+ from .none_check_detector import NoneCheckDetector, NoneCheckPattern
32
+ from .string_validator_detector import StringValidatorDetector, StringValidatorPattern
33
+
34
+ __all__ = [
35
+ "BaseLBYLDetector",
36
+ "LBYLPattern",
37
+ "DictKeyDetector",
38
+ "DictKeyPattern",
39
+ "DivisionCheckDetector",
40
+ "DivisionCheckPattern",
41
+ "FileExistsDetector",
42
+ "FileExistsPattern",
43
+ "HasattrDetector",
44
+ "HasattrPattern",
45
+ "IsinstanceDetector",
46
+ "IsinstancePattern",
47
+ "LenCheckDetector",
48
+ "LenCheckPattern",
49
+ "NoneCheckDetector",
50
+ "NoneCheckPattern",
51
+ "StringValidatorDetector",
52
+ "StringValidatorPattern",
53
+ ]
@@ -0,0 +1,63 @@
1
+ """
2
+ Purpose: Base class for LBYL pattern detectors
3
+
4
+ Scope: Abstract base providing common detector interface
5
+
6
+ Overview: Defines BaseLBYLDetector abstract class that all pattern detectors extend.
7
+ Inherits from ast.NodeVisitor for AST traversal. Defines LBYLPattern base dataclass
8
+ for representing detected patterns with line number and column information. Each
9
+ concrete detector implements find_patterns() to identify specific LBYL anti-patterns.
10
+ Uses Generic TypeVar for type-safe subclass pattern storage.
11
+
12
+ Dependencies: abc, ast, dataclasses, typing
13
+
14
+ Exports: BaseLBYLDetector, LBYLPattern
15
+
16
+ Interfaces: find_patterns(tree: ast.AST) -> list[LBYLPattern]
17
+
18
+ Implementation: Abstract base with NodeVisitor pattern for extensibility, Generic for type safety
19
+ """
20
+
21
+ import ast
22
+ from abc import ABC
23
+ from dataclasses import dataclass
24
+ from typing import Generic, TypeVar
25
+
26
+
27
+ @dataclass
28
+ class LBYLPattern:
29
+ """Base pattern data for detected LBYL anti-patterns."""
30
+
31
+ line_number: int
32
+ column: int
33
+
34
+
35
+ PatternT = TypeVar("PatternT", bound=LBYLPattern)
36
+
37
+
38
+ class BaseLBYLDetector(ast.NodeVisitor, ABC, Generic[PatternT]):
39
+ """Base class for LBYL pattern detectors.
40
+
41
+ Subclasses must initialize self._patterns as an empty list in __init__
42
+ and populate it in visit methods. The _patterns attribute stores subclass-
43
+ specific pattern types (DictKeyPattern, HasattrPattern, etc.) which all
44
+ inherit from LBYLPattern.
45
+
46
+ Type Parameters:
47
+ PatternT: The specific pattern type used by this detector
48
+ """
49
+
50
+ _patterns: list[PatternT]
51
+
52
+ def find_patterns(self, tree: ast.AST) -> list[LBYLPattern]:
53
+ """Find LBYL patterns in AST.
54
+
55
+ Args:
56
+ tree: Python AST to analyze
57
+
58
+ Returns:
59
+ List of detected LBYL patterns
60
+ """
61
+ self._patterns = []
62
+ self.visit(tree)
63
+ return list(self._patterns)