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,414 @@
1
+ """
2
+ Purpose: Main method-should-be-property linter rule implementation
3
+
4
+ Scope: Method-should-be-property detection for Python files
5
+
6
+ Overview: Implements method-should-be-property linter rule following MultiLanguageLintRule
7
+ interface. Orchestrates configuration loading, Python AST analysis for property candidates,
8
+ and violation building through focused helper classes. Detects methods that should be
9
+ converted to @property decorators following Pythonic conventions. Supports configurable
10
+ max_body_statements threshold, ignore patterns for excluding files, and inline ignore
11
+ directives (thailint: ignore, noqa) for suppressing specific violations. Handles test file
12
+ detection and non-Python languages gracefully.
13
+
14
+ Dependencies: BaseLintContext and MultiLanguageLintRule from core, ast module, pathlib,
15
+ analyzer classes, config classes
16
+
17
+ Exports: MethodPropertyRule class implementing MultiLanguageLintRule interface
18
+
19
+ Interfaces: check(context) -> list[Violation] for rule validation, standard rule properties
20
+ (rule_id, rule_name, description)
21
+
22
+ Implementation: Composition pattern with helper classes (analyzer, violation builder),
23
+ AST-based analysis for Python with comprehensive exclusion rules
24
+
25
+ Suppressions:
26
+ - srp,dry: Rule class coordinates analyzer, config, and violation building. Method count
27
+ exceeds limit due to comprehensive ignore directive support.
28
+ """
29
+
30
+ import ast
31
+ from pathlib import Path
32
+
33
+ from src.core.base import BaseLintContext, MultiLanguageLintRule
34
+ from src.core.types import Violation
35
+
36
+ from .config import MethodPropertyConfig
37
+ from .python_analyzer import PropertyCandidate, PythonMethodAnalyzer
38
+ from .violation_builder import ViolationBuilder
39
+
40
+
41
+ class MethodPropertyRule(MultiLanguageLintRule): # thailint: ignore[srp,dry]
42
+ """Detects methods that should be @property decorators."""
43
+
44
+ def __init__(self) -> None:
45
+ """Initialize the method property rule."""
46
+ self._violation_builder = ViolationBuilder(self.rule_id)
47
+
48
+ @property
49
+ def rule_id(self) -> str:
50
+ """Unique identifier for this rule."""
51
+ return "method-property.should-be-property"
52
+
53
+ @property
54
+ def rule_name(self) -> str:
55
+ """Human-readable name for this rule."""
56
+ return "method should be property"
57
+
58
+ @property
59
+ def description(self) -> str:
60
+ """Description of what this rule checks."""
61
+ return "Methods should be converted to @property decorators for Pythonic attribute access"
62
+
63
+ def _load_config(self, context: BaseLintContext) -> MethodPropertyConfig:
64
+ """Load configuration from context.
65
+
66
+ Args:
67
+ context: Lint context
68
+
69
+ Returns:
70
+ MethodPropertyConfig instance
71
+ """
72
+ test_config = self._try_load_test_config(context)
73
+ if test_config is not None:
74
+ return test_config
75
+
76
+ return MethodPropertyConfig()
77
+
78
+ def _try_load_test_config(self, context: BaseLintContext) -> MethodPropertyConfig | None:
79
+ """Try to load test-style configuration.
80
+
81
+ Args:
82
+ context: Lint context
83
+
84
+ Returns:
85
+ Config if found, None otherwise
86
+ """
87
+ if not hasattr(context, "config"):
88
+ return None
89
+ config_attr = context.config
90
+ if config_attr is None or not isinstance(config_attr, dict):
91
+ return None
92
+
93
+ # Check for method-property specific config
94
+ linter_config = config_attr.get("method-property", config_attr)
95
+ return MethodPropertyConfig.from_dict(linter_config)
96
+
97
+ def _is_file_ignored(self, context: BaseLintContext, config: MethodPropertyConfig) -> bool:
98
+ """Check if file matches ignore patterns.
99
+
100
+ Args:
101
+ context: Lint context
102
+ config: Configuration
103
+
104
+ Returns:
105
+ True if file should be ignored
106
+ """
107
+ if not config.ignore:
108
+ return False
109
+
110
+ if not context.file_path:
111
+ return False
112
+
113
+ file_path = Path(context.file_path)
114
+ return any(self._matches_pattern(file_path, pattern) for pattern in config.ignore)
115
+
116
+ def _matches_pattern(self, file_path: Path, pattern: str) -> bool:
117
+ """Check if file path matches a glob pattern.
118
+
119
+ Args:
120
+ file_path: Path to check
121
+ pattern: Glob pattern
122
+
123
+ Returns:
124
+ True if path matches pattern
125
+ """
126
+ if file_path.match(pattern):
127
+ return True
128
+ if pattern in str(file_path):
129
+ return True
130
+ return False
131
+
132
+ def _is_test_file(self, file_path: object) -> bool:
133
+ """Check if file is a test file.
134
+
135
+ Args:
136
+ file_path: Path to check
137
+
138
+ Returns:
139
+ True if test file
140
+ """
141
+ path_str = str(file_path)
142
+ file_name = Path(path_str).name
143
+
144
+ # Check test_*.py pattern
145
+ if file_name.startswith("test_") and file_name.endswith(".py"):
146
+ return True
147
+
148
+ # Check *_test.py pattern
149
+ if file_name.endswith("_test.py"):
150
+ return True
151
+
152
+ return False
153
+
154
+ def _check_python(
155
+ self, context: BaseLintContext, config: MethodPropertyConfig
156
+ ) -> list[Violation]:
157
+ """Check Python code for method property violations.
158
+
159
+ Args:
160
+ context: Lint context with Python file information
161
+ config: Method property configuration
162
+
163
+ Returns:
164
+ List of violations found in Python code
165
+ """
166
+ if self._is_file_ignored(context, config):
167
+ return []
168
+
169
+ if self._is_test_file(context.file_path):
170
+ return []
171
+
172
+ tree = self._parse_python_code(context.file_content)
173
+ if tree is None:
174
+ return []
175
+
176
+ analyzer = PythonMethodAnalyzer(
177
+ max_body_statements=config.max_body_statements,
178
+ exclude_prefixes=config.exclude_prefixes,
179
+ exclude_names=config.exclude_names,
180
+ )
181
+ candidates = analyzer.find_property_candidates(tree)
182
+ candidates = self._filter_ignored_methods(candidates, config)
183
+ return self._collect_violations(candidates, context)
184
+
185
+ def _filter_ignored_methods(
186
+ self,
187
+ candidates: list[PropertyCandidate],
188
+ config: MethodPropertyConfig,
189
+ ) -> list[PropertyCandidate]:
190
+ """Filter out candidates with ignored method names.
191
+
192
+ Args:
193
+ candidates: List of property candidates
194
+ config: Configuration with ignore_methods list
195
+
196
+ Returns:
197
+ Filtered list of candidates
198
+ """
199
+ if not config.ignore_methods:
200
+ return candidates
201
+ return [c for c in candidates if c.method_name not in config.ignore_methods]
202
+
203
+ def _parse_python_code(self, code: str | None) -> ast.AST | None:
204
+ """Parse Python code into AST.
205
+
206
+ Args:
207
+ code: Python source code
208
+
209
+ Returns:
210
+ AST or None if parse fails
211
+ """
212
+ try:
213
+ return ast.parse(code or "")
214
+ except SyntaxError:
215
+ return None
216
+
217
+ def _collect_violations(
218
+ self,
219
+ candidates: list[PropertyCandidate],
220
+ context: BaseLintContext,
221
+ ) -> list[Violation]:
222
+ """Collect violations from property candidates.
223
+
224
+ Args:
225
+ candidates: List of property candidates
226
+ context: Lint context
227
+
228
+ Returns:
229
+ List of violations
230
+ """
231
+ violations = []
232
+ for candidate in candidates:
233
+ violation = self._create_violation(candidate, context)
234
+ if not self._should_ignore(violation, candidate, context):
235
+ violations.append(violation)
236
+ return violations
237
+
238
+ def _create_violation(
239
+ self,
240
+ candidate: PropertyCandidate,
241
+ context: BaseLintContext,
242
+ ) -> Violation:
243
+ """Create a violation for a property candidate.
244
+
245
+ Args:
246
+ candidate: The property candidate
247
+ context: Lint context
248
+
249
+ Returns:
250
+ Violation object
251
+ """
252
+ return self._violation_builder.create_violation(
253
+ method_name=candidate.method_name,
254
+ line=candidate.line,
255
+ column=candidate.column,
256
+ file_path=context.file_path,
257
+ is_get_prefix=candidate.is_get_prefix,
258
+ class_name=candidate.class_name,
259
+ )
260
+
261
+ def _should_ignore(
262
+ self,
263
+ violation: Violation,
264
+ candidate: PropertyCandidate,
265
+ context: BaseLintContext,
266
+ ) -> bool:
267
+ """Check if violation should be ignored based on directives.
268
+
269
+ Args:
270
+ violation: Violation to check
271
+ candidate: The property candidate
272
+ context: Lint context
273
+
274
+ Returns:
275
+ True if violation should be ignored
276
+ """
277
+ if self._has_inline_ignore(violation, context):
278
+ return True
279
+ if self._has_docstring_ignore(candidate, context):
280
+ return True
281
+ return False
282
+
283
+ def _has_inline_ignore(self, violation: Violation, context: BaseLintContext) -> bool:
284
+ """Check for inline ignore directive on method line.
285
+
286
+ Args:
287
+ violation: Violation to check
288
+ context: Lint context
289
+
290
+ Returns:
291
+ True if line has ignore directive
292
+ """
293
+ line_text = self._get_line_text(violation.line, context)
294
+ if line_text is None:
295
+ return False
296
+
297
+ line_lower = line_text.lower()
298
+
299
+ # Check for thailint: ignore[method-property]
300
+ if "thailint:" in line_lower and "ignore" in line_lower:
301
+ return True
302
+
303
+ # Check for noqa
304
+ if "# noqa" in line_lower:
305
+ return True
306
+
307
+ return False
308
+
309
+ def _has_docstring_ignore(
310
+ self,
311
+ candidate: PropertyCandidate,
312
+ context: BaseLintContext,
313
+ ) -> bool:
314
+ """Check for ignore directive in method docstring.
315
+
316
+ Args:
317
+ candidate: Property candidate
318
+ context: Lint context
319
+
320
+ Returns:
321
+ True if docstring has ignore directive
322
+ """
323
+ tree = self._parse_python_code(context.file_content)
324
+ if tree is None:
325
+ return False
326
+
327
+ docstring = self._find_method_docstring(tree, candidate)
328
+ if docstring is None:
329
+ return False
330
+
331
+ docstring_lower = docstring.lower()
332
+ return "thailint: ignore" in docstring_lower
333
+
334
+ def _find_method_docstring(
335
+ self,
336
+ tree: ast.AST,
337
+ candidate: PropertyCandidate,
338
+ ) -> str | None:
339
+ """Find the docstring for a method.
340
+
341
+ Args:
342
+ tree: AST tree
343
+ candidate: Property candidate
344
+
345
+ Returns:
346
+ Docstring text or None
347
+ """
348
+ target_class = self._find_class_node(tree, candidate.class_name)
349
+ if target_class is None:
350
+ return None
351
+ return self._find_method_in_class(target_class, candidate.method_name)
352
+
353
+ def _find_class_node(self, tree: ast.AST, class_name: str) -> ast.ClassDef | None:
354
+ """Find a class node by name in the AST.
355
+
356
+ Args:
357
+ tree: AST tree
358
+ class_name: Name of the class to find
359
+
360
+ Returns:
361
+ ClassDef node or None
362
+ """
363
+ for node in ast.walk(tree):
364
+ if isinstance(node, ast.ClassDef) and node.name == class_name:
365
+ return node
366
+ return None
367
+
368
+ def _find_method_in_class(self, class_node: ast.ClassDef, method_name: str) -> str | None:
369
+ """Find method docstring within a class.
370
+
371
+ Args:
372
+ class_node: Class node to search
373
+ method_name: Method name to find
374
+
375
+ Returns:
376
+ Docstring or None
377
+ """
378
+ for item in class_node.body:
379
+ if isinstance(item, ast.FunctionDef) and item.name == method_name:
380
+ return ast.get_docstring(item)
381
+ return None
382
+
383
+ def _get_line_text(self, line: int, context: BaseLintContext) -> str | None:
384
+ """Get the text of a specific line.
385
+
386
+ Args:
387
+ line: Line number (1-indexed)
388
+ context: Lint context
389
+
390
+ Returns:
391
+ Line text or None
392
+ """
393
+ if not context.file_content:
394
+ return None
395
+
396
+ lines = context.file_content.splitlines()
397
+ if line <= 0 or line > len(lines):
398
+ return None
399
+
400
+ return lines[line - 1]
401
+
402
+ def _check_typescript(
403
+ self, context: BaseLintContext, config: MethodPropertyConfig
404
+ ) -> list[Violation]:
405
+ """Check TypeScript code for violations.
406
+
407
+ Args:
408
+ context: Lint context
409
+ config: Configuration
410
+
411
+ Returns:
412
+ Empty list (not implemented for TypeScript)
413
+ """
414
+ return []