thailint 0.10.0__py3-none-any.whl → 0.11.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 (62) hide show
  1. src/__init__.py +1 -0
  2. src/cli/__init__.py +27 -0
  3. src/cli/__main__.py +22 -0
  4. src/cli/config.py +478 -0
  5. src/cli/linters/__init__.py +58 -0
  6. src/cli/linters/code_patterns.py +372 -0
  7. src/cli/linters/code_smells.py +343 -0
  8. src/cli/linters/documentation.py +155 -0
  9. src/cli/linters/shared.py +89 -0
  10. src/cli/linters/structure.py +313 -0
  11. src/cli/linters/structure_quality.py +316 -0
  12. src/cli/main.py +120 -0
  13. src/cli/utils.py +375 -0
  14. src/cli_main.py +34 -0
  15. src/core/types.py +13 -0
  16. src/core/violation_utils.py +69 -0
  17. src/linter_config/ignore.py +32 -16
  18. src/linters/collection_pipeline/linter.py +2 -2
  19. src/linters/dry/block_filter.py +97 -1
  20. src/linters/dry/cache.py +94 -6
  21. src/linters/dry/config.py +47 -10
  22. src/linters/dry/constant.py +92 -0
  23. src/linters/dry/constant_matcher.py +214 -0
  24. src/linters/dry/constant_violation_builder.py +98 -0
  25. src/linters/dry/linter.py +89 -48
  26. src/linters/dry/python_analyzer.py +12 -415
  27. src/linters/dry/python_constant_extractor.py +101 -0
  28. src/linters/dry/single_statement_detector.py +415 -0
  29. src/linters/dry/token_hasher.py +5 -5
  30. src/linters/dry/typescript_analyzer.py +5 -354
  31. src/linters/dry/typescript_constant_extractor.py +134 -0
  32. src/linters/dry/typescript_statement_detector.py +255 -0
  33. src/linters/dry/typescript_value_extractor.py +66 -0
  34. src/linters/file_header/linter.py +2 -2
  35. src/linters/file_placement/linter.py +2 -2
  36. src/linters/file_placement/pattern_matcher.py +19 -5
  37. src/linters/magic_numbers/linter.py +8 -67
  38. src/linters/magic_numbers/typescript_ignore_checker.py +81 -0
  39. src/linters/nesting/linter.py +12 -9
  40. src/linters/print_statements/linter.py +7 -24
  41. src/linters/srp/class_analyzer.py +9 -9
  42. src/linters/srp/heuristics.py +2 -2
  43. src/linters/srp/linter.py +2 -2
  44. src/linters/stateless_class/linter.py +2 -2
  45. src/linters/stringly_typed/__init__.py +23 -0
  46. src/linters/stringly_typed/config.py +165 -0
  47. src/linters/stringly_typed/python/__init__.py +29 -0
  48. src/linters/stringly_typed/python/analyzer.py +198 -0
  49. src/linters/stringly_typed/python/condition_extractor.py +131 -0
  50. src/linters/stringly_typed/python/conditional_detector.py +176 -0
  51. src/linters/stringly_typed/python/constants.py +21 -0
  52. src/linters/stringly_typed/python/match_analyzer.py +88 -0
  53. src/linters/stringly_typed/python/validation_detector.py +186 -0
  54. src/linters/stringly_typed/python/variable_extractor.py +96 -0
  55. src/orchestrator/core.py +241 -12
  56. {thailint-0.10.0.dist-info → thailint-0.11.0.dist-info}/METADATA +2 -2
  57. {thailint-0.10.0.dist-info → thailint-0.11.0.dist-info}/RECORD +60 -28
  58. thailint-0.11.0.dist-info/entry_points.txt +4 -0
  59. src/cli.py +0 -2141
  60. thailint-0.10.0.dist-info/entry_points.txt +0 -4
  61. {thailint-0.10.0.dist-info → thailint-0.11.0.dist-info}/WHEEL +0 -0
  62. {thailint-0.10.0.dist-info → thailint-0.11.0.dist-info}/licenses/LICENSE +0 -0
