thailint 0.2.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 (214) 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 +44 -27
  23. src/core/base.py +95 -5
  24. src/core/cli_utils.py +19 -2
  25. src/core/config_parser.py +36 -6
  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 +125 -22
  65. src/linters/dry/block_grouper.py +4 -0
  66. src/linters/dry/cache.py +142 -94
  67. src/linters/dry/cache_query.py +4 -0
  68. src/linters/dry/config.py +68 -21
  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 +20 -82
  73. src/linters/dry/file_analyzer.py +15 -50
  74. src/linters/dry/inline_ignore.py +7 -16
  75. src/linters/dry/linter.py +182 -54
  76. src/linters/dry/python_analyzer.py +108 -336
  77. src/linters/dry/python_constant_extractor.py +100 -0
  78. src/linters/dry/single_statement_detector.py +417 -0
  79. src/linters/dry/storage_initializer.py +9 -18
  80. src/linters/dry/token_hasher.py +129 -71
  81. src/linters/dry/typescript_analyzer.py +68 -380
  82. src/linters/dry/typescript_constant_extractor.py +138 -0
  83. src/linters/dry/typescript_statement_detector.py +255 -0
  84. src/linters/dry/typescript_value_extractor.py +70 -0
  85. src/linters/dry/violation_builder.py +4 -0
  86. src/linters/dry/violation_filter.py +9 -5
  87. src/linters/dry/violation_generator.py +71 -14
  88. src/linters/file_header/__init__.py +24 -0
  89. src/linters/file_header/atemporal_detector.py +105 -0
  90. src/linters/file_header/base_parser.py +93 -0
  91. src/linters/file_header/bash_parser.py +66 -0
  92. src/linters/file_header/config.py +140 -0
  93. src/linters/file_header/css_parser.py +70 -0
  94. src/linters/file_header/field_validator.py +72 -0
  95. src/linters/file_header/linter.py +309 -0
  96. src/linters/file_header/markdown_parser.py +130 -0
  97. src/linters/file_header/python_parser.py +42 -0
  98. src/linters/file_header/typescript_parser.py +73 -0
  99. src/linters/file_header/violation_builder.py +79 -0
  100. src/linters/file_placement/config_loader.py +3 -1
  101. src/linters/file_placement/directory_matcher.py +4 -0
  102. src/linters/file_placement/linter.py +74 -31
  103. src/linters/file_placement/pattern_matcher.py +41 -6
  104. src/linters/file_placement/pattern_validator.py +31 -12
  105. src/linters/file_placement/rule_checker.py +12 -7
  106. src/linters/lazy_ignores/__init__.py +43 -0
  107. src/linters/lazy_ignores/config.py +74 -0
  108. src/linters/lazy_ignores/directive_utils.py +164 -0
  109. src/linters/lazy_ignores/header_parser.py +177 -0
  110. src/linters/lazy_ignores/linter.py +158 -0
  111. src/linters/lazy_ignores/matcher.py +168 -0
  112. src/linters/lazy_ignores/python_analyzer.py +209 -0
  113. src/linters/lazy_ignores/rule_id_utils.py +180 -0
  114. src/linters/lazy_ignores/skip_detector.py +298 -0
  115. src/linters/lazy_ignores/types.py +71 -0
  116. src/linters/lazy_ignores/typescript_analyzer.py +146 -0
  117. src/linters/lazy_ignores/violation_builder.py +135 -0
  118. src/linters/lbyl/__init__.py +31 -0
  119. src/linters/lbyl/config.py +63 -0
  120. src/linters/lbyl/linter.py +67 -0
  121. src/linters/lbyl/pattern_detectors/__init__.py +53 -0
  122. src/linters/lbyl/pattern_detectors/base.py +63 -0
  123. src/linters/lbyl/pattern_detectors/dict_key_detector.py +107 -0
  124. src/linters/lbyl/pattern_detectors/division_check_detector.py +232 -0
  125. src/linters/lbyl/pattern_detectors/file_exists_detector.py +220 -0
  126. src/linters/lbyl/pattern_detectors/hasattr_detector.py +119 -0
  127. src/linters/lbyl/pattern_detectors/isinstance_detector.py +119 -0
  128. src/linters/lbyl/pattern_detectors/len_check_detector.py +173 -0
  129. src/linters/lbyl/pattern_detectors/none_check_detector.py +146 -0
  130. src/linters/lbyl/pattern_detectors/string_validator_detector.py +145 -0
  131. src/linters/lbyl/python_analyzer.py +215 -0
  132. src/linters/lbyl/violation_builder.py +354 -0
  133. src/linters/magic_numbers/__init__.py +48 -0
  134. src/linters/magic_numbers/config.py +82 -0
  135. src/linters/magic_numbers/context_analyzer.py +249 -0
  136. src/linters/magic_numbers/linter.py +462 -0
  137. src/linters/magic_numbers/python_analyzer.py +64 -0
  138. src/linters/magic_numbers/typescript_analyzer.py +215 -0
  139. src/linters/magic_numbers/typescript_ignore_checker.py +81 -0
  140. src/linters/magic_numbers/violation_builder.py +98 -0
  141. src/linters/method_property/__init__.py +49 -0
  142. src/linters/method_property/config.py +138 -0
  143. src/linters/method_property/linter.py +414 -0
  144. src/linters/method_property/python_analyzer.py +473 -0
  145. src/linters/method_property/violation_builder.py +119 -0
  146. src/linters/nesting/__init__.py +6 -2
  147. src/linters/nesting/config.py +6 -3
  148. src/linters/nesting/linter.py +31 -34
  149. src/linters/nesting/python_analyzer.py +4 -0
  150. src/linters/nesting/typescript_analyzer.py +6 -11
  151. src/linters/nesting/violation_builder.py +1 -0
  152. src/linters/performance/__init__.py +91 -0
  153. src/linters/performance/config.py +43 -0
  154. src/linters/performance/constants.py +49 -0
  155. src/linters/performance/linter.py +149 -0
  156. src/linters/performance/python_analyzer.py +365 -0
  157. src/linters/performance/regex_analyzer.py +312 -0
  158. src/linters/performance/regex_linter.py +139 -0
  159. src/linters/performance/typescript_analyzer.py +236 -0
  160. src/linters/performance/violation_builder.py +160 -0
  161. src/linters/print_statements/__init__.py +53 -0
  162. src/linters/print_statements/config.py +78 -0
  163. src/linters/print_statements/linter.py +413 -0
  164. src/linters/print_statements/python_analyzer.py +153 -0
  165. src/linters/print_statements/typescript_analyzer.py +125 -0
  166. src/linters/print_statements/violation_builder.py +96 -0
  167. src/linters/srp/__init__.py +3 -3
  168. src/linters/srp/class_analyzer.py +11 -7
  169. src/linters/srp/config.py +12 -6
  170. src/linters/srp/heuristics.py +56 -22
  171. src/linters/srp/linter.py +47 -39
  172. src/linters/srp/python_analyzer.py +55 -20
  173. src/linters/srp/typescript_metrics_calculator.py +110 -50
  174. src/linters/stateless_class/__init__.py +25 -0
  175. src/linters/stateless_class/config.py +58 -0
  176. src/linters/stateless_class/linter.py +349 -0
  177. src/linters/stateless_class/python_analyzer.py +290 -0
  178. src/linters/stringly_typed/__init__.py +36 -0
  179. src/linters/stringly_typed/config.py +189 -0
  180. src/linters/stringly_typed/context_filter.py +451 -0
  181. src/linters/stringly_typed/function_call_violation_builder.py +135 -0
  182. src/linters/stringly_typed/ignore_checker.py +100 -0
  183. src/linters/stringly_typed/ignore_utils.py +51 -0
  184. src/linters/stringly_typed/linter.py +376 -0
  185. src/linters/stringly_typed/python/__init__.py +33 -0
  186. src/linters/stringly_typed/python/analyzer.py +348 -0
  187. src/linters/stringly_typed/python/call_tracker.py +175 -0
  188. src/linters/stringly_typed/python/comparison_tracker.py +257 -0
  189. src/linters/stringly_typed/python/condition_extractor.py +134 -0
  190. src/linters/stringly_typed/python/conditional_detector.py +179 -0
  191. src/linters/stringly_typed/python/constants.py +21 -0
  192. src/linters/stringly_typed/python/match_analyzer.py +94 -0
  193. src/linters/stringly_typed/python/validation_detector.py +189 -0
  194. src/linters/stringly_typed/python/variable_extractor.py +96 -0
  195. src/linters/stringly_typed/storage.py +620 -0
  196. src/linters/stringly_typed/storage_initializer.py +45 -0
  197. src/linters/stringly_typed/typescript/__init__.py +28 -0
  198. src/linters/stringly_typed/typescript/analyzer.py +157 -0
  199. src/linters/stringly_typed/typescript/call_tracker.py +335 -0
  200. src/linters/stringly_typed/typescript/comparison_tracker.py +378 -0
  201. src/linters/stringly_typed/violation_generator.py +419 -0
  202. src/orchestrator/core.py +264 -16
  203. src/orchestrator/language_detector.py +5 -3
  204. src/templates/thailint_config_template.yaml +354 -0
  205. src/utils/project_root.py +138 -16
  206. thailint-0.15.3.dist-info/METADATA +187 -0
  207. thailint-0.15.3.dist-info/RECORD +226 -0
  208. {thailint-0.2.0.dist-info → thailint-0.15.3.dist-info}/WHEEL +1 -1
  209. thailint-0.15.3.dist-info/entry_points.txt +4 -0
  210. src/cli.py +0 -1055
  211. thailint-0.2.0.dist-info/METADATA +0 -980
  212. thailint-0.2.0.dist-info/RECORD +0 -75
  213. thailint-0.2.0.dist-info/entry_points.txt +0 -4
  214. {thailint-0.2.0.dist-info → thailint-0.15.3.dist-info/licenses}/LICENSE +0 -0
