thailint 0.12.0__py3-none-any.whl → 0.13.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 (121) hide show
  1. src/analyzers/__init__.py +4 -3
  2. src/analyzers/ast_utils.py +54 -0
  3. src/analyzers/typescript_base.py +4 -0
  4. src/cli/__init__.py +3 -0
  5. src/cli/config.py +12 -12
  6. src/cli/config_merge.py +241 -0
  7. src/cli/linters/__init__.py +3 -0
  8. src/cli/linters/code_patterns.py +113 -5
  9. src/cli/linters/code_smells.py +4 -0
  10. src/cli/linters/documentation.py +3 -0
  11. src/cli/linters/structure.py +3 -0
  12. src/cli/linters/structure_quality.py +3 -0
  13. src/cli_main.py +3 -0
  14. src/config.py +2 -1
  15. src/core/base.py +3 -2
  16. src/core/cli_utils.py +3 -1
  17. src/core/config_parser.py +5 -2
  18. src/core/constants.py +54 -0
  19. src/core/linter_utils.py +4 -0
  20. src/core/rule_discovery.py +5 -1
  21. src/core/violation_builder.py +3 -0
  22. src/linter_config/directive_markers.py +109 -0
  23. src/linter_config/ignore.py +225 -383
  24. src/linter_config/pattern_utils.py +65 -0
  25. src/linter_config/rule_matcher.py +89 -0
  26. src/linters/collection_pipeline/any_all_analyzer.py +281 -0
  27. src/linters/collection_pipeline/ast_utils.py +40 -0
  28. src/linters/collection_pipeline/config.py +12 -0
  29. src/linters/collection_pipeline/continue_analyzer.py +2 -8
  30. src/linters/collection_pipeline/detector.py +262 -32
  31. src/linters/collection_pipeline/filter_map_analyzer.py +402 -0
  32. src/linters/collection_pipeline/linter.py +18 -35
  33. src/linters/collection_pipeline/suggestion_builder.py +68 -1
  34. src/linters/dry/base_token_analyzer.py +16 -9
  35. src/linters/dry/block_filter.py +7 -4
  36. src/linters/dry/cache.py +7 -2
  37. src/linters/dry/config.py +7 -1
  38. src/linters/dry/constant_matcher.py +34 -25
  39. src/linters/dry/file_analyzer.py +4 -2
  40. src/linters/dry/inline_ignore.py +7 -16
  41. src/linters/dry/linter.py +48 -25
  42. src/linters/dry/python_analyzer.py +18 -10
  43. src/linters/dry/python_constant_extractor.py +51 -52
  44. src/linters/dry/single_statement_detector.py +14 -12
  45. src/linters/dry/token_hasher.py +115 -115
  46. src/linters/dry/typescript_analyzer.py +11 -6
  47. src/linters/dry/typescript_constant_extractor.py +4 -0
  48. src/linters/dry/typescript_statement_detector.py +208 -208
  49. src/linters/dry/typescript_value_extractor.py +3 -0
  50. src/linters/dry/violation_filter.py +1 -4
  51. src/linters/dry/violation_generator.py +1 -4
  52. src/linters/file_header/atemporal_detector.py +4 -0
  53. src/linters/file_header/base_parser.py +4 -0
  54. src/linters/file_header/bash_parser.py +4 -0
  55. src/linters/file_header/field_validator.py +5 -8
  56. src/linters/file_header/linter.py +19 -12
  57. src/linters/file_header/markdown_parser.py +6 -0
  58. src/linters/file_placement/config_loader.py +3 -1
  59. src/linters/file_placement/linter.py +22 -8
  60. src/linters/file_placement/pattern_matcher.py +21 -4
  61. src/linters/file_placement/pattern_validator.py +21 -7
  62. src/linters/file_placement/rule_checker.py +2 -2
  63. src/linters/lazy_ignores/__init__.py +43 -0
  64. src/linters/lazy_ignores/config.py +66 -0
  65. src/linters/lazy_ignores/directive_utils.py +121 -0
  66. src/linters/lazy_ignores/header_parser.py +177 -0
  67. src/linters/lazy_ignores/linter.py +158 -0
  68. src/linters/lazy_ignores/matcher.py +135 -0
  69. src/linters/lazy_ignores/python_analyzer.py +201 -0
  70. src/linters/lazy_ignores/rule_id_utils.py +180 -0
  71. src/linters/lazy_ignores/skip_detector.py +298 -0
  72. src/linters/lazy_ignores/types.py +67 -0
  73. src/linters/lazy_ignores/typescript_analyzer.py +146 -0
  74. src/linters/lazy_ignores/violation_builder.py +131 -0
  75. src/linters/lbyl/__init__.py +29 -0
  76. src/linters/lbyl/config.py +63 -0
  77. src/linters/lbyl/pattern_detectors/__init__.py +25 -0
  78. src/linters/lbyl/pattern_detectors/base.py +46 -0
  79. src/linters/magic_numbers/context_analyzer.py +227 -229
  80. src/linters/magic_numbers/linter.py +20 -15
  81. src/linters/magic_numbers/python_analyzer.py +4 -16
  82. src/linters/magic_numbers/typescript_analyzer.py +9 -16
  83. src/linters/method_property/config.py +4 -0
  84. src/linters/method_property/linter.py +5 -4
  85. src/linters/method_property/python_analyzer.py +5 -4
  86. src/linters/method_property/violation_builder.py +3 -0
  87. src/linters/nesting/typescript_analyzer.py +6 -12
  88. src/linters/nesting/typescript_function_extractor.py +0 -4
  89. src/linters/print_statements/linter.py +6 -4
  90. src/linters/print_statements/python_analyzer.py +85 -81
  91. src/linters/print_statements/typescript_analyzer.py +6 -15
  92. src/linters/srp/heuristics.py +4 -4
  93. src/linters/srp/linter.py +12 -12
  94. src/linters/srp/violation_builder.py +0 -4
  95. src/linters/stateless_class/linter.py +30 -36
  96. src/linters/stateless_class/python_analyzer.py +11 -20
  97. src/linters/stringly_typed/config.py +4 -5
  98. src/linters/stringly_typed/context_filter.py +410 -410
  99. src/linters/stringly_typed/function_call_violation_builder.py +93 -95
  100. src/linters/stringly_typed/linter.py +48 -16
  101. src/linters/stringly_typed/python/analyzer.py +5 -1
  102. src/linters/stringly_typed/python/call_tracker.py +8 -5
  103. src/linters/stringly_typed/python/comparison_tracker.py +10 -5
  104. src/linters/stringly_typed/python/condition_extractor.py +3 -0
  105. src/linters/stringly_typed/python/conditional_detector.py +4 -1
  106. src/linters/stringly_typed/python/match_analyzer.py +8 -2
  107. src/linters/stringly_typed/python/validation_detector.py +3 -0
  108. src/linters/stringly_typed/storage.py +14 -14
  109. src/linters/stringly_typed/typescript/call_tracker.py +9 -3
  110. src/linters/stringly_typed/typescript/comparison_tracker.py +9 -3
  111. src/linters/stringly_typed/violation_generator.py +288 -259
  112. src/orchestrator/core.py +13 -4
  113. src/templates/thailint_config_template.yaml +166 -0
  114. src/utils/project_root.py +3 -0
  115. thailint-0.13.0.dist-info/METADATA +184 -0
  116. thailint-0.13.0.dist-info/RECORD +189 -0
  117. thailint-0.12.0.dist-info/METADATA +0 -1667
  118. thailint-0.12.0.dist-info/RECORD +0 -164
  119. {thailint-0.12.0.dist-info → thailint-0.13.0.dist-info}/WHEEL +0 -0
  120. {thailint-0.12.0.dist-info → thailint-0.13.0.dist-info}/entry_points.txt +0 -0
  121. {thailint-0.12.0.dist-info → thailint-0.13.0.dist-info}/licenses/LICENSE +0 -0