@@ -30,7 +30,7 @@ SRP Exception: TypeScriptDuplicateAnalyzer has 20 methods and 324 lines (exceeds
30
30
  responsibility: accurately detecting duplicate TypeScript/JavaScript code while minimizing false positives.
31
31
  """
32
32
 
33
- from collections.abc import Generator, Iterable
33
+ from collections.abc import Iterable
34
34
  from pathlib import Path
35
35
 
36
36
  from src.analyzers.typescript_base import TREE_SITTER_AVAILABLE
@@ -39,6 +39,7 @@ from .base_token_analyzer import BaseTokenAnalyzer
39
39
  from .block_filter import BlockFilterRegistry, create_default_registry
40
40
  from .cache import CodeBlock
41
41
  from .config import DRYConfig
42
+ from .typescript_statement_detector import TypeScriptStatementDetector
42
43
 
43
44
  if TREE_SITTER_AVAILABLE:
44
45
  from tree_sitter import Node
@@ -61,6 +62,7 @@ class TypeScriptDuplicateAnalyzer(BaseTokenAnalyzer): # thailint: ignore[srp.vi
61
62
  """
62
63
  super().__init__()
63
64
  self._filter_registry = filter_registry or create_default_registry()
65
+ self._statement_detector = TypeScriptStatementDetector()
64
66
 
65
67
  def analyze(self, file_path: Path, content: str, config: DRYConfig) -> list[CodeBlock]:
66
68
  """Analyze TypeScript/JavaScript file for duplicate code blocks.
@@ -88,8 +90,8 @@ class TypeScriptDuplicateAnalyzer(BaseTokenAnalyzer): # thailint: ignore[srp.vi
88
90
  valid_windows = (
89
91
  (hash_val, start_line, end_line, snippet)
90
92
  for hash_val, start_line, end_line, snippet in windows
91
- if self._should_include_block(content, start_line, end_line)
92
- and not self._is_single_statement_in_source(content, start_line, end_line)
93
+ if self._statement_detector.should_include_block(content, start_line, end_line)
94
+ and not self._statement_detector.is_single_statement(content, start_line, end_line)
93
95
  )
94
96
  return self._build_blocks(valid_windows, file_path, content)
95
97
 
@@ -269,354 +271,3 @@ class TypeScriptDuplicateAnalyzer(BaseTokenAnalyzer): # thailint: ignore[srp.vi
269
271
  hashes.append((hash_val, start_line, end_line, snippet))
270
272
 
271
273
  return hashes
272
-
273
- def _should_include_block(self, content: str, start_line: int, end_line: int) -> bool:
274
- """Filter out blocks that overlap with interface/type definitions.
275
-
276
- Args:
277
- content: File content
278
- start_line: Block start line
279
- end_line: Block end line
280
-
281
- Returns:
282
- False if block overlaps interface definition, True otherwise
283
- """
284
- interface_ranges = self._find_interface_ranges(content)
285
- return not self._overlaps_interface(start_line, end_line, interface_ranges)
286
-
287
- def _is_single_statement_in_source(self, content: str, start_line: int, end_line: int) -> bool:
288
- """Check if a line range in the original source is a single logical statement.
289
-
290
- Uses tree-sitter AST analysis to detect patterns like:
291
- - Decorators (@Component(...))
292
- - Function call arguments
293
- - Object literal properties
294
- - Class field definitions
295
- - Type assertions
296
- - Chained method calls (single expression)
297
-
298
- Args:
299
- content: TypeScript source code
300
- start_line: Starting line number (1-indexed)
301
- end_line: Ending line number (1-indexed)
302
-
303
- Returns:
304
- True if this range represents a single logical statement/expression
305
- """
306
- if not TREE_SITTER_AVAILABLE:
307
- return False
308
-
309
- from src.analyzers.typescript_base import TypeScriptBaseAnalyzer
310
-
311
- analyzer = TypeScriptBaseAnalyzer()
312
- root = analyzer.parse_typescript(content)
313
- if not root:
314
- return False
315
-
316
- return self._check_overlapping_nodes(root, start_line, end_line)
317
-
318
- def _check_overlapping_nodes(self, root: Node, start_line: int, end_line: int) -> bool:
319
- """Check if any AST node overlaps and matches single-statement pattern.
320
-
321
- Args:
322
- root: Root tree-sitter node
323
- start_line: Starting line number (1-indexed)
324
- end_line: Ending line number (1-indexed)
325
-
326
- Returns:
327
- True if any node matches single-statement pattern
328
- """
329
- # Convert to 0-indexed for tree-sitter
330
- ts_start = start_line - 1
331
- ts_end = end_line - 1
332
-
333
- for node in self._walk_nodes(root):
334
- if self._node_overlaps_and_matches(node, ts_start, ts_end):
335
- return True
336
- return False
337
-
338
- def _walk_nodes(self, node: Node) -> Generator[Node, None, None]:
339
- """Generator to walk all nodes in tree.
340
-
341
- Args:
342
- node: Starting node
343
-
344
- Yields:
345
- All nodes in tree
346
- """
347
- yield node
348
- for child in node.children:
349
- yield from self._walk_nodes(child)
350
-
351
- def _node_overlaps_and_matches(self, node: Node, ts_start: int, ts_end: int) -> bool:
352
- """Check if node overlaps with range and matches single-statement pattern.
353
-
354
- Args:
355
- node: Tree-sitter node
356
- ts_start: Starting line (0-indexed)
357
- ts_end: Ending line (0-indexed)
358
-
359
- Returns:
360
- True if node overlaps and matches pattern
361
- """
362
- node_start = node.start_point[0]
363
- node_end = node.end_point[0]
364
-
365
- # Check if ranges overlap
366
- overlaps = not (node_end < ts_start or node_start > ts_end)
367
- if not overlaps:
368
- return False
369
-
370
- return self._is_single_statement_pattern(node, ts_start, ts_end)
371
-
372
- def _matches_simple_container_pattern(self, node: Node, contains: bool) -> bool:
373
- """Check if node is a simple container pattern (decorator, object, etc.).
374
-
375
- Args:
376
- node: AST node to check
377
- contains: Whether node contains the range
378
-
379
- Returns:
380
- True if node matches simple container pattern
381
- """
382
- simple_types = (
383
- "decorator",
384
- "object",
385
- "member_expression",
386
- "as_expression",
387
- "array_pattern",
388
- )
389
- return node.type in simple_types and contains
390
-
391
- def _matches_call_expression_pattern(
392
- self, node: Node, ts_start: int, ts_end: int, contains: bool
393
- ) -> bool:
394
- """Check if node is a call expression pattern.
395
-
396
- Args:
397
- node: AST node to check
398
- ts_start: Starting line (0-indexed)
399
- ts_end: Ending line (0-indexed)
400
- contains: Whether node contains the range
401
-
402
- Returns:
403
- True if node matches call expression pattern
404
- """
405
- if node.type != "call_expression":
406
- return False
407
-
408
- # Check if this is a multi-line call containing the range
409
- node_start = node.start_point[0]
410
- node_end = node.end_point[0]
411
- is_multiline = node_start < node_end
412
- if is_multiline and node_start <= ts_start <= node_end:
413
- return True
414
-
415
- return contains
416
-
417
- def _matches_declaration_pattern(self, node: Node, contains: bool) -> bool:
418
- """Check if node is a lexical declaration pattern.
419
-
420
- Args:
421
- node: AST node to check
422
- contains: Whether node contains the range
423
-
424
- Returns:
425
- True if node matches declaration pattern (excluding function bodies)
426
- """
427
- if node.type != "lexical_declaration" or not contains:
428
- return False
429
-
430
- # Only filter if simple value assignment, NOT a function body
431
- if self._contains_function_body(node):
432
- return False
433
-
434
- return True
435
-
436
- def _matches_jsx_pattern(self, node: Node, contains: bool) -> bool:
437
- """Check if node is a JSX element pattern.
438
-
439
- Args:
440
- node: AST node to check
441
- contains: Whether node contains the range
442
-
443
- Returns:
444
- True if node matches JSX pattern
445
- """
446
- jsx_types = ("jsx_opening_element", "jsx_self_closing_element")
447
- return node.type in jsx_types and contains
448
-
449
- def _matches_class_body_pattern(self, node: Node, ts_start: int, ts_end: int) -> bool:
450
- """Check if node is a class body field definition pattern.
451
-
452
- Args:
453
- node: AST node to check
454
- ts_start: Starting line (0-indexed)
455
- ts_end: Ending line (0-indexed)
456
-
457
- Returns:
458
- True if node is class body with field definitions
459
- """
460
- if node.type != "class_body":
461
- return False
462
-
463
- return self._is_in_class_field_area(node, ts_start, ts_end)
464
-
465
- def _is_single_statement_pattern(self, node: Node, ts_start: int, ts_end: int) -> bool:
466
- """Check if an AST node represents a single-statement pattern to filter.
467
-
468
- Delegates to specialized pattern matchers for different AST node categories.
469
-
470
- Args:
471
- node: AST node that overlaps with the line range
472
- ts_start: Starting line number (0-indexed)
473
- ts_end: Ending line number (0-indexed)
474
-
475
- Returns:
476
- True if this node represents a single logical statement pattern
477
- """
478
- node_start = node.start_point[0]
479
- node_end = node.end_point[0]
480
- contains = (node_start <= ts_start) and (node_end >= ts_end)
481
-
482
- # Check pattern categories using specialized helpers - use list for any()
483
- matchers = [
484
- self._matches_simple_container_pattern(node, contains),
485
- self._matches_call_expression_pattern(node, ts_start, ts_end, contains),
486
- self._matches_declaration_pattern(node, contains),
487
- self._matches_jsx_pattern(node, contains),
488
- self._matches_class_body_pattern(node, ts_start, ts_end),
489
- ]
490
- return any(matchers)
491
-
492
- def _contains_function_body(self, node: Node) -> bool:
493
- """Check if node contains an arrow function or function expression.
494
-
495
- Args:
496
- node: Node to check
497
-
498
- Returns:
499
- True if node contains a function with a body
500
- """
501
- for child in node.children:
502
- if child.type in ("arrow_function", "function", "function_expression"):
503
- return True
504
- if self._contains_function_body(child):
505
- return True
506
- return False
507
-
508
- def _find_first_method_line(self, class_body: Node) -> int | None:
509
- """Find line number of first method in class body.
510
-
511
- Args:
512
- class_body: Class body node
513
-
514
- Returns:
515
- Line number of first method or None if no methods
516
- """
517
- for child in class_body.children:
518
- if child.type in ("method_definition", "function_declaration"):
519
- return child.start_point[0]
520
- return None
521
-
522
- def _is_in_class_field_area(self, class_body: Node, ts_start: int, ts_end: int) -> bool:
523
- """Check if range is in class field definition area (before methods).
524
-
525
- Args:
526
- class_body: Class body node
527
- ts_start: Starting line (0-indexed)
528
- ts_end: Ending line (0-indexed)
529
-
530
- Returns:
531
- True if range is in field area
532
- """
533
- first_method_line = self._find_first_method_line(class_body)
534
- class_start = class_body.start_point[0]
535
- class_end = class_body.end_point[0]
536
-
537
- # No methods: check if range is in class body
538
- if first_method_line is None:
539
- return class_start <= ts_start and class_end >= ts_end
540
-
541
- # Has methods: check if range is before first method
542
- return class_start <= ts_start and ts_end < first_method_line
543
-
544
- def _find_interface_ranges(self, content: str) -> list[tuple[int, int]]:
545
- """Find line ranges of interface/type definitions.
546
-
547
- Args:
548
- content: File content
549
-
550
- Returns:
551
- List of (start_line, end_line) tuples for interface blocks
552
- """
553
- ranges: list[tuple[int, int]] = []
554
- lines = content.split("\n")
555
- state = {"in_interface": False, "start_line": 0, "brace_count": 0}
556
-
557
- for i, line in enumerate(lines, start=1):
558
- stripped = line.strip()
559
- self._process_line_for_interface(stripped, i, state, ranges)
560
-
561
- return ranges
562
-
563
- def _process_line_for_interface(
564
- self, stripped: str, line_num: int, state: dict, ranges: list[tuple[int, int]]
565
- ) -> None:
566
- """Process single line for interface detection.
567
-
568
- Args:
569
- stripped: Stripped line content
570
- line_num: Line number
571
- state: Tracking state (in_interface, start_line, brace_count)
572
- ranges: Accumulated interface ranges
573
- """
574
- if self._is_interface_start(stripped):
575
- self._handle_interface_start(stripped, line_num, state, ranges)
576
- return
577
-
578
- if state["in_interface"]:
579
- self._handle_interface_continuation(stripped, line_num, state, ranges)
580
-
581
- def _is_interface_start(self, stripped: str) -> bool:
582
- """Check if line starts interface/type definition."""
583
- return stripped.startswith(("interface ", "type ")) and "{" in stripped
584
-
585
- def _handle_interface_start(
586
- self, stripped: str, line_num: int, state: dict, ranges: list[tuple[int, int]]
587
- ) -> None:
588
- """Handle start of interface definition."""
589
- state["in_interface"] = True
590
- state["start_line"] = line_num
591
- state["brace_count"] = stripped.count("{") - stripped.count("}")
592
-
593
- if state["brace_count"] == 0: # Single-line interface
594
- ranges.append((line_num, line_num))
595
- state["in_interface"] = False
596
-
597
- def _handle_interface_continuation(
598
- self, stripped: str, line_num: int, state: dict, ranges: list[tuple[int, int]]
599
- ) -> None:
600
- """Handle continuation of interface definition."""
601
- state["brace_count"] += stripped.count("{") - stripped.count("}")
602
- if state["brace_count"] == 0:
603
- ranges.append((state["start_line"], line_num))
604
- state["in_interface"] = False
605
-
606
- def _overlaps_interface(
607
- self, start: int, end: int, interface_ranges: list[tuple[int, int]]
608
- ) -> bool:
609
- """Check if block overlaps with any interface range.
610
-
611
- Args:
612
- start: Block start line
613
- end: Block end line
614
- interface_ranges: List of interface definition ranges
615
-
616
- Returns:
617
- True if block overlaps with an interface
618
- """
619
- for if_start, if_end in interface_ranges:
620
- if start <= if_end and end >= if_start:
621
- return True
622
- return False
@@ -0,0 +1,134 @@
1
+ """
2
+ Purpose: Extract TypeScript module-level constants using tree-sitter parsing
3
+
4
+ Scope: TypeScript constant extraction for duplicate constants detection
5
+
6
+ Overview: Extracts module-level constant definitions from TypeScript source code using tree-sitter.
7
+ Identifies constants as top-level `const` declarations where the variable name matches the
8
+ UPPER_SNAKE_CASE naming convention (e.g., const API_TIMEOUT = 30). Excludes non-const
9
+ declarations (let, var), class-level constants, and function-level constants to focus on
10
+ public module constants that should be consolidated across files.
11
+
12
+ Dependencies: tree-sitter, tree-sitter-typescript, re for pattern matching, ConstantInfo,
13
+ TypeScriptValueExtractor
14
+
15
+ Exports: TypeScriptConstantExtractor class
16
+
17
+ Interfaces: TypeScriptConstantExtractor.extract(content: str) -> list[ConstantInfo]
18
+
19
+ Implementation: Tree-sitter-based parsing with const declaration filtering and ALL_CAPS regex matching
20
+ """
21
+
22
+ from typing import Any
23
+
24
+ from src.analyzers.typescript_base import TREE_SITTER_AVAILABLE, TS_PARSER
25
+
26
+ from .constant import CONSTANT_NAME_PATTERN, ConstantInfo
27
+ from .typescript_value_extractor import TypeScriptValueExtractor
28
+
29
+ if TREE_SITTER_AVAILABLE:
30
+ from tree_sitter import Node
31
+ else:
32
+ Node = Any # type: ignore[assignment,misc]
33
+
34
+ # Node types that represent values
35
+ VALUE_TYPES = frozenset(
36
+ (
37
+ "number",
38
+ "string",
39
+ "true",
40
+ "false",
41
+ "null",
42
+ "identifier",
43
+ "array",
44
+ "object",
45
+ "call_expression",
46
+ )
47
+ )
48
+
49
+
50
+ class TypeScriptConstantExtractor:
51
+ """Extracts module-level constants from TypeScript source code."""
52
+
53
+ def __init__(self) -> None:
54
+ """Initialize the TypeScript constant extractor."""
55
+ self.tree_sitter_available = TREE_SITTER_AVAILABLE
56
+ self._value_extractor = TypeScriptValueExtractor()
57
+
58
+ def extract(self, content: str) -> list[ConstantInfo]:
59
+ """Extract constants from TypeScript source code."""
60
+ root = _parse_content(content)
61
+ if root is None:
62
+ return []
63
+ constants: list[ConstantInfo] = []
64
+ for child in root.children:
65
+ constants.extend(self._extract_from_node(child, content))
66
+ return constants
67
+
68
+ def _extract_from_node(self, node: Node, content: str) -> list[ConstantInfo]:
69
+ """Extract constants from a single AST node."""
70
+ if node.type == "lexical_declaration":
71
+ return self._extract_from_lexical_declaration(node, content)
72
+ if node.type == "export_statement":
73
+ return self._extract_from_export(node, content)
74
+ return []
75
+
76
+ def _extract_from_lexical_declaration(self, node: Node, content: str) -> list[ConstantInfo]:
77
+ """Extract constants from a lexical declaration."""
78
+ if not _is_const_declaration(node):
79
+ return []
80
+ return [
81
+ info
82
+ for c in node.children
83
+ if c.type == "variable_declarator"
84
+ and (info := self._extract_from_declarator(c, content))
85
+ ]
86
+
87
+ def _extract_from_export(self, node: Node, content: str) -> list[ConstantInfo]:
88
+ """Extract constants from an export statement."""
89
+ for child in node.children:
90
+ if child.type == "lexical_declaration":
91
+ return self._extract_from_lexical_declaration(child, content)
92
+ return []
93
+
94
+ def _extract_from_declarator(self, node: Node, content: str) -> ConstantInfo | None:
95
+ """Extract constant info from a variable declarator."""
96
+ name, value = _get_name_and_value(node, content, self._value_extractor)
97
+ if not name or not _is_constant_name(name):
98
+ return None
99
+ return ConstantInfo(name=name, line_number=node.start_point[0] + 1, value=value)
100
+
101
+
102
+ def _parse_content(content: str) -> Node | None:
103
+ """Parse content and return root node, or None on failure."""
104
+ if not TREE_SITTER_AVAILABLE or TS_PARSER is None:
105
+ return None
106
+ try:
107
+ return TS_PARSER.parse(bytes(content, "utf8")).root_node
108
+ except Exception: # pylint: disable=broad-exception-caught
109
+ return None
110
+
111
+
112
+ def _is_const_declaration(node: Node) -> bool:
113
+ """Check if lexical declaration is a const."""
114
+ return any(child.type == "const" for child in node.children)
115
+
116
+
117
+ def _get_name_and_value(
118
+ node: Node, content: str, extractor: TypeScriptValueExtractor
119
+ ) -> tuple[str | None, str | None]:
120
+ """Extract name and value from declarator node."""
121
+ name = next(
122
+ (extractor.get_node_text(c, content) for c in node.children if c.type == "identifier"),
123
+ None,
124
+ )
125
+ value = next(
126
+ (extractor.get_value_string(c, content) for c in node.children if c.type in VALUE_TYPES),
127
+ None,
128
+ )
129
+ return name, value
130
+
131
+
132
+ def _is_constant_name(name: str) -> bool:
133
+ """Check if name matches constant naming convention."""
134
+ return not name.startswith("_") and bool(CONSTANT_NAME_PATTERN.match(name))