thailint 0.16.0__py3-none-any.whl → 0.17.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 (35) hide show
  1. src/cli/linters/__init__.py +8 -1
  2. src/cli/linters/rust.py +177 -0
  3. src/core/base.py +30 -0
  4. src/core/constants.py +1 -0
  5. src/core/linter_utils.py +42 -1
  6. src/linters/blocking_async/__init__.py +31 -0
  7. src/linters/blocking_async/config.py +67 -0
  8. src/linters/blocking_async/linter.py +183 -0
  9. src/linters/blocking_async/rust_analyzer.py +419 -0
  10. src/linters/blocking_async/violation_builder.py +97 -0
  11. src/linters/clone_abuse/__init__.py +31 -0
  12. src/linters/clone_abuse/config.py +65 -0
  13. src/linters/clone_abuse/linter.py +183 -0
  14. src/linters/clone_abuse/rust_analyzer.py +356 -0
  15. src/linters/clone_abuse/violation_builder.py +94 -0
  16. src/linters/magic_numbers/linter.py +92 -0
  17. src/linters/magic_numbers/rust_analyzer.py +148 -0
  18. src/linters/magic_numbers/violation_builder.py +31 -0
  19. src/linters/nesting/linter.py +50 -0
  20. src/linters/nesting/rust_analyzer.py +118 -0
  21. src/linters/nesting/violation_builder.py +32 -0
  22. src/linters/srp/class_analyzer.py +49 -0
  23. src/linters/srp/linter.py +22 -0
  24. src/linters/srp/rust_analyzer.py +206 -0
  25. src/linters/unwrap_abuse/__init__.py +30 -0
  26. src/linters/unwrap_abuse/config.py +59 -0
  27. src/linters/unwrap_abuse/linter.py +166 -0
  28. src/linters/unwrap_abuse/rust_analyzer.py +118 -0
  29. src/linters/unwrap_abuse/violation_builder.py +89 -0
  30. src/templates/thailint_config_template.yaml +88 -0
  31. {thailint-0.16.0.dist-info → thailint-0.17.0.dist-info}/METADATA +5 -2
  32. {thailint-0.16.0.dist-info → thailint-0.17.0.dist-info}/RECORD +35 -16
  33. {thailint-0.16.0.dist-info → thailint-0.17.0.dist-info}/WHEEL +0 -0
  34. {thailint-0.16.0.dist-info → thailint-0.17.0.dist-info}/entry_points.txt +0 -0
  35. {thailint-0.16.0.dist-info → thailint-0.17.0.dist-info}/licenses/LICENSE +0 -0
@@ -0,0 +1,419 @@
1
+ """
2
+ Purpose: Analyzer for detecting blocking operations inside async functions in Rust code
3
+
4
+ Scope: Pattern detection for std::fs, std::thread::sleep, and std::net calls in async contexts
5
+
6
+ Overview: Provides RustBlockingAsyncAnalyzer that extends RustBaseAnalyzer to detect blocking
7
+ API calls inside async functions in Rust code. Detects three categories: filesystem operations
8
+ (std::fs::read_to_string, std::fs::write, etc.), thread sleep (std::thread::sleep), and
9
+ blocking network calls (std::net::TcpStream::connect, etc.). Supports both fully-qualified
10
+ paths (std::fs::read_to_string) and short paths (fs::read_to_string after use std::fs).
11
+ Excludes blocking calls wrapped in async-safe wrappers (asyncify, spawn_blocking,
12
+ block_in_place) which correctly offload work to a thread pool. Uses tree-sitter AST to
13
+ find function_item nodes, filter to async functions, walk bodies for call_expression nodes
14
+ with scoped_identifier paths, and match against known blocking API patterns. Returns
15
+ structured BlockingCall dataclass instances with location, pattern type, test context, and
16
+ surrounding code for violation reporting.
17
+
18
+ Dependencies: src.analyzers.rust_base for tree-sitter parsing and traversal
19
+
20
+ Exports: RustBlockingAsyncAnalyzer, BlockingCall
21
+
22
+ Interfaces: find_blocking_calls(code: str) -> list[BlockingCall]
23
+
24
+ Implementation: AST-based async function detection with scoped_identifier path extraction
25
+ and pattern matching against known blocking APIs
26
+ """
27
+
28
+ from __future__ import annotations
29
+
30
+ from dataclasses import dataclass
31
+ from typing import TYPE_CHECKING
32
+
33
+ from src.analyzers.rust_base import RustBaseAnalyzer
34
+ from src.core.linter_utils import get_line_context
35
+
36
+ if TYPE_CHECKING:
37
+ from tree_sitter import Node
38
+
39
+ # Blocking std::fs function names
40
+ _BLOCKING_FS_FUNCTIONS = frozenset(
41
+ {
42
+ "read_to_string",
43
+ "read",
44
+ "write",
45
+ "create_dir",
46
+ "create_dir_all",
47
+ "remove_file",
48
+ "remove_dir",
49
+ "remove_dir_all",
50
+ "rename",
51
+ "copy",
52
+ "metadata",
53
+ "read_dir",
54
+ "canonicalize",
55
+ "read_link",
56
+ }
57
+ )
58
+
59
+ # Blocking std::net type names that have blocking methods
60
+ _BLOCKING_NET_TYPES = frozenset(
61
+ {
62
+ "TcpStream",
63
+ "TcpListener",
64
+ "UdpSocket",
65
+ }
66
+ )
67
+
68
+
69
+ @dataclass
70
+ class BlockingCall:
71
+ """Represents a detected blocking call inside an async function."""
72
+
73
+ line: int
74
+ column: int
75
+ pattern: str # "fs-in-async", "sleep-in-async", "net-in-async"
76
+ is_in_test: bool
77
+ context: str # Surrounding code snippet
78
+ blocking_api: str # e.g., "std::fs::read_to_string"
79
+
80
+
81
+ class RustBlockingAsyncAnalyzer(RustBaseAnalyzer):
82
+ """Analyzer for detecting blocking operations inside async functions."""
83
+
84
+ def find_blocking_calls(self, code: str) -> list[BlockingCall]:
85
+ """Find all blocking calls inside async functions.
86
+
87
+ Args:
88
+ code: Rust source code to analyze
89
+
90
+ Returns:
91
+ List of detected blocking calls with pattern classification
92
+ """
93
+ if not self.tree_sitter_available:
94
+ return []
95
+
96
+ root = self.parse_rust(code)
97
+ if root is None:
98
+ return []
99
+
100
+ calls: list[BlockingCall] = []
101
+ self._scan_for_blocking_calls(root, code, calls)
102
+ return calls
103
+
104
+ def _scan_for_blocking_calls(self, node: Node, code: str, calls: list[BlockingCall]) -> None:
105
+ """Recursively scan AST for blocking calls in async contexts.
106
+
107
+ Finds call_expression nodes inside async functions and checks if they
108
+ invoke known blocking APIs.
109
+
110
+ Args:
111
+ node: Current tree-sitter node to inspect
112
+ code: Original source code for context extraction
113
+ calls: Accumulator list for detected calls
114
+ """
115
+ if node.type == "call_expression" and self._is_in_async_context(node):
116
+ blocking_call = self._check_blocking_call(node, code)
117
+ if blocking_call is not None:
118
+ calls.append(blocking_call)
119
+
120
+ for child in node.children:
121
+ self._scan_for_blocking_calls(child, code, calls)
122
+
123
+ def _is_in_async_context(self, node: Node) -> bool:
124
+ """Check if node is inside an async function body.
125
+
126
+ Walks up the parent chain looking for function_item nodes that
127
+ are async functions.
128
+
129
+ Args:
130
+ node: Node to check
131
+
132
+ Returns:
133
+ True if inside an async function
134
+ """
135
+ current: Node | None = node.parent
136
+ while current is not None:
137
+ if current.type == "function_item" and self.is_async_function(current):
138
+ return True
139
+ current = current.parent
140
+ return False
141
+
142
+ def _check_blocking_call(self, call_node: Node, code: str) -> BlockingCall | None:
143
+ """Check if a call expression is a blocking API call.
144
+
145
+ Extracts the call path from the scoped_identifier child and matches
146
+ it against known blocking API patterns. Skips calls wrapped in
147
+ spawn_blocking/asyncify which are correctly offloaded to a thread pool.
148
+
149
+ Args:
150
+ call_node: A call_expression node
151
+ code: Original source code for context extraction
152
+
153
+ Returns:
154
+ BlockingCall if blocking API detected, None otherwise
155
+ """
156
+ path = self._extract_call_path(call_node)
157
+ if not path:
158
+ return None
159
+
160
+ pattern = _classify_blocking_pattern(path)
161
+ if pattern is None:
162
+ return None
163
+
164
+ if _is_inside_blocking_wrapper(call_node):
165
+ return None
166
+
167
+ return BlockingCall(
168
+ line=call_node.start_point[0] + 1,
169
+ column=call_node.start_point[1],
170
+ pattern=pattern,
171
+ is_in_test=self.is_inside_test(call_node),
172
+ context=get_line_context(code, call_node.start_point[0]),
173
+ blocking_api=path,
174
+ )
175
+
176
+ def _extract_call_path(self, call_node: Node) -> str:
177
+ """Extract the full call path from a call_expression.
178
+
179
+ Handles both direct scoped calls (std::fs::read_to_string(...))
180
+ and method-style calls that chain on scoped calls.
181
+
182
+ Args:
183
+ call_node: A call_expression node
184
+
185
+ Returns:
186
+ Full path string (e.g., "std::fs::read_to_string"), or empty string
187
+ """
188
+ for child in call_node.children:
189
+ if child.type == "scoped_identifier":
190
+ return self.extract_node_text(child)
191
+ return ""
192
+
193
+
194
+ def _classify_blocking_pattern(path: str) -> str | None:
195
+ """Classify a call path into a blocking pattern category.
196
+
197
+ Checks the path against known blocking API patterns for filesystem,
198
+ thread sleep, and network operations.
199
+
200
+ Args:
201
+ path: Full or short call path (e.g., "std::fs::read_to_string")
202
+
203
+ Returns:
204
+ Pattern string or None if not a blocking pattern
205
+ """
206
+ if _is_blocking_fs(path):
207
+ return "fs-in-async"
208
+ if _is_blocking_sleep(path):
209
+ return "sleep-in-async"
210
+ if _is_blocking_net(path):
211
+ return "net-in-async"
212
+ return None
213
+
214
+
215
+ def _is_blocking_fs(path: str) -> bool:
216
+ """Check if path matches a blocking filesystem operation.
217
+
218
+ Matches both fully-qualified (std::fs::read_to_string) and
219
+ short paths (fs::read_to_string).
220
+
221
+ Args:
222
+ path: Call path to check
223
+
224
+ Returns:
225
+ True if path is a blocking fs operation
226
+ """
227
+ parts = path.split("::")
228
+ return _matches_std_fs_pattern(parts) or _matches_short_fs_pattern(parts)
229
+
230
+
231
+ def _matches_std_fs_pattern(parts: list[str]) -> bool:
232
+ """Check for fully-qualified std::fs::function pattern.
233
+
234
+ Args:
235
+ parts: Path components split by ::
236
+
237
+ Returns:
238
+ True if matches std::fs::function_name
239
+ """
240
+ if len(parts) < 3:
241
+ return False
242
+ return parts[0] == "std" and parts[1] == "fs" and parts[2] in _BLOCKING_FS_FUNCTIONS
243
+
244
+
245
+ def _matches_short_fs_pattern(parts: list[str]) -> bool:
246
+ """Check for short fs::function pattern.
247
+
248
+ Args:
249
+ parts: Path components split by ::
250
+
251
+ Returns:
252
+ True if matches fs::function_name
253
+ """
254
+ if len(parts) < 2:
255
+ return False
256
+ return parts[0] == "fs" and parts[1] in _BLOCKING_FS_FUNCTIONS
257
+
258
+
259
+ def _is_blocking_sleep(path: str) -> bool:
260
+ """Check if path matches std::thread::sleep.
261
+
262
+ Matches both std::thread::sleep and thread::sleep.
263
+
264
+ Args:
265
+ path: Call path to check
266
+
267
+ Returns:
268
+ True if path is a blocking sleep call
269
+ """
270
+ parts = path.split("::")
271
+ return _matches_std_sleep_pattern(parts) or _matches_short_sleep_pattern(parts)
272
+
273
+
274
+ def _matches_std_sleep_pattern(parts: list[str]) -> bool:
275
+ """Check for fully-qualified std::thread::sleep pattern.
276
+
277
+ Args:
278
+ parts: Path components split by ::
279
+
280
+ Returns:
281
+ True if matches std::thread::sleep
282
+ """
283
+ if len(parts) < 3:
284
+ return False
285
+ return parts[0] == "std" and parts[1] == "thread" and parts[2] == "sleep"
286
+
287
+
288
+ def _matches_short_sleep_pattern(parts: list[str]) -> bool:
289
+ """Check for short thread::sleep pattern.
290
+
291
+ Args:
292
+ parts: Path components split by ::
293
+
294
+ Returns:
295
+ True if matches thread::sleep
296
+ """
297
+ if len(parts) < 2:
298
+ return False
299
+ return parts[0] == "thread" and parts[1] == "sleep"
300
+
301
+
302
+ def _is_blocking_net(path: str) -> bool:
303
+ """Check if path matches a blocking network operation.
304
+
305
+ Matches both fully-qualified (std::net::TcpStream::connect) and
306
+ short paths (net::TcpStream::connect, TcpStream::connect).
307
+
308
+ Args:
309
+ path: Call path to check
310
+
311
+ Returns:
312
+ True if path is a blocking net operation
313
+ """
314
+ parts = path.split("::")
315
+ return _matches_std_net_pattern(parts) or _matches_short_net_pattern(parts)
316
+
317
+
318
+ def _matches_std_net_pattern(parts: list[str]) -> bool:
319
+ """Check for fully-qualified std::net::Type::method pattern.
320
+
321
+ Args:
322
+ parts: Path components split by ::
323
+
324
+ Returns:
325
+ True if matches std::net::TcpStream/TcpListener/UdpSocket pattern
326
+ """
327
+ if len(parts) < 3:
328
+ return False
329
+ return parts[0] == "std" and parts[1] == "net" and parts[2] in _BLOCKING_NET_TYPES
330
+
331
+
332
+ def _matches_short_net_pattern(parts: list[str]) -> bool:
333
+ """Check for short net::Type::method or Type::method pattern.
334
+
335
+ Args:
336
+ parts: Path components split by ::
337
+
338
+ Returns:
339
+ True if matches short net pattern
340
+ """
341
+ if len(parts) >= 2 and parts[0] == "net" and parts[1] in _BLOCKING_NET_TYPES:
342
+ return True
343
+ return False
344
+
345
+
346
+ # Function names that safely wrap blocking operations for async execution
347
+ _ASYNC_WRAPPER_FUNCTIONS = frozenset(
348
+ {
349
+ "asyncify",
350
+ "spawn_blocking",
351
+ "block_in_place",
352
+ }
353
+ )
354
+
355
+
356
+ def _is_inside_blocking_wrapper(node: Node) -> bool:
357
+ """Check if a blocking call is wrapped in an async-safe wrapper function.
358
+
359
+ Detects patterns like asyncify(move || std::fs::read(...)) or
360
+ spawn_blocking(move || { std::fs::write(...) }) where blocking calls
361
+ are correctly offloaded to a thread pool.
362
+
363
+ Args:
364
+ node: The blocking call_expression node
365
+
366
+ Returns:
367
+ True if the call is inside a known async wrapper function
368
+ """
369
+ current: Node | None = node.parent
370
+ while current is not None:
371
+ if _is_wrapper_call(current):
372
+ return True
373
+ current = current.parent
374
+ return False
375
+
376
+
377
+ def _is_wrapper_call(node: Node) -> bool:
378
+ """Check if a node is a call to a known async wrapper function.
379
+
380
+ Args:
381
+ node: Node to check
382
+
383
+ Returns:
384
+ True if node is a call_expression to asyncify/spawn_blocking/block_in_place
385
+ """
386
+ if node.type != "call_expression":
387
+ return False
388
+ return any(_child_is_wrapper_name(child) for child in node.children)
389
+
390
+
391
+ def _child_is_wrapper_name(child: Node) -> bool:
392
+ """Check if a child node is a wrapper function name.
393
+
394
+ Args:
395
+ child: Child node of a call_expression
396
+
397
+ Returns:
398
+ True if the child is an identifier or scoped_identifier matching a wrapper name
399
+ """
400
+ if child.type == "identifier":
401
+ return _node_text_matches_wrapper(child)
402
+ if child.type == "scoped_identifier":
403
+ return _scoped_name_matches_wrapper(child)
404
+ return False
405
+
406
+
407
+ def _node_text_matches_wrapper(node: Node) -> bool:
408
+ """Check if a node's text matches a wrapper function name."""
409
+ text = node.text
410
+ return text is not None and text.decode() in _ASYNC_WRAPPER_FUNCTIONS
411
+
412
+
413
+ def _scoped_name_matches_wrapper(node: Node) -> bool:
414
+ """Check if a scoped identifier's final segment matches a wrapper function name."""
415
+ text = node.text
416
+ if text is None:
417
+ return False
418
+ func_name = text.decode().split("::")[-1]
419
+ return func_name in _ASYNC_WRAPPER_FUNCTIONS
@@ -0,0 +1,97 @@
1
+ """
2
+ Purpose: Build Violation objects for Rust blocking-in-async patterns
3
+
4
+ Scope: Creates violations with actionable suggestions for fs-in-async, sleep-in-async,
5
+ and net-in-async patterns
6
+
7
+ Overview: Provides module-level functions that create Violation objects for detected
8
+ blocking operations inside async functions in Rust code. Each violation includes the
9
+ rule ID, location, descriptive message explaining the concurrency impact, and a
10
+ suggestion for async-compatible alternatives such as tokio::fs, tokio::time::sleep,
11
+ or tokio::net equivalents.
12
+
13
+ Dependencies: src.core.types for Violation dataclass
14
+
15
+ Exports: build_fs_in_async_violation, build_sleep_in_async_violation, build_net_in_async_violation
16
+
17
+ Interfaces: Module functions taking file_path, line, column, context and returning Violation
18
+
19
+ Implementation: Factory functions for each blocking-in-async pattern with pattern-specific suggestions
20
+ """
21
+
22
+ from src.core.types import Violation
23
+
24
+ _FS_IN_ASYNC_SUGGESTION = (
25
+ "Use tokio::fs equivalents (e.g., tokio::fs::read_to_string) for async-compatible "
26
+ "file I/O operations. Blocking std::fs calls in async functions can cause thread "
27
+ "starvation and deadlocks."
28
+ )
29
+
30
+ _SLEEP_IN_ASYNC_SUGGESTION = (
31
+ "Use tokio::time::sleep instead of std::thread::sleep in async functions. "
32
+ "Blocking the thread with std::thread::sleep prevents the async runtime from "
33
+ "processing other tasks on the same thread."
34
+ )
35
+
36
+ _NET_IN_ASYNC_SUGGESTION = (
37
+ "Use tokio::net equivalents (e.g., tokio::net::TcpStream) for async-compatible "
38
+ "networking. Blocking std::net calls in async functions can cause thread starvation "
39
+ "and deadlocks in the async runtime."
40
+ )
41
+
42
+
43
+ def build_fs_in_async_violation(
44
+ file_path: str,
45
+ line: int,
46
+ column: int,
47
+ context: str,
48
+ ) -> Violation:
49
+ """Build a violation for std::fs operation inside an async function."""
50
+ message = f"Blocking std::fs operation inside async function: {context}"
51
+
52
+ return Violation(
53
+ rule_id="blocking-async.fs-in-async",
54
+ file_path=file_path,
55
+ line=line,
56
+ column=column,
57
+ message=message,
58
+ suggestion=_FS_IN_ASYNC_SUGGESTION,
59
+ )
60
+
61
+
62
+ def build_sleep_in_async_violation(
63
+ file_path: str,
64
+ line: int,
65
+ column: int,
66
+ context: str,
67
+ ) -> Violation:
68
+ """Build a violation for std::thread::sleep inside an async function."""
69
+ message = f"Blocking std::thread::sleep inside async function: {context}"
70
+
71
+ return Violation(
72
+ rule_id="blocking-async.sleep-in-async",
73
+ file_path=file_path,
74
+ line=line,
75
+ column=column,
76
+ message=message,
77
+ suggestion=_SLEEP_IN_ASYNC_SUGGESTION,
78
+ )
79
+
80
+
81
+ def build_net_in_async_violation(
82
+ file_path: str,
83
+ line: int,
84
+ column: int,
85
+ context: str,
86
+ ) -> Violation:
87
+ """Build a violation for blocking std::net operation inside an async function."""
88
+ message = f"Blocking std::net operation inside async function: {context}"
89
+
90
+ return Violation(
91
+ rule_id="blocking-async.net-in-async",
92
+ file_path=file_path,
93
+ line=line,
94
+ column=column,
95
+ message=message,
96
+ suggestion=_NET_IN_ASYNC_SUGGESTION,
97
+ )
@@ -0,0 +1,31 @@
1
+ """
2
+ Purpose: Rust clone abuse detector package exports
3
+
4
+ Scope: Detect .clone() abuse patterns in Rust code and suggest safer alternatives
5
+
6
+ Overview: Package providing clone abuse detection for Rust code. Identifies .clone() calls
7
+ in loop bodies, chained .clone().clone() calls, and unnecessary clones where the source
8
+ is not used after cloning. Suggests safer alternatives including borrowing, Rc/Arc for
9
+ shared ownership, and Cow for clone-on-write patterns. Supports configuration for allowing
10
+ calls in test code, toggling individual pattern detection, and ignoring specific directories.
11
+ Uses tree-sitter for accurate AST-based detection.
12
+
13
+ Dependencies: tree-sitter-rust (optional) for AST parsing, src.core for base classes
14
+
15
+ Exports: CloneAbuseConfig, CloneAbuseRule, RustCloneAnalyzer, CloneCall
16
+
17
+ Interfaces: CloneAbuseConfig.from_dict() for YAML configuration loading
18
+
19
+ Implementation: Tree-sitter AST-based pattern detection with configurable filtering
20
+ """
21
+
22
+ from .config import CloneAbuseConfig
23
+ from .linter import CloneAbuseRule
24
+ from .rust_analyzer import CloneCall, RustCloneAnalyzer
25
+
26
+ __all__ = [
27
+ "CloneAbuseConfig",
28
+ "CloneAbuseRule",
29
+ "RustCloneAnalyzer",
30
+ "CloneCall",
31
+ ]
@@ -0,0 +1,65 @@
1
+ """
2
+ Purpose: Configuration dataclass for Rust clone abuse detector
3
+
4
+ Scope: Pattern toggles, ignore patterns, and configuration for clone abuse detection
5
+
6
+ Overview: Provides CloneAbuseConfig dataclass with toggles for controlling detection of
7
+ .clone() abuse patterns in Rust code. Supports toggling detection of clone-in-loop,
8
+ clone-chain, and unnecessary-clone patterns independently. Includes configuration for
9
+ allowing calls in test code, ignoring example and benchmark directories. Configuration
10
+ loads from YAML with sensible defaults via from_dict() class method.
11
+
12
+ Dependencies: dataclasses, typing
13
+
14
+ Exports: CloneAbuseConfig
15
+
16
+ Interfaces: CloneAbuseConfig.from_dict() for YAML configuration loading
17
+
18
+ Implementation: Dataclass with factory defaults and conservative default settings
19
+ """
20
+
21
+ from dataclasses import dataclass, field
22
+ from typing import Any
23
+
24
+
25
+ @dataclass
26
+ class CloneAbuseConfig:
27
+ """Configuration for clone abuse detection."""
28
+
29
+ enabled: bool = True
30
+
31
+ # Allow .clone() in test functions and #[cfg(test)] modules
32
+ allow_in_tests: bool = True
33
+
34
+ # Toggle detection of .clone() inside loop bodies
35
+ detect_clone_in_loop: bool = True
36
+
37
+ # Toggle detection of chained .clone().clone() calls
38
+ detect_clone_chain: bool = True
39
+
40
+ # Toggle detection of unnecessary clones (clone before move)
41
+ detect_unnecessary_clone: bool = True
42
+
43
+ # File path patterns to ignore (e.g., examples/, benches/)
44
+ ignore: list[str] = field(default_factory=lambda: ["examples/", "benches/", "tests/"])
45
+
46
+ @classmethod
47
+ def from_dict(cls, config: dict[str, Any], language: str | None = None) -> "CloneAbuseConfig":
48
+ """Load configuration from dictionary.
49
+
50
+ Args:
51
+ config: Configuration dictionary from YAML
52
+ language: Language parameter (reserved for future use)
53
+
54
+ Returns:
55
+ Configured CloneAbuseConfig instance
56
+ """
57
+ _ = language
58
+ return cls(
59
+ enabled=config.get("enabled", True),
60
+ allow_in_tests=config.get("allow_in_tests", True),
61
+ detect_clone_in_loop=config.get("detect_clone_in_loop", True),
62
+ detect_clone_chain=config.get("detect_clone_chain", True),
63
+ detect_unnecessary_clone=config.get("detect_unnecessary_clone", True),
64
+ ignore=config.get("ignore", ["examples/", "benches/", "tests/"]),
65
+ )