src/core/config_parser.py CHANGED
@@ -27,6 +27,8 @@ from typing import Any, TextIO
27
27
 
28
28
  import yaml
29
29
 
30
+ from src.core.constants import CONFIG_EXTENSIONS
31
+
30
32
 
31
33
  class ConfigParseError(Exception):
32
34
  """Configuration file parsing errors."""
@@ -113,11 +115,12 @@ def parse_config_file(path: Path, encoding: str = "utf-8") -> dict[str, Any]:
113
115
  """
114
116
  suffix = path.suffix.lower()
115
117
 
116
- if suffix not in [".yaml", ".yml", ".json"]:
118
+ valid_suffixes = (*CONFIG_EXTENSIONS, ".json")
119
+ if suffix not in valid_suffixes:
117
120
  raise ConfigParseError(f"Unsupported config format: {suffix}")
118
121
 
119
122
  with path.open(encoding=encoding) as f:
120
- if suffix in [".yaml", ".yml"]:
123
+ if suffix in CONFIG_EXTENSIONS:
121
124
  config = parse_yaml(f, path)
122
125
  else:
123
126
  config = parse_json(f, path)
src/core/constants.py ADDED
@@ -0,0 +1,54 @@
1
+ """
2
+ Purpose: Core constants and enums used across the thai-lint codebase
3
+
4
+ Scope: Centralized definitions for language names, storage modes, config extensions
5
+
6
+ Overview: Provides type-safe enums and constants for consistent stringly-typed patterns
7
+ across the codebase. Includes Language enum for programming language detection,
8
+ StorageMode for cache storage options, and CONFIG_EXTENSIONS for config file
9
+ discovery. Using enums ensures compile-time safety and IDE autocompletion.
10
+
11
+ Dependencies: enum module
12
+
13
+ Exports: Language enum, StorageMode enum, CONFIG_EXTENSIONS, IgnoreDirective enum,
14
+ HEADER_SCAN_LINES, MAX_ATTRIBUTE_CHAIN_DEPTH
15
+
16
+ Interfaces: Use enum values instead of string literals throughout codebase
17
+
18
+ Implementation: Standard Python enums with string values for compatibility
19
+ """
20
+
21
+ from enum import Enum
22
+
23
+
24
+ class Language(str, Enum):
25
+ """Supported programming languages for linting."""
26
+
27
+ PYTHON = "python"
28
+ TYPESCRIPT = "typescript"
29
+ JAVASCRIPT = "javascript"
30
+ MARKDOWN = "markdown"
31
+
32
+
33
+ class StorageMode(str, Enum):
34
+ """Storage modes for DRY linter cache."""
35
+
36
+ MEMORY = "memory"
37
+ TEMPFILE = "tempfile"
38
+
39
+
40
+ class IgnoreDirective(str, Enum):
41
+ """Inline ignore directive types."""
42
+
43
+ IGNORE = "ignore"
44
+ IGNORE_FILE = "ignore-file"
45
+
46
+
47
+ # Valid config file extensions
48
+ CONFIG_EXTENSIONS: tuple[str, str] = (".yaml", ".yml")
49
+
50
+ # Number of lines to scan at file start for ignore directives and headers
51
+ HEADER_SCAN_LINES: int = 10
52
+
53
+ # Maximum depth for attribute chain traversal (e.g., obj.attr.attr2.attr3)
54
+ MAX_ATTRIBUTE_CHAIN_DEPTH: int = 3
src/core/linter_utils.py CHANGED
@@ -16,6 +16,10 @@ Exports: get_metadata, get_metadata_value, load_linter_config, has_file_content
16
16
  Interfaces: All functions take BaseLintContext and return typed values (dict, str, bool, Any)
17
17
 
18
18
  Implementation: Type-safe metadata access with fallbacks, generic config loading with language support
19
+
20
+ Suppressions:
21
+ - invalid-name: T type variable follows Python generic naming convention
22
+ - type:ignore[return-value]: Generic config factory with runtime type checking
19
23
  """
