thailint 0.4.0__py3-none-any.whl → 0.4.2__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.
src/cli.py CHANGED
@@ -1216,10 +1216,13 @@ def _setup_magic_numbers_orchestrator(
1216
1216
  path_objs: list[Path], config_file: str | None, verbose: bool
1217
1217
  ):
1218
1218
  """Set up orchestrator for magic-numbers command."""
1219
- first_path = path_objs[0] if path_objs else Path.cwd()
1220
- project_root = first_path if first_path.is_dir() else first_path.parent
1221
-
1222
1219
  from src.orchestrator.core import Orchestrator
1220
+ from src.utils.project_root import get_project_root
1221
+
1222
+ # Find actual project root (where .git or .thailint.yaml exists)
1223
+ first_path = path_objs[0] if path_objs else Path.cwd()
1224
+ search_start = first_path if first_path.is_dir() else first_path.parent
1225
+ project_root = get_project_root(search_start)
1223
1226
 
1224
1227
  orchestrator = Orchestrator(project_root=project_root)
1225
1228
 
src/core/config_parser.py CHANGED
@@ -72,18 +72,41 @@ def parse_json(file_obj: TextIO, path: Path) -> dict[str, Any]:
72
72
  raise ConfigParseError(f"Invalid JSON in {path}: {e}") from e
73
73
 
74
74
 
75
+ def _normalize_config_keys(config: dict[str, Any]) -> dict[str, Any]:
76
+ """Normalize configuration keys from hyphens to underscores.
77
+
78
+ Converts top-level keys like "magic-numbers" to "magic_numbers" to match
79
+ internal linter expectations while maintaining backward compatibility with
80
+ both formats in config files.
81
+
82
+ Args:
83
+ config: Configuration dictionary with potentially hyphenated keys
84
+
85
+ Returns:
86
+ Configuration dictionary with normalized (underscored) keys
87
+ """
88
+ normalized = {}
89
+ for key, value in config.items():
90
+ # Replace hyphens with underscores in keys
91
+ normalized_key = key.replace("-", "_")
92
+ normalized[normalized_key] = value
93
+ return normalized
94
+
95
+
75
96
  def parse_config_file(path: Path, encoding: str = "utf-8") -> dict[str, Any]:
76
97
  """Parse configuration file based on extension.
77
98
 
78
99
  Supports .yaml, .yml, and .json formats. Automatically detects format
79
- from file extension and uses appropriate parser.
100
+ from file extension and uses appropriate parser. Normalizes hyphenated
101
+ keys (e.g., "magic-numbers") to underscored keys (e.g., "magic_numbers")
102
+ for internal consistency.
80
103
 
81
104
  Args:
82
105
  path: Path to configuration file.
83
106
  encoding: File encoding (default: utf-8).
84
107
 
85
108
  Returns:
86
- Parsed configuration dictionary.
109
+ Parsed configuration dictionary with normalized keys.
87
110
 
88
111
  Raises:
89
112
  ConfigParseError: If file format is unsupported or parsing fails.
@@ -95,5 +118,9 @@ def parse_config_file(path: Path, encoding: str = "utf-8") -> dict[str, Any]:
95
118
 
96
119
  with path.open(encoding=encoding) as f:
97
120
  if suffix in [".yaml", ".yml"]:
98
- return parse_yaml(f, path)
99
- return parse_json(f, path)
121
+ config = parse_yaml(f, path)
122
+ else:
123
+ config = parse_json(f, path)
124
+
125
+ # Normalize keys from hyphens to underscores
126
+ return _normalize_config_keys(config)
@@ -75,9 +75,12 @@ class FilePlacementLinter:
75
75
  # Load and validate config
76
76
  if config_obj:
77
77
  # Handle both wrapped and unwrapped config formats
78
- # Wrapped: {"file-placement": {...}}
78
+ # Wrapped: {"file-placement": {...}} or {"file_placement": {...}}
79
79
  # Unwrapped: {"directories": {...}, "global_deny": [...], ...}
80
- self.config = config_obj.get("file-placement", config_obj)
80
+ # Try both hyphenated and underscored keys for backward compatibility
81
+ self.config = config_obj.get(
82
+ "file-placement", config_obj.get("file_placement", config_obj)
83
+ )
81
84
  elif config_file:
82
85
  self.config = self._components.config_loader.load_config_file(config_file)
83
86
  else:
@@ -279,7 +282,9 @@ class FilePlacementRule(BaseLintRule): # thailint: ignore[srp.violation]
279
282
 
280
283
  @staticmethod
281
284
  def _get_wrapped_config(context: BaseLintContext) -> dict[str, Any] | None:
282
- """Get config from wrapped format: {"file-placement": {...}}.
285
+ """Get config from wrapped format: {"file-placement": {...}} or {"file_placement": {...}}.
286
+
287
+ Supports both hyphenated and underscored keys for backward compatibility.
283
288
 
284
289
  Args:
285
290
  context: Lint context with metadata
@@ -289,8 +294,12 @@ class FilePlacementRule(BaseLintRule): # thailint: ignore[srp.violation]
289
294
  """
290
295
  if not hasattr(context, "metadata"):
291
296
  return None
297
+ # Try hyphenated format first (original format)
292
298
  if "file-placement" in context.metadata:
293
299
  return context.metadata["file-placement"]
300
+ # Try underscored format (normalized format)
301
+ if "file_placement" in context.metadata:
302
+ return context.metadata["file_placement"]
294
303
  return None
295
304
 
296
305
  @staticmethod
@@ -378,9 +387,11 @@ class FilePlacementRule(BaseLintRule): # thailint: ignore[srp.violation]
378
387
  try:
379
388
  config = self._parse_layout_file(layout_path)
380
389
 
381
- # Unwrap file-placement key if present
390
+ # Unwrap file-placement key if present (try both formats for backward compatibility)
382
391
  if "file-placement" in config:
383
392
  return config["file-placement"]
393
+ if "file_placement" in config:
394
+ return config["file_placement"]
384
395
 
385
396
  return config
386
397
  except Exception:
@@ -33,6 +33,7 @@ class MagicNumberConfig:
33
33
  default_factory=lambda: {-1, 0, 1, 2, 3, 4, 5, 10, 100, 1000}
34
34
  )
35
35
  max_small_integer: int = 10
36
+ ignore: list[str] = field(default_factory=list)
36
37
 
37
38
  def __post_init__(self) -> None:
38
39
  """Validate configuration values."""
@@ -69,8 +70,13 @@ class MagicNumberConfig:
69
70
  )
70
71
  max_small_integer = config.get("max_small_integer", 10)
71
72
 
73
+ ignore_patterns = config.get("ignore", [])
74
+ if not isinstance(ignore_patterns, list):
75
+ ignore_patterns = []
76
+
72
77
  return cls(
73
78
  enabled=config.get("enabled", True),
74
79
  allowed_numbers=allowed_numbers,
75
80
  max_small_integer=max_small_integer,
81
+ ignore=ignore_patterns,
76
82
  )
@@ -24,6 +24,7 @@ Implementation: Composition pattern with helper classes, AST-based analysis with
24
24
  """
25
25
 
26
26
  import ast
27
+ from pathlib import Path
27
28
 
28
29
  from src.core.base import BaseLintContext, MultiLanguageLintRule
29
30
  from src.core.linter_utils import load_linter_config
@@ -96,7 +97,64 @@ class MagicNumberRule(MultiLanguageLintRule): # thailint: ignore[srp]
96
97
  """Try to load production configuration."""
97
98
  if not hasattr(context, "metadata") or not isinstance(context.metadata, dict):
98
99
  return None
99
- return load_linter_config(context, "magic_numbers", MagicNumberConfig)
100
+
101
+ # Try both hyphenated and underscored keys for backward compatibility
102
+ # The config parser normalizes keys when loading from YAML, but
103
+ # direct metadata injection (tests) may use either format
104
+ metadata = context.metadata
105
+
106
+ # Try underscore version first (normalized format)
107
+ if "magic_numbers" in metadata:
108
+ return load_linter_config(context, "magic_numbers", MagicNumberConfig)
109
+
110
+ # Fallback to hyphenated version (for direct test injection)
111
+ if "magic-numbers" in metadata:
112
+ return load_linter_config(context, "magic-numbers", MagicNumberConfig)
113
+
114
+ # No config found, return None to use defaults
115
+ return None
116
+
117
+ def _is_file_ignored(self, context: BaseLintContext, config: MagicNumberConfig) -> bool:
118
+ """Check if file matches ignore patterns.
119
+
120
+ Args:
121
+ context: Lint context
122
+ config: Magic numbers configuration
123
+
124
+ Returns:
125
+ True if file should be ignored
126
+ """
127
+ if not config.ignore:
128
+ return False
129
+
130
+ if not context.file_path:
131
+ return False
132
+
133
+ file_path = Path(context.file_path)
134
+ for pattern in config.ignore:
135
+ if self._matches_pattern(file_path, pattern):
136
+ return True
137
+ return False
138
+
139
+ def _matches_pattern(self, file_path: Path, pattern: str) -> bool:
140
+ """Check if file path matches a glob pattern.
141
+
142
+ Args:
143
+ file_path: Path to check
144
+ pattern: Glob pattern (e.g., "test/**", "**/test_*.py", "specific/file.py")
145
+
146
+ Returns:
147
+ True if path matches pattern
148
+ """
149
+ # Try glob pattern matching first (handles **, *, etc.)
150
+ if file_path.match(pattern):
151
+ return True
152
+
153
+ # Also check if pattern is a substring (for partial path matching)
154
+ if pattern in str(file_path):
155
+ return True
156
+
157
+ return False
100
158
 
101
159
  def _check_python(self, context: BaseLintContext, config: MagicNumberConfig) -> list[Violation]:
102
160
  """Check Python code for magic number violations.
@@ -108,6 +166,9 @@ class MagicNumberRule(MultiLanguageLintRule): # thailint: ignore[srp]
108
166
  Returns:
109
167
  List of violations found in Python code
110
168
  """
169
+ if self._is_file_ignored(context, config):
170
+ return []
171
+
111
172
  tree = self._parse_python_code(context.file_content)
112
173
  if tree is None:
113
174
  return []
@@ -267,6 +328,9 @@ class MagicNumberRule(MultiLanguageLintRule): # thailint: ignore[srp]
267
328
  Returns:
268
329
  List of violations found in TypeScript/JavaScript code
269
330
  """
331
+ if self._is_file_ignored(context, config):
332
+ return []
333
+
270
334
  analyzer = TypeScriptMagicNumberAnalyzer()
271
335
  root_node = analyzer.parse_typescript(context.file_content or "")
272
336
  if root_node is None:
src/utils/project_root.py CHANGED
@@ -52,14 +52,30 @@ def is_project_root(path: Path) -> bool:
52
52
  return False
53
53
 
54
54
 
55
+ def _try_find_with_criterion(criterion: object, start_path: Path) -> Path | None:
56
+ """Try to find project root with a specific criterion.
57
+
58
+ Args:
59
+ criterion: pyprojroot criterion function (e.g., has_dir(".git"))
60
+ start_path: Path to start searching from
61
+
62
+ Returns:
63
+ Found project root or None if not found
64
+ """
65
+ try:
66
+ return find_root(criterion, start=start_path) # type: ignore[arg-type]
67
+ except (OSError, RuntimeError):
68
+ return None
69
+
70
+
55
71
  def get_project_root(start_path: Path | None = None) -> Path:
56
72
  """Find project root by walking up the directory tree.
57
73
 
58
74
  This is the single source of truth for project root detection.
59
75
  All code that needs to find the project root should use this function.
60
76
 
61
- Uses pyprojroot which searches for standard project markers defined by the
62
- pyprojroot library (git repos, Python projects, etc).
77
+ Uses pyprojroot which searches for standard project markers (.git directory,
78
+ pyproject.toml, .thailint.yaml, etc) starting from start_path and walking upward.
63
79
 
64
80
  Args:
65
81
  start_path: Directory to start searching from. If None, uses current working directory.
@@ -71,14 +87,19 @@ def get_project_root(start_path: Path | None = None) -> Path:
71
87
  >>> root = get_project_root()
72
88
  >>> config_file = root / ".thailint.yaml"
73
89
  """
90
+ from pyprojroot import has_dir, has_file
91
+
74
92
  if start_path is None:
75
93
  start_path = Path.cwd()
76
94
 
77
95
  current = start_path.resolve()
78
96
 
79
- try:
80
- # Use pyprojroot to find the project root
81
- return find_root(current)
82
- except (OSError, RuntimeError):
83
- # No project markers found, return the start path
84
- return current
97
+ # Search for project root markers in priority order
98
+ # Try .git first (most reliable), then .thailint.yaml, then pyproject.toml
99
+ for criterion in [has_dir(".git"), has_file(".thailint.yaml"), has_file("pyproject.toml")]:
100
+ root = _try_find_with_criterion(criterion, current)
101
+ if root is not None:
102
+ return root
103
+
104
+ # No markers found, return start path
105
+ return current
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.3
2
2
  Name: thailint
3
- Version: 0.4.0
3
+ Version: 0.4.2
4
4
  Summary: The AI Linter - Enterprise-grade linting and governance for AI-generated code across multiple languages
5
5
  License: MIT
6
6
  Keywords: linter,ai,code-quality,static-analysis,file-placement,governance,multi-language,cli,docker,python
@@ -35,11 +35,20 @@ Description-Content-Type: text/markdown
35
35
 
36
36
  [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
37
37
  [![Python 3.11+](https://img.shields.io/badge/python-3.11+-blue.svg)](https://www.python.org/downloads/)
38
- [![Tests](https://img.shields.io/badge/tests-253%2F253%20passing-brightgreen.svg)](tests/)
38
+ [![Tests](https://img.shields.io/badge/tests-267%2F267%20passing-brightgreen.svg)](tests/)
39
39
  [![Coverage](https://img.shields.io/badge/coverage-87%25-brightgreen.svg)](htmlcov/)
40
40
 
41
41
  The AI Linter - Enterprise-ready linting and governance for AI-generated code across multiple languages.
42
42
 
43
+ ## Documentation
44
+
45
+ **New to thailint?** Start here:
46
+ - **[Quick Start Guide](docs/quick-start.md)** - Get running in 5 minutes
47
+ - **[Configuration Reference](docs/configuration.md)** - Complete config options for all linters
48
+ - **[Troubleshooting Guide](docs/troubleshooting.md)** - Common issues and solutions
49
+
50
+ **Full Documentation:** Browse the **[docs/](docs/)** folder for comprehensive guides covering installation, all linters, configuration patterns, and integration examples.
51
+
43
52
  ## Overview
44
53
 
45
54
  thailint is a modern, enterprise-ready multi-language linter designed specifically for AI-generated code. It focuses on common mistakes and anti-patterns that AI coding assistants frequently introduce—issues that existing linters don't catch or don't handle consistently across languages.
@@ -55,6 +64,8 @@ We're not trying to replace the wonderful existing linters like Pylint, ESLint,
55
64
 
56
65
  thailint complements your existing linting stack by catching the patterns AI tools repeatedly miss.
57
66
 
67
+ **Complete documentation available in the [docs/](docs/) folder** covering installation, configuration, all linters, and troubleshooting.
68
+
58
69
  ## Features
59
70
 
60
71
  ### Core Capabilities
@@ -151,6 +162,8 @@ thailint dry --config .thailint.yaml src/
151
162
  thailint dry --format json src/
152
163
  ```
153
164
 
165
+ **New to thailint?** See the **[Quick Start Guide](docs/quick-start.md)** for a complete walkthrough including config generation, understanding output, and next steps.
166
+
154
167
  ### Library Mode
155
168
 
156
169
  ```python
@@ -2,12 +2,12 @@ src/__init__.py,sha256=f601zncODr2twrUHqTLS5wyOdZqZi9tMjAe2INhRKqU,2175
2
2
  src/analyzers/__init__.py,sha256=fFloZtjkBGwYbAhKTxS3Qy3yDr2_3i3WSfKTw1mAioo,972
3
3
  src/analyzers/typescript_base.py,sha256=4I7fAcMOAY9vY1AXh52QpohgFmguBECwOkvBRP4zCS4,5054
4
4
  src/api.py,sha256=pJ5l3qxccKBEY-BkANwzTgLAl1ZFq7OP6hx6LSxbhDw,4664
5
- src/cli.py,sha256=zRl6FHyr24puWli77ogFtuPFWKbTNMI93kmu1V4kPEQ,40369
5
+ src/cli.py,sha256=54g24wpF1tzyJW6gflZoIDYfeewjP123no3XliJdVsQ,40544
6
6
  src/config.py,sha256=2ebAjIpAhw4bHbOxViEA5nCjfBlDEIrMR59DBrzcYzM,12460
7
7
  src/core/__init__.py,sha256=5FtsDvhMt4SNRx3pbcGURrxn135XRbeRrjSUxiXwkNc,381
8
8
  src/core/base.py,sha256=Eklcagi2ktfY4Kytl_ObXov2U49N9OGDpw4cu4PUzGY,7824
9
9
  src/core/cli_utils.py,sha256=rYOJz4mnr8RLP-nJdHOy-GJyxGNqkWtK3_rvKriHXj4,6083
10
- src/core/config_parser.py,sha256=0PfLNSC0awV21SrpqdcJ2L8Cn2-B2YcwbA8znaC5YnE,3195
10
+ src/core/config_parser.py,sha256=zAY4bDptNlVID0a4JDXN0YlUKXLM92cFqTAwhp_8uGc,4183
11
11
  src/core/linter_utils.py,sha256=4jmC2YfpPvGhS_XHlHXa5SBIJh9CQlNj5zuW_GpdPKc,5273
12
12
  src/core/registry.py,sha256=IMkic1ukmo8HCX1TY5YoKjwKT_IT-ZmVx6sdntC5X2M,3289
13
13
  src/core/rule_discovery.py,sha256=smxJ9PEyMqEAIicsWaHOaSHD1PHUAOeFZT_a3DNRwgE,4163
@@ -40,16 +40,16 @@ src/linters/dry/violation_generator.py,sha256=cc6aKvTxtHSZm0F7Y-gL1bmD3JUphRmAvc
40
40
  src/linters/file_placement/__init__.py,sha256=vJ43GZujcbAk-K3DwfsQZ0J3yP_5G35CKssatLyntXk,862
41
41
  src/linters/file_placement/config_loader.py,sha256=Of5sTG2S-04efn3KOlXrSxpMcC1ipBpSvCjtJOMmWno,2640
42
42
  src/linters/file_placement/directory_matcher.py,sha256=YaBeLGiT4bgqN_v4FmEmSASOBxkMC1lyEYpL17wLIDY,2607
43
- src/linters/file_placement/linter.py,sha256=r3wnfGkss6HsPCqxQjBZ1dvyZdNdxppbSu9VRh0PMzw,13802
43
+ src/linters/file_placement/linter.py,sha256=HtQf08UmKCe5YAHoPvVgWmxdUFu6ZeP2qicg-q7BgzM,14438
44
44
  src/linters/file_placement/path_resolver.py,sha256=S6g7xOYsoSc0O_RDJh8j4Z2klcwzp16rSUfEAErGOTI,1972
45
45
  src/linters/file_placement/pattern_matcher.py,sha256=3HZWYgQKXz_y13z3lO1YHn51khCaiGOrneGxKXGWGw0,1898
46
46
  src/linters/file_placement/pattern_validator.py,sha256=eMt5GB5lgJMhhQACOlfDXQFfSfNrOY-wJN1JanGka6Q,3717
47
47
  src/linters/file_placement/rule_checker.py,sha256=JONXcaYxZ8CM_7Zg6Th01p5cve1rJ8YkReAUJ44nfUg,7795
48
48
  src/linters/file_placement/violation_factory.py,sha256=NkQmBcgpa3g3W2ZdFZNQ5djLVP4x9OKs65d7F1rCKvM,6040
49
49
  src/linters/magic_numbers/__init__.py,sha256=17dkCUf0uiYLvpOZF01VDojj92NzxXZMtRhrSBUzsdc,1689
50
- src/linters/magic_numbers/config.py,sha256=yL4E440FaT1RDsiQgIRcEs-MozMylOubHrQ3rT2usOM,3047
50
+ src/linters/magic_numbers/config.py,sha256=3zV6ZNezouBWUYy4kMw5PUlPNvIWXVwOxTz1moZfRoI,3270
51
51
  src/linters/magic_numbers/context_analyzer.py,sha256=cGXozlKll10Zao56c2E8ThIyH2mSQaPaUau_g7ngRLw,8446
52
- src/linters/magic_numbers/linter.py,sha256=f240nyxsSN9-ttF6c7gA0LNwo6mBd85bWVO2I45G3DI,16354
52
+ src/linters/magic_numbers/linter.py,sha256=maj4NgrDapv0RurKvaVgOI1BUujixZv4E7UeYy4eGT4,18394
53
53
  src/linters/magic_numbers/python_analyzer.py,sha256=0u1cFaaFCqOW5yeW-YbmPoZuVIeN_KtmkFyyxup6aR0,2803
54
54
  src/linters/magic_numbers/typescript_analyzer.py,sha256=DCYRdxjgMd6PkhJWKnc1W-S1T0sa-F9AHCLV2JwcR8g,7468
55
55
  src/linters/magic_numbers/violation_builder.py,sha256=SqIQv3N9lpP2GRC1TC5InrvaEdrAq24V7Ec2Xj5olb0,3308
@@ -75,9 +75,9 @@ src/orchestrator/core.py,sha256=zb4H4HtDNLmnsRCUXI3oNtfM3T-nTPW9Q2pAbI61VEs,8374
75
75
  src/orchestrator/language_detector.py,sha256=rHyVMApit80NTTNyDH1ObD1usKD8LjGmH3DwqNAWYGc,2736
76
76
  src/templates/thailint_config_template.yaml,sha256=u8WFv2coE4uqfgf_slw7xjo4kGYIowDm1RIgxsKQzrE,4275
77
77
  src/utils/__init__.py,sha256=NiBtKeQ09Y3kuUzeN4O1JNfUIYPQDS2AP1l5ODq-Dec,125
78
- src/utils/project_root.py,sha256=ldv2-XeMT0IElpSgrHdTaP2CUfwmdZix8vQ2qXO1O5s,2735
79
- thailint-0.4.0.dist-info/LICENSE,sha256=kxh1J0Sb62XvhNJ6MZsVNe8PqNVJ7LHRn_EWa-T3djw,1070
80
- thailint-0.4.0.dist-info/METADATA,sha256=c_ZXWle6WqMTEnSzLWodRM38CcS1O4ZnzRbJD0u-IeQ,33249
81
- thailint-0.4.0.dist-info/WHEEL,sha256=b4K_helf-jlQoXBBETfwnf4B04YC67LOev0jo4fX5m8,88
82
- thailint-0.4.0.dist-info/entry_points.txt,sha256=l7DQJgU18sVLDpSaXOXY3lLhnQHQIRrSJZTQjG1cEAk,62
83
- thailint-0.4.0.dist-info/RECORD,,
78
+ src/utils/project_root.py,sha256=kGxQU1nHItffiR6iIHT_xCy9RQJtl1Y6dgSUnJHRJQ8,3498
79
+ thailint-0.4.2.dist-info/LICENSE,sha256=kxh1J0Sb62XvhNJ6MZsVNe8PqNVJ7LHRn_EWa-T3djw,1070
80
+ thailint-0.4.2.dist-info/METADATA,sha256=HT1yu78t_cI-2Mv3kw0Kci1o6iBAEdz3jpwJwS6dOkg,34038
81
+ thailint-0.4.2.dist-info/WHEEL,sha256=b4K_helf-jlQoXBBETfwnf4B04YC67LOev0jo4fX5m8,88
82
+ thailint-0.4.2.dist-info/entry_points.txt,sha256=l7DQJgU18sVLDpSaXOXY3lLhnQHQIRrSJZTQjG1cEAk,62
83
+ thailint-0.4.2.dist-info/RECORD,,