@@ -10,7 +10,7 @@ Overview: Provides automatic rule discovery functionality for the linter framewo
10
10
 
11
11
  Dependencies: importlib, inspect, pkgutil, BaseLintRule
12
12
 
13
- Exports: RuleDiscovery
13
+ Exports: discover_from_package function, RuleDiscovery class (compat)
14
14
 
15
15
  Interfaces: discover_from_package(package_path) -> list[BaseLintRule]
16
16
 
@@ -19,114 +19,177 @@ Implementation: Package traversal with pkgutil, class introspection with inspect
19
19
 
20
20
  import importlib
21
21
  import inspect
22
+ import logging
22
23
  import pkgutil
24
+ from types import ModuleType
23
25
  from typing import Any
24
26
 
25
27
  from .base import BaseLintRule
26
28
 
29
+ logger = logging.getLogger(__name__)
27
30
 
28
- class RuleDiscovery:
29
- """Discovers linting rules from Python packages."""
30
31
 
31
- def discover_from_package(self, package_path: str) -> list[BaseLintRule]:
32
- """Discover rules from a package and its modules.
32
+ def discover_from_package(package_path: str) -> list[BaseLintRule]:
33
+ """Discover rules from a package and its modules.
33
34
 
34
- Args:
35
- package_path: Python package path (e.g., 'src.linters')
35
+ Args:
36
+ package_path: Python package path (e.g., 'src.linters')
36
37
 