20
24
 
21
25
  from typing import Any, Protocol, TypeVar
@@ -19,12 +19,15 @@ 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
23
24
  from types import ModuleType
24
25
  from typing import Any
25
26
 
26
27
  from .base import BaseLintRule
27
28
 
29
+ logger = logging.getLogger(__name__)
30
+
28
31
 
29
32
  def discover_from_package(package_path: str) -> list[BaseLintRule]:
30
33
  """Discover rules from a package and its modules.
@@ -37,7 +40,8 @@ def discover_from_package(package_path: str) -> list[BaseLintRule]:
37
40
  """
38
41
  try:
39
42
  package = importlib.import_module(package_path)
40
- except ImportError:
43
+ except ImportError as e:
44
+ logger.debug("Failed to import package %s: %s", package_path, e)
41
45
  return []
42
46
 
43
47
  if not hasattr(package, "__path__"):
@@ -21,6 +21,9 @@ Interfaces: ViolationInfo(rule_id, file_path, line, message, column, severity),
21
21
 
22
22
  Implementation: Uses dataclass for type-safe violation info, functions provide build logic
23
23
  that constructs Violation objects with proper defaults
24
+
25
+ Suppressions:
26
+ - too-many-arguments,too-many-positional-arguments: Violation fields as parameters
24
27
  """
25
28
 
26
29
  from dataclasses import dataclass
@@ -0,0 +1,109 @@
1
+ """
2
+ Purpose: Ignore directive marker detection for thailint comments
3
+
4
+ Scope: Detection of thailint and design-lint ignore markers in source code
5
+
6
+ Overview: Provides functions for detecting various ignore directive markers in code
7
+ comments. Supports file-level ignores, line-level ignores, block ignores, and
8
+ next-line ignores. Works with both Python (#) and JavaScript (//) comment styles.
9
+ All checks are case-insensitive.
10
+
11
+ Dependencies: None (pure string operations)
12
+
13
+ Exports: Marker detection functions for various ignore directive types
14
+
15
+ Interfaces: has_*_marker(line) -> bool for each marker type
16
+
17
+ Implementation: String-based pattern detection with case-insensitive matching
18
+ """
19
+
20
+
21
+ def has_ignore_directive_marker(line: str) -> bool:
22
+ """Check if line contains a file-level ignore directive marker.
23
+
24
+ Args:
25
+ line: Line of code to check
26
+
27
+ Returns:
28
+ True if line has ignore-file marker
29
+ """
30
+ line_lower = line.lower()
31
+ return "# thailint: ignore-file" in line_lower or "# design-lint: ignore-file" in line_lower
32
+
33
+
34
+ def has_line_ignore_marker(code: str) -> bool:
35
+ """Check if code line has an inline ignore marker.
36
+
37
+ Args:
38
+ code: Line of code to check
39
+
40
+ Returns:
41
+ True if line has inline ignore marker
42
+ """
43
+ code_lower = code.lower()
44
+ return (
45
+ "# thailint: ignore" in code_lower
46
+ or "# design-lint: ignore" in code_lower
47
+ or "// thailint: ignore" in code_lower
48
+ or "// design-lint: ignore" in code_lower
49
+ )
50
+
51
+
52
+ def has_ignore_next_line_marker(line: str) -> bool:
53
+ """Check if line has ignore-next-line marker.
54
+
55
+ Args:
56
+ line: Line of code to check
57
+
58
+ Returns:
59
+ True if line has ignore-next-line marker
60
+ """
61
+ return "# thailint: ignore-next-line" in line or "# design-lint: ignore-next-line" in line
62
+
63
+
64
+ def has_ignore_start_marker(line: str) -> bool:
65
+ """Check if line has ignore-start comment marker.
66
+
67
+ Only matches actual comment lines (starting with # or //), not strings
68
+ containing the marker text.
69
+
70
+ Args:
71
+ line: Line of code to check
72
+
73
+ Returns:
74
+ True if line is a proper ignore-start comment
75
+ """
76
+ stripped = line.strip().lower()
77
+ if not (stripped.startswith("#") or stripped.startswith("//")):
78
+ return False
79
+ return "ignore-start" in stripped and ("thailint:" in stripped or "design-lint:" in stripped)
80
+
81
+
82
+ def has_ignore_end_marker(line: str) -> bool:
83
+ """Check if line has ignore-end comment marker.
84
+
85
+ Only matches actual comment lines (starting with # or //), not strings
86
+ containing the marker text.
87
+
88
+ Args:
89
+ line: Line of code to check
90
+
91
+ Returns:
92
+ True if line is a proper ignore-end comment
93
+ """
94
+ stripped = line.strip().lower()
95
+ if not (stripped.startswith("#") or stripped.startswith("//")):
96
+ return False
97
+ return "ignore-end" in stripped and ("thailint:" in stripped or "design-lint:" in stripped)
98
+
99
+
100
+ def check_general_ignore(line: str) -> bool:
101
+ """Check if line has general ignore directive (no specific rules).
102
+
103
+ Args:
104
+ line: Line containing ignore directive
105
+
106
+ Returns:
107
+ True if no specific rules are specified (not bracket syntax)
108
+ """
109
+ return "ignore-file[" not in line