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.
Files changed (83) hide show
  1. {thailint-0.4.1 → thailint-0.4.2}/PKG-INFO +2 -2
  2. {thailint-0.4.1 → thailint-0.4.2}/README.md +1 -1
  3. {thailint-0.4.1 → thailint-0.4.2}/pyproject.toml +1 -1
  4. {thailint-0.4.1 → thailint-0.4.2}/src/cli.py +6 -3
  5. {thailint-0.4.1 → thailint-0.4.2}/src/core/config_parser.py +31 -4
  6. {thailint-0.4.1 → thailint-0.4.2}/src/linters/file_placement/linter.py +15 -4
  7. {thailint-0.4.1 → thailint-0.4.2}/src/linters/magic_numbers/linter.py +16 -1
  8. {thailint-0.4.1 → thailint-0.4.2}/src/utils/project_root.py +29 -8
  9. {thailint-0.4.1 → thailint-0.4.2}/CHANGELOG.md +0 -0
  10. {thailint-0.4.1 → thailint-0.4.2}/LICENSE +0 -0
  11. {thailint-0.4.1 → thailint-0.4.2}/src/__init__.py +0 -0
  12. {thailint-0.4.1 → thailint-0.4.2}/src/analyzers/__init__.py +0 -0
  13. {thailint-0.4.1 → thailint-0.4.2}/src/analyzers/typescript_base.py +0 -0
  14. {thailint-0.4.1 → thailint-0.4.2}/src/api.py +0 -0
  15. {thailint-0.4.1 → thailint-0.4.2}/src/config.py +0 -0
  16. {thailint-0.4.1 → thailint-0.4.2}/src/core/__init__.py +0 -0
  17. {thailint-0.4.1 → thailint-0.4.2}/src/core/base.py +0 -0
  18. {thailint-0.4.1 → thailint-0.4.2}/src/core/cli_utils.py +0 -0
  19. {thailint-0.4.1 → thailint-0.4.2}/src/core/linter_utils.py +0 -0
  20. {thailint-0.4.1 → thailint-0.4.2}/src/core/registry.py +0 -0
  21. {thailint-0.4.1 → thailint-0.4.2}/src/core/rule_discovery.py +0 -0
  22. {thailint-0.4.1 → thailint-0.4.2}/src/core/types.py +0 -0
  23. {thailint-0.4.1 → thailint-0.4.2}/src/core/violation_builder.py +0 -0
  24. {thailint-0.4.1 → thailint-0.4.2}/src/linter_config/__init__.py +0 -0
  25. {thailint-0.4.1 → thailint-0.4.2}/src/linter_config/ignore.py +0 -0
  26. {thailint-0.4.1 → thailint-0.4.2}/src/linter_config/loader.py +0 -0
  27. {thailint-0.4.1 → thailint-0.4.2}/src/linters/__init__.py +0 -0
  28. {thailint-0.4.1 → thailint-0.4.2}/src/linters/dry/__init__.py +0 -0
  29. {thailint-0.4.1 → thailint-0.4.2}/src/linters/dry/base_token_analyzer.py +0 -0
  30. {thailint-0.4.1 → thailint-0.4.2}/src/linters/dry/block_filter.py +0 -0
  31. {thailint-0.4.1 → thailint-0.4.2}/src/linters/dry/block_grouper.py +0 -0
  32. {thailint-0.4.1 → thailint-0.4.2}/src/linters/dry/cache.py +0 -0
  33. {thailint-0.4.1 → thailint-0.4.2}/src/linters/dry/cache_query.py +0 -0
  34. {thailint-0.4.1 → thailint-0.4.2}/src/linters/dry/config.py +0 -0
  35. {thailint-0.4.1 → thailint-0.4.2}/src/linters/dry/config_loader.py +0 -0
  36. {thailint-0.4.1 → thailint-0.4.2}/src/linters/dry/deduplicator.py +0 -0
  37. {thailint-0.4.1 → thailint-0.4.2}/src/linters/dry/duplicate_storage.py +0 -0
  38. {thailint-0.4.1 → thailint-0.4.2}/src/linters/dry/file_analyzer.py +0 -0
  39. {thailint-0.4.1 → thailint-0.4.2}/src/linters/dry/inline_ignore.py +0 -0
  40. {thailint-0.4.1 → thailint-0.4.2}/src/linters/dry/linter.py +0 -0
  41. {thailint-0.4.1 → thailint-0.4.2}/src/linters/dry/python_analyzer.py +0 -0
  42. {thailint-0.4.1 → thailint-0.4.2}/src/linters/dry/storage_initializer.py +0 -0
  43. {thailint-0.4.1 → thailint-0.4.2}/src/linters/dry/token_hasher.py +0 -0
  44. {thailint-0.4.1 → thailint-0.4.2}/src/linters/dry/typescript_analyzer.py +0 -0
  45. {thailint-0.4.1 → thailint-0.4.2}/src/linters/dry/violation_builder.py +0 -0
  46. {thailint-0.4.1 → thailint-0.4.2}/src/linters/dry/violation_filter.py +0 -0
  47. {thailint-0.4.1 → thailint-0.4.2}/src/linters/dry/violation_generator.py +0 -0
  48. {thailint-0.4.1 → thailint-0.4.2}/src/linters/file_placement/__init__.py +0 -0
  49. {thailint-0.4.1 → thailint-0.4.2}/src/linters/file_placement/config_loader.py +0 -0
  50. {thailint-0.4.1 → thailint-0.4.2}/src/linters/file_placement/directory_matcher.py +0 -0
  51. {thailint-0.4.1 → thailint-0.4.2}/src/linters/file_placement/path_resolver.py +0 -0
  52. {thailint-0.4.1 → thailint-0.4.2}/src/linters/file_placement/pattern_matcher.py +0 -0
  53. {thailint-0.4.1 → thailint-0.4.2}/src/linters/file_placement/pattern_validator.py +0 -0
  54. {thailint-0.4.1 → thailint-0.4.2}/src/linters/file_placement/rule_checker.py +0 -0
  55. {thailint-0.4.1 → thailint-0.4.2}/src/linters/file_placement/violation_factory.py +0 -0
  56. {thailint-0.4.1 → thailint-0.4.2}/src/linters/magic_numbers/__init__.py +0 -0
  57. {thailint-0.4.1 → thailint-0.4.2}/src/linters/magic_numbers/config.py +0 -0
  58. {thailint-0.4.1 → thailint-0.4.2}/src/linters/magic_numbers/context_analyzer.py +0 -0
  59. {thailint-0.4.1 → thailint-0.4.2}/src/linters/magic_numbers/python_analyzer.py +0 -0
  60. {thailint-0.4.1 → thailint-0.4.2}/src/linters/magic_numbers/typescript_analyzer.py +0 -0
  61. {thailint-0.4.1 → thailint-0.4.2}/src/linters/magic_numbers/violation_builder.py +0 -0
  62. {thailint-0.4.1 → thailint-0.4.2}/src/linters/nesting/__init__.py +0 -0
  63. {thailint-0.4.1 → thailint-0.4.2}/src/linters/nesting/config.py +0 -0
  64. {thailint-0.4.1 → thailint-0.4.2}/src/linters/nesting/linter.py +0 -0
  65. {thailint-0.4.1 → thailint-0.4.2}/src/linters/nesting/python_analyzer.py +0 -0
  66. {thailint-0.4.1 → thailint-0.4.2}/src/linters/nesting/typescript_analyzer.py +0 -0
  67. {thailint-0.4.1 → thailint-0.4.2}/src/linters/nesting/typescript_function_extractor.py +0 -0
  68. {thailint-0.4.1 → thailint-0.4.2}/src/linters/nesting/violation_builder.py +0 -0
  69. {thailint-0.4.1 → thailint-0.4.2}/src/linters/srp/__init__.py +0 -0
  70. {thailint-0.4.1 → thailint-0.4.2}/src/linters/srp/class_analyzer.py +0 -0
  71. {thailint-0.4.1 → thailint-0.4.2}/src/linters/srp/config.py +0 -0
  72. {thailint-0.4.1 → thailint-0.4.2}/src/linters/srp/heuristics.py +0 -0
  73. {thailint-0.4.1 → thailint-0.4.2}/src/linters/srp/linter.py +0 -0
  74. {thailint-0.4.1 → thailint-0.4.2}/src/linters/srp/metrics_evaluator.py +0 -0
  75. {thailint-0.4.1 → thailint-0.4.2}/src/linters/srp/python_analyzer.py +0 -0
  76. {thailint-0.4.1 → thailint-0.4.2}/src/linters/srp/typescript_analyzer.py +0 -0
  77. {thailint-0.4.1 → thailint-0.4.2}/src/linters/srp/typescript_metrics_calculator.py +0 -0
  78. {thailint-0.4.1 → thailint-0.4.2}/src/linters/srp/violation_builder.py +0 -0
  79. {thailint-0.4.1 → thailint-0.4.2}/src/orchestrator/__init__.py +0 -0
  80. {thailint-0.4.1 → thailint-0.4.2}/src/orchestrator/core.py +0 -0
  81. {thailint-0.4.1 → thailint-0.4.2}/src/orchestrator/language_detector.py +0 -0
  82. {thailint-0.4.1 → thailint-0.4.2}/src/templates/thailint_config_template.yaml +0 -0
  83. {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.1
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
  [![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-263%2F263%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.
@@ -2,7 +2,7 @@
2
2
 
3
3
  [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
4
4
  [![Python 3.11+](https://img.shields.io/badge/python-3.11+-blue.svg)](https://www.python.org/downloads/)
5
- [![Tests](https://img.shields.io/badge/tests-263%2F263%20passing-brightgreen.svg)](tests/)
5
+ [![Tests](https://img.shields.io/badge/tests-267%2F267%20passing-brightgreen.svg)](tests/)
6
6
  [![Coverage](https://img.shields.io/badge/coverage-87%25-brightgreen.svg)](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.1"
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
- 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:
@@ -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
- 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
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 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
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