37
- Returns:
38
- List of discovered rule instances
39
- """
40
- try:
41
- package = importlib.import_module(package_path)
42
- except ImportError:
43
- return []
38
+ Returns:
39
+ List of discovered rule instances
40
+ """
41
+ try:
42
+ package = importlib.import_module(package_path)
43
+ except ImportError as e:
44
+ logger.debug("Failed to import package %s: %s", package_path, e)
45
+ return []
44
46
 
45
- if not hasattr(package, "__path__"):
46
- return self._discover_from_module(package_path)
47
+ if not hasattr(package, "__path__"):
48
+ return _discover_from_module(package_path)
47
49
 
48
- return self._discover_from_package_modules(package_path, package)
50
+ return _discover_from_package_modules(package_path, package)
49
51
 
50
- def _discover_from_package_modules(self, package_path: str, package: Any) -> list[BaseLintRule]:
51
- """Discover rules from all modules in a package.
52
52
 
53
- Args:
54
- package_path: Package path
55
- package: Imported package object
53
+ def _discover_from_package_modules(package_path: str, package: Any) -> list[BaseLintRule]:
54
+ """Discover rules from all modules in a package.
56
55
 
57
- Returns:
58
- List of discovered rules
59
- """
60
- rules = []
61
- for _, module_name, _ in pkgutil.iter_modules(package.__path__):
62
- full_module_name = f"{package_path}.{module_name}"
63
- module_rules = self._try_discover_from_module(full_module_name)
64
- rules.extend(module_rules)
65
- return rules
56
+ Args:
57
+ package_path: Package path
58
+ package: Imported package object
66
59
 
67
- def _try_discover_from_module(self, module_name: str) -> list[BaseLintRule]:
68
- """Try to discover rules from a module, return empty list on error.
60
+ Returns:
61
+ List of discovered rules
62
+ """
63
+ rules = []
64
+ for _, module_name, _ in pkgutil.iter_modules(package.__path__):
65
+ full_module_name = f"{package_path}.{module_name}"
66
+ module_rules = _try_discover_from_module(full_module_name)
67
+ rules.extend(module_rules)
68
+ return rules
69
69
 
70
- Args:
71
- module_name: Full module name
72
70
 
73
- Returns:
74
- List of discovered rules (empty on error)
75
- """
76
- try:
77
- return self._discover_from_module(module_name)
78
- except (ImportError, AttributeError):
79
- return []
71
+ def _try_discover_from_module(module_name: str) -> list[BaseLintRule]:
72
+ """Try to discover rules from a module, return empty list on error.
80
73
 
81
- def _discover_from_module(self, module_path: str) -> list[BaseLintRule]:
82
- """Discover rules from a specific module.
74
+ Args:
75
+ module_name: Full module name
83
76
 
84
- Args:
85
- module_path: Full module path to search
77
+ Returns:
78
+ List of discovered rules (empty on error)
79
+ """
80
+ try:
81
+ return _discover_from_module(module_name)
82
+ except (ImportError, AttributeError):
83
+ return []
86
84
 
87
- Returns:
88
- List of discovered rule instances
89
- """
90
- try:
91
- module = importlib.import_module(module_path)
92
- except (ImportError, AttributeError):
93
- return []
94
-
95
- rules = []
96
- for _name, obj in inspect.getmembers(module):
97
- if not self._is_rule_class(obj):
98
- continue
99
- rule_instance = self._try_instantiate_rule(obj)
100
- if rule_instance:
101
- rules.append(rule_instance)
102
- return rules
103
-
104
- def _try_instantiate_rule(self, rule_class: type[BaseLintRule]) -> BaseLintRule | None:
105
- """Try to instantiate a rule class.
106
85
 
107
- Args:
108
- rule_class: Rule class to instantiate
86
+ def _discover_from_module(module_path: str) -> list[BaseLintRule]:
87
+ """Discover rules from a specific module.
109
88
 
110
- Returns:
111
- Rule instance or None on error
112
- """
113
- try:
114
- return rule_class()
115
- except (TypeError, AttributeError):
116
- return None
89
+ Args:
90
+ module_path: Full module path to search
91
+
92
+ Returns:
93
+ List of discovered rule instances
94
+ """
95
+ module = _try_import_module(module_path)
96
+ if module is None:
97
+ return []
98
+ return _extract_rules_from_module(module)
99
+
100
+
101
+ def _try_import_module(module_path: str) -> ModuleType | None:
102
+ """Try to import a module, returning None on failure.
103
+
104
+ Args:
105
+ module_path: Full module path to import
106
+
107
+ Returns:
108
+ Module object or None if import fails
109
+ """
110
+ try:
111
+ return importlib.import_module(module_path)
112
+ except (ImportError, AttributeError):
113
+ return None
117
114
 
118
- def _is_rule_class(self, obj: Any) -> bool:
119
- """Check if an object is a valid rule class.
115
+
116
+ def _extract_rules_from_module(module: ModuleType) -> list[BaseLintRule]:
117
+ """Extract rule instances from a module.
118
+
119
+ Args:
120
+ module: Imported module to scan
121
+
122
+ Returns:
123
+ List of discovered rule instances
124
+ """
125
+ rule_classes = [obj for _name, obj in inspect.getmembers(module) if _is_rule_class(obj)]
126
+ return _instantiate_rules(rule_classes)
127
+
128
+
129
+ def _instantiate_rules(rule_classes: list[type[BaseLintRule]]) -> list[BaseLintRule]:
130
+ """Instantiate a list of rule classes.
131
+
132
+ Args:
133
+ rule_classes: List of rule classes to instantiate
134
+
135
+ Returns:
136
+ List of successfully instantiated rules
137
+ """
138
+ instances = (_try_instantiate_rule(cls) for cls in rule_classes)
139
+ return [inst for inst in instances if inst is not None]
140
+
141
+
142
+ def _try_instantiate_rule(rule_class: type[BaseLintRule]) -> BaseLintRule | None:
143
+ """Try to instantiate a rule class.
144
+
145
+ Args:
146
+ rule_class: Rule class to instantiate
147
+
148
+ Returns:
149
+ Rule instance or None on error
150
+ """
151
+ try:
152
+ return rule_class()
153
+ except (TypeError, AttributeError):
154
+ return None
155
+
156
+
157
+ def _is_rule_class(obj: Any) -> bool:
158
+ """Check if an object is a valid rule class.
159
+
160
+ Args:
161
+ obj: Object to check
162
+
163
+ Returns:
164
+ True if obj is a concrete BaseLintRule subclass
165
+ """
166
+ return (
167
+ inspect.isclass(obj)
168
+ and issubclass(obj, BaseLintRule)
169
+ and obj is not BaseLintRule
170
+ and not inspect.isabstract(obj)
171
+ )
172
+
173
+
174
+ # Legacy class wrapper for backward compatibility
175
+ class RuleDiscovery:
176
+ """Discovers linting rules from Python packages.
177
+
178
+ Note: This class is a thin wrapper around module-level functions
179
+ for backward compatibility.
180
+ """
181
+
182
+ def __init__(self) -> None:
183
+ """Initialize the discovery service."""
184
+ pass # No state needed
185
+
186
+ def discover_from_package(self, package_path: str) -> list[BaseLintRule]:
187
+ """Discover rules from a package and its modules.
120
188
 
121
189
  Args:
122
- obj: Object to check
190
+ package_path: Python package path (e.g., 'src.linters')
123
191
 
124
192
  Returns:
125
- True if obj is a concrete BaseLintRule subclass
193
+ List of discovered rule instances
126
194
  """
127
- return (
128
- inspect.isclass(obj)
129
- and issubclass(obj, BaseLintRule)
130
- and obj is not BaseLintRule
131
- and not inspect.isabstract(obj)
132
- )
195
+ return discover_from_package(package_path)
src/core/types.py CHANGED
@@ -81,3 +81,16 @@ class Violation:
81
81
  "severity": self.severity.value,
82
82
  "suggestion": self.suggestion,
83
83
  }
84
+
85
+ @classmethod
86
+ def from_dict(cls, data: dict) -> "Violation":
87
+ """Reconstruct Violation from dictionary (for parallel processing)."""
88
+ return cls(
89
+ rule_id=data["rule_id"],
90
+ file_path=data["file_path"],
91
+ line=data["line"],
92
+ column=data["column"],
93
+ message=data["message"],
94
+ severity=Severity(data["severity"]),
95
+ suggestion=data.get("suggestion"),
96
+ )
@@ -13,13 +13,17 @@ Overview: Provides base classes and data structures for violation creation acros
13
13
 
14
14
  Dependencies: dataclasses, src.core.types (Violation, Severity)
15
15
 
16
- Exports: ViolationInfo dataclass, BaseViolationBuilder class
16
+ Exports: ViolationInfo dataclass, build_violation function, build_violation_from_params function,
17
+ BaseViolationBuilder class (compat)
17
18
 
18
19
  Interfaces: ViolationInfo(rule_id, file_path, line, message, column, severity),
19
- BaseViolationBuilder.build(info: ViolationInfo) -> Violation
20
+ build_violation(info: ViolationInfo) -> Violation
20
21
 
21
- Implementation: Uses dataclass for type-safe violation info, base class provides build()
22
- method that constructs Violation objects with proper defaults
22
+ Implementation: Uses dataclass for type-safe violation info, functions provide build logic
23
+ that constructs Violation objects with proper defaults
24
+
25
+ Suppressions:
26
+ - too-many-arguments,too-many-positional-arguments: Violation fields as parameters
23
27
  """
24
28
 
25
29
  from dataclasses import dataclass
@@ -50,14 +54,82 @@ class ViolationInfo:
50
54
  suggestion: str | None = None
51
55
 
52
56
 
57
+ def build_violation(info: ViolationInfo) -> Violation:
58
+ """Build a Violation from ViolationInfo.
59
+
60
+ Args:
61
+ info: ViolationInfo containing all violation details
62
+
63
+ Returns:
64
+ Violation object with all fields populated
65
+ """
66
+ return Violation(
67
+ rule_id=info.rule_id,
68
+ file_path=info.file_path,
69
+ line=info.line,
70
+ column=info.column,
71
+ message=info.message,
72
+ severity=info.severity,
73
+ suggestion=info.suggestion,
74
+ )
75
+
76
+
77
+ def build_violation_from_params( # pylint: disable=too-many-arguments,too-many-positional-arguments
78
+ rule_id: str,
79
+ file_path: str,
80
+ line: int,
81
+ message: str,
82
+ column: int = 1,
83
+ severity: Severity = Severity.ERROR,
84
+ suggestion: str | None = None,
85
+ ) -> Violation:
86
+ """Build a Violation directly from parameters.
87
+
88
+ Note: Pylint too-many-arguments disabled. This convenience function mirrors the
89
+ ViolationInfo dataclass fields (7 parameters, 3 with defaults). The alternative
90
+ would require every caller to create ViolationInfo objects manually, reducing
91
+ readability.
92
+
93
+ Args:
94
+ rule_id: Identifier for the rule that was violated
95
+ file_path: Path to the file containing the violation
96
+ line: Line number where violation occurs (1-indexed)
97
+ message: Description of the violation
98
+ column: Column number where violation occurs (0-indexed, default=1)
99
+ severity: Severity level of the violation (default=ERROR)
100
+ suggestion: Optional suggestion for fixing the violation
101
+
102
+ Returns:
103
+ Violation object with all fields populated
104
+ """
105
+ info = ViolationInfo(
106
+ rule_id=rule_id,
107
+ file_path=file_path,
108
+ line=line,
109
+ message=message,
110
+ column=column,
111
+ severity=severity,
112
+ suggestion=suggestion,
113
+ )
114
+ return build_violation(info)
115
+
116
+
117
+ # Legacy class wrapper for backward compatibility
53
118
  class BaseViolationBuilder:
54
119
  """Base class for building violations with consistent structure.
55
120
 
56
121
  Provides common build() method for creating Violation objects from ViolationInfo.
57
122
  Linter-specific builders extend this class to add their domain-specific violation
58
123
  creation methods while inheriting the common construction logic.
124
+
125
+ Note: This class is a thin wrapper around module-level functions
126
+ for backward compatibility.
59
127
  """
60
128
 
129
+ def __init__(self) -> None:
130
+ """Initialize the builder."""
131
+ pass # No state needed
132
+
61
133
  def build(self, info: ViolationInfo) -> Violation:
62
134
  """Build a Violation from ViolationInfo.
63
135
 
@@ -67,15 +139,7 @@ class BaseViolationBuilder:
67
139
  Returns:
68
140
  Violation object with all fields populated
69
141
  """
70
- return Violation(
71
- rule_id=info.rule_id,
72
- file_path=info.file_path,
73
- line=info.line,
74
- column=info.column,
75
- message=info.message,
76
- severity=info.severity,
77
- suggestion=info.suggestion,
78
- )
142
+ return build_violation(info)
79
143
 
80
144
  def build_from_params( # pylint: disable=too-many-arguments,too-many-positional-arguments
81
145
  self,
@@ -110,7 +174,7 @@ class BaseViolationBuilder:
110
174
  Returns:
111
175
  Violation object with all fields populated
112
176
  """
113
- info = ViolationInfo(
177
+ return build_violation_from_params(
114
178
  rule_id=rule_id,
115
179
  file_path=file_path,
116
180
  line=line,
@@ -119,4 +183,3 @@ class BaseViolationBuilder:
119
183
  severity=severity,
120
184
  suggestion=suggestion,
121
185
  )
122
- return self.build(info)
@@ -0,0 +1,69 @@
1
+ """
2
+ Purpose: Shared utility functions for violation processing across linters
3
+
4
+ Scope: Common violation-related operations used by multiple linters
5
+
6
+ Overview: Provides shared utility functions for working with violations, including
7
+ extracting line text and checking for ignore directives. These patterns were
8
+ previously duplicated across multiple linter modules (magic_numbers, print_statements,
9
+ method_property). Centralizing them here improves maintainability and ensures
10
+ consistent behavior across all linters.
11
+
12
+ Dependencies: BaseLintContext, Violation types
13
+
14
+ Exports: get_violation_line, has_python_noqa, has_typescript_noqa
15
+
16
+ Interfaces:
17
+ get_violation_line(violation, context) -> str | None
18
+ has_python_noqa(line_text) -> bool
19
+ has_typescript_noqa(line_text) -> bool
20
+
21
+ Implementation: Simple text extraction and pattern matching
22
+ """
23
+
24
+ from src.core.base import BaseLintContext
25
+ from src.core.types import Violation
26
+
27
+
28
+ def get_violation_line(violation: Violation, context: BaseLintContext) -> str | None:
29
+ """Get the line text for a violation, lowercased.
30
+
31
+ Args:
32
+ violation: Violation to get line for
33
+ context: Lint context with file content
34
+
35
+ Returns:
36
+ Lowercased line text, or None if not available
37
+ """
38
+ if not context.file_content:
39
+ return None
40
+
41
+ lines = context.file_content.splitlines()
42
+ if violation.line <= 0 or violation.line > len(lines):
43
+ return None
44
+
45
+ return lines[violation.line - 1].lower()
46
+
47
+
48
+ def has_python_noqa(line_text: str) -> bool:
49
+ """Check if line has Python-style noqa directive.
50
+
51
+ Args:
52
+ line_text: Lowercased line text
53
+
54
+ Returns:
55
+ True if line has # noqa comment
56
+ """
57
+ return "# noqa" in line_text
58
+
59
+
60
+ def has_typescript_noqa(line_text: str) -> bool:
61
+ """Check if line has TypeScript-style noqa directive.
62
+
63
+ Args:
64
+ line_text: Lowercased line text
65
+
66
+ Returns:
67
+ True if line has // noqa comment
68
+ """
69
+ return "// noqa" in line_text
@@ -0,0 +1,22 @@
1
+ """
2
+ Purpose: SARIF formatter package for thai-lint output
3
+
4
+ Scope: SARIF v2.1.0 formatter implementation and package exports
5
+
6
+ Overview: Formatters package providing SARIF (Static Analysis Results Interchange Format) v2.1.0
7
+ output generation from thai-lint Violation objects. Enables integration with GitHub Code
8
+ Scanning, Azure DevOps, VS Code SARIF Viewer, and other industry-standard CI/CD platforms.
9
+ Provides the SarifFormatter class for converting violations to SARIF JSON documents.
10
+
11
+ Dependencies: sarif module for SarifFormatter class
12
+
13
+ Exports: SarifFormatter class from sarif.py module
14
+
15
+ Interfaces: from src.formatters.sarif import SarifFormatter
16
+
17
+ Implementation: Package initialization with SarifFormatter export
18
+ """
19
+
20
+ from src.formatters.sarif import SarifFormatter
21
+
22
+ __all__ = ["SarifFormatter"]