thailint 0.4.1__tar.gz → 0.4.2__tar.gz
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.
- {thailint-0.4.1 → thailint-0.4.2}/PKG-INFO +2 -2
- {thailint-0.4.1 → thailint-0.4.2}/README.md +1 -1
- {thailint-0.4.1 → thailint-0.4.2}/pyproject.toml +1 -1
- {thailint-0.4.1 → thailint-0.4.2}/src/cli.py +6 -3
- {thailint-0.4.1 → thailint-0.4.2}/src/core/config_parser.py +31 -4
- {thailint-0.4.1 → thailint-0.4.2}/src/linters/file_placement/linter.py +15 -4
- {thailint-0.4.1 → thailint-0.4.2}/src/linters/magic_numbers/linter.py +16 -1
- {thailint-0.4.1 → thailint-0.4.2}/src/utils/project_root.py +29 -8
- {thailint-0.4.1 → thailint-0.4.2}/CHANGELOG.md +0 -0
- {thailint-0.4.1 → thailint-0.4.2}/LICENSE +0 -0
- {thailint-0.4.1 → thailint-0.4.2}/src/__init__.py +0 -0
- {thailint-0.4.1 → thailint-0.4.2}/src/analyzers/__init__.py +0 -0
- {thailint-0.4.1 → thailint-0.4.2}/src/analyzers/typescript_base.py +0 -0
- {thailint-0.4.1 → thailint-0.4.2}/src/api.py +0 -0
- {thailint-0.4.1 → thailint-0.4.2}/src/config.py +0 -0
- {thailint-0.4.1 → thailint-0.4.2}/src/core/__init__.py +0 -0
- {thailint-0.4.1 → thailint-0.4.2}/src/core/base.py +0 -0
- {thailint-0.4.1 → thailint-0.4.2}/src/core/cli_utils.py +0 -0
- {thailint-0.4.1 → thailint-0.4.2}/src/core/linter_utils.py +0 -0
- {thailint-0.4.1 → thailint-0.4.2}/src/core/registry.py +0 -0
- {thailint-0.4.1 → thailint-0.4.2}/src/core/rule_discovery.py +0 -0
- {thailint-0.4.1 → thailint-0.4.2}/src/core/types.py +0 -0
- {thailint-0.4.1 → thailint-0.4.2}/src/core/violation_builder.py +0 -0
- {thailint-0.4.1 → thailint-0.4.2}/src/linter_config/__init__.py +0 -0
- {thailint-0.4.1 → thailint-0.4.2}/src/linter_config/ignore.py +0 -0
- {thailint-0.4.1 → thailint-0.4.2}/src/linter_config/loader.py +0 -0
- {thailint-0.4.1 → thailint-0.4.2}/src/linters/__init__.py +0 -0
- {thailint-0.4.1 → thailint-0.4.2}/src/linters/dry/__init__.py +0 -0
- {thailint-0.4.1 → thailint-0.4.2}/src/linters/dry/base_token_analyzer.py +0 -0
- {thailint-0.4.1 → thailint-0.4.2}/src/linters/dry/block_filter.py +0 -0
- {thailint-0.4.1 → thailint-0.4.2}/src/linters/dry/block_grouper.py +0 -0
- {thailint-0.4.1 → thailint-0.4.2}/src/linters/dry/cache.py +0 -0
- {thailint-0.4.1 → thailint-0.4.2}/src/linters/dry/cache_query.py +0 -0
- {thailint-0.4.1 → thailint-0.4.2}/src/linters/dry/config.py +0 -0
- {thailint-0.4.1 → thailint-0.4.2}/src/linters/dry/config_loader.py +0 -0
- {thailint-0.4.1 → thailint-0.4.2}/src/linters/dry/deduplicator.py +0 -0
- {thailint-0.4.1 → thailint-0.4.2}/src/linters/dry/duplicate_storage.py +0 -0
- {thailint-0.4.1 → thailint-0.4.2}/src/linters/dry/file_analyzer.py +0 -0
- {thailint-0.4.1 → thailint-0.4.2}/src/linters/dry/inline_ignore.py +0 -0
- {thailint-0.4.1 → thailint-0.4.2}/src/linters/dry/linter.py +0 -0
- {thailint-0.4.1 → thailint-0.4.2}/src/linters/dry/python_analyzer.py +0 -0
- {thailint-0.4.1 → thailint-0.4.2}/src/linters/dry/storage_initializer.py +0 -0
- {thailint-0.4.1 → thailint-0.4.2}/src/linters/dry/token_hasher.py +0 -0
- {thailint-0.4.1 → thailint-0.4.2}/src/linters/dry/typescript_analyzer.py +0 -0
- {thailint-0.4.1 → thailint-0.4.2}/src/linters/dry/violation_builder.py +0 -0
- {thailint-0.4.1 → thailint-0.4.2}/src/linters/dry/violation_filter.py +0 -0
- {thailint-0.4.1 → thailint-0.4.2}/src/linters/dry/violation_generator.py +0 -0
- {thailint-0.4.1 → thailint-0.4.2}/src/linters/file_placement/__init__.py +0 -0
- {thailint-0.4.1 → thailint-0.4.2}/src/linters/file_placement/config_loader.py +0 -0
- {thailint-0.4.1 → thailint-0.4.2}/src/linters/file_placement/directory_matcher.py +0 -0
- {thailint-0.4.1 → thailint-0.4.2}/src/linters/file_placement/path_resolver.py +0 -0
- {thailint-0.4.1 → thailint-0.4.2}/src/linters/file_placement/pattern_matcher.py +0 -0
- {thailint-0.4.1 → thailint-0.4.2}/src/linters/file_placement/pattern_validator.py +0 -0
- {thailint-0.4.1 → thailint-0.4.2}/src/linters/file_placement/rule_checker.py +0 -0
- {thailint-0.4.1 → thailint-0.4.2}/src/linters/file_placement/violation_factory.py +0 -0
- {thailint-0.4.1 → thailint-0.4.2}/src/linters/magic_numbers/__init__.py +0 -0
- {thailint-0.4.1 → thailint-0.4.2}/src/linters/magic_numbers/config.py +0 -0
- {thailint-0.4.1 → thailint-0.4.2}/src/linters/magic_numbers/context_analyzer.py +0 -0
- {thailint-0.4.1 → thailint-0.4.2}/src/linters/magic_numbers/python_analyzer.py +0 -0
- {thailint-0.4.1 → thailint-0.4.2}/src/linters/magic_numbers/typescript_analyzer.py +0 -0
- {thailint-0.4.1 → thailint-0.4.2}/src/linters/magic_numbers/violation_builder.py +0 -0
- {thailint-0.4.1 → thailint-0.4.2}/src/linters/nesting/__init__.py +0 -0
- {thailint-0.4.1 → thailint-0.4.2}/src/linters/nesting/config.py +0 -0
- {thailint-0.4.1 → thailint-0.4.2}/src/linters/nesting/linter.py +0 -0
- {thailint-0.4.1 → thailint-0.4.2}/src/linters/nesting/python_analyzer.py +0 -0
- {thailint-0.4.1 → thailint-0.4.2}/src/linters/nesting/typescript_analyzer.py +0 -0
- {thailint-0.4.1 → thailint-0.4.2}/src/linters/nesting/typescript_function_extractor.py +0 -0
- {thailint-0.4.1 → thailint-0.4.2}/src/linters/nesting/violation_builder.py +0 -0
- {thailint-0.4.1 → thailint-0.4.2}/src/linters/srp/__init__.py +0 -0
- {thailint-0.4.1 → thailint-0.4.2}/src/linters/srp/class_analyzer.py +0 -0
- {thailint-0.4.1 → thailint-0.4.2}/src/linters/srp/config.py +0 -0
- {thailint-0.4.1 → thailint-0.4.2}/src/linters/srp/heuristics.py +0 -0
- {thailint-0.4.1 → thailint-0.4.2}/src/linters/srp/linter.py +0 -0
- {thailint-0.4.1 → thailint-0.4.2}/src/linters/srp/metrics_evaluator.py +0 -0
- {thailint-0.4.1 → thailint-0.4.2}/src/linters/srp/python_analyzer.py +0 -0
- {thailint-0.4.1 → thailint-0.4.2}/src/linters/srp/typescript_analyzer.py +0 -0
- {thailint-0.4.1 → thailint-0.4.2}/src/linters/srp/typescript_metrics_calculator.py +0 -0
- {thailint-0.4.1 → thailint-0.4.2}/src/linters/srp/violation_builder.py +0 -0
- {thailint-0.4.1 → thailint-0.4.2}/src/orchestrator/__init__.py +0 -0
- {thailint-0.4.1 → thailint-0.4.2}/src/orchestrator/core.py +0 -0
- {thailint-0.4.1 → thailint-0.4.2}/src/orchestrator/language_detector.py +0 -0
- {thailint-0.4.1 → thailint-0.4.2}/src/templates/thailint_config_template.yaml +0 -0
- {thailint-0.4.1 → thailint-0.4.2}/src/utils/__init__.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.3
|
|
2
2
|
Name: thailint
|
|
3
|
-
Version: 0.4.
|
|
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,7 +35,7 @@ Description-Content-Type: text/markdown
|
|
|
35
35
|
|
|
36
36
|
[](https://opensource.org/licenses/MIT)
|
|
37
37
|
[](https://www.python.org/downloads/)
|
|
38
|
-
[](tests/)
|
|
39
39
|
[](htmlcov/)
|
|
40
40
|
|
|
41
41
|
The AI Linter - Enterprise-ready linting and governance for AI-generated code across multiple languages.
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
[](https://opensource.org/licenses/MIT)
|
|
4
4
|
[](https://www.python.org/downloads/)
|
|
5
|
-
[](tests/)
|
|
6
6
|
[](htmlcov/)
|
|
7
7
|
|
|
8
8
|
The AI Linter - Enterprise-ready linting and governance for AI-generated code across multiple languages.
|
|
@@ -17,7 +17,7 @@ build-backend = "poetry.core.masonry.api"
|
|
|
17
17
|
|
|
18
18
|
[tool.poetry]
|
|
19
19
|
name = "thailint"
|
|
20
|
-
version = "0.4.
|
|
20
|
+
version = "0.4.2"
|
|
21
21
|
description = "The AI Linter - Enterprise-grade linting and governance for AI-generated code across multiple languages"
|
|
22
22
|
authors = ["Steve Jackson"]
|
|
23
23
|
license = "MIT"
|
|
@@ -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
|
|
|
@@ -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
|
-
|
|
99
|
-
|
|
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
|
-
|
|
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:
|
|
@@ -97,7 +97,22 @@ class MagicNumberRule(MultiLanguageLintRule): # thailint: ignore[srp]
|
|
|
97
97
|
"""Try to load production configuration."""
|
|
98
98
|
if not hasattr(context, "metadata") or not isinstance(context.metadata, dict):
|
|
99
99
|
return None
|
|
100
|
-
|
|
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
|
|
101
116
|
|
|
102
117
|
def _is_file_ignored(self, context: BaseLintContext, config: MagicNumberConfig) -> bool:
|
|
103
118
|
"""Check if file matches ignore patterns.
|
|
@@ -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
|
|
62
|
-
|
|
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
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
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
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|