thailint 0.7.0__tar.gz → 0.8.0__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 (108) hide show
  1. {thailint-0.7.0 → thailint-0.8.0}/PKG-INFO +9 -3
  2. {thailint-0.7.0 → thailint-0.8.0}/README.md +7 -1
  3. {thailint-0.7.0 → thailint-0.8.0}/pyproject.toml +2 -2
  4. {thailint-0.7.0 → thailint-0.8.0}/src/cli.py +118 -1
  5. {thailint-0.7.0 → thailint-0.8.0}/src/linter_config/loader.py +5 -4
  6. {thailint-0.7.0 → thailint-0.8.0}/src/linters/dry/block_filter.py +11 -8
  7. {thailint-0.7.0 → thailint-0.8.0}/src/linters/dry/cache.py +3 -2
  8. {thailint-0.7.0 → thailint-0.8.0}/src/linters/dry/duplicate_storage.py +5 -4
  9. {thailint-0.7.0 → thailint-0.8.0}/src/linters/dry/violation_generator.py +1 -1
  10. thailint-0.8.0/src/linters/method_property/__init__.py +49 -0
  11. thailint-0.8.0/src/linters/method_property/config.py +135 -0
  12. thailint-0.8.0/src/linters/method_property/linter.py +419 -0
  13. thailint-0.8.0/src/linters/method_property/python_analyzer.py +472 -0
  14. thailint-0.8.0/src/linters/method_property/violation_builder.py +116 -0
  15. {thailint-0.7.0 → thailint-0.8.0}/CHANGELOG.md +0 -0
  16. {thailint-0.7.0 → thailint-0.8.0}/LICENSE +0 -0
  17. {thailint-0.7.0 → thailint-0.8.0}/src/__init__.py +0 -0
  18. {thailint-0.7.0 → thailint-0.8.0}/src/analyzers/__init__.py +0 -0
  19. {thailint-0.7.0 → thailint-0.8.0}/src/analyzers/typescript_base.py +0 -0
  20. {thailint-0.7.0 → thailint-0.8.0}/src/api.py +0 -0
  21. {thailint-0.7.0 → thailint-0.8.0}/src/config.py +0 -0
  22. {thailint-0.7.0 → thailint-0.8.0}/src/core/__init__.py +0 -0
  23. {thailint-0.7.0 → thailint-0.8.0}/src/core/base.py +0 -0
  24. {thailint-0.7.0 → thailint-0.8.0}/src/core/cli_utils.py +0 -0
  25. {thailint-0.7.0 → thailint-0.8.0}/src/core/config_parser.py +0 -0
  26. {thailint-0.7.0 → thailint-0.8.0}/src/core/linter_utils.py +0 -0
  27. {thailint-0.7.0 → thailint-0.8.0}/src/core/registry.py +0 -0
  28. {thailint-0.7.0 → thailint-0.8.0}/src/core/rule_discovery.py +0 -0
  29. {thailint-0.7.0 → thailint-0.8.0}/src/core/types.py +0 -0
  30. {thailint-0.7.0 → thailint-0.8.0}/src/core/violation_builder.py +0 -0
  31. {thailint-0.7.0 → thailint-0.8.0}/src/formatters/__init__.py +0 -0
  32. {thailint-0.7.0 → thailint-0.8.0}/src/formatters/sarif.py +0 -0
  33. {thailint-0.7.0 → thailint-0.8.0}/src/linter_config/__init__.py +0 -0
  34. {thailint-0.7.0 → thailint-0.8.0}/src/linter_config/ignore.py +0 -0
  35. {thailint-0.7.0 → thailint-0.8.0}/src/linters/__init__.py +0 -0
  36. {thailint-0.7.0 → thailint-0.8.0}/src/linters/dry/__init__.py +0 -0
  37. {thailint-0.7.0 → thailint-0.8.0}/src/linters/dry/base_token_analyzer.py +0 -0
  38. {thailint-0.7.0 → thailint-0.8.0}/src/linters/dry/block_grouper.py +0 -0
  39. {thailint-0.7.0 → thailint-0.8.0}/src/linters/dry/cache_query.py +0 -0
  40. {thailint-0.7.0 → thailint-0.8.0}/src/linters/dry/config.py +0 -0
  41. {thailint-0.7.0 → thailint-0.8.0}/src/linters/dry/config_loader.py +0 -0
  42. {thailint-0.7.0 → thailint-0.8.0}/src/linters/dry/deduplicator.py +0 -0
  43. {thailint-0.7.0 → thailint-0.8.0}/src/linters/dry/file_analyzer.py +0 -0
  44. {thailint-0.7.0 → thailint-0.8.0}/src/linters/dry/inline_ignore.py +0 -0
  45. {thailint-0.7.0 → thailint-0.8.0}/src/linters/dry/linter.py +0 -0
  46. {thailint-0.7.0 → thailint-0.8.0}/src/linters/dry/python_analyzer.py +0 -0
  47. {thailint-0.7.0 → thailint-0.8.0}/src/linters/dry/storage_initializer.py +0 -0
  48. {thailint-0.7.0 → thailint-0.8.0}/src/linters/dry/token_hasher.py +0 -0
  49. {thailint-0.7.0 → thailint-0.8.0}/src/linters/dry/typescript_analyzer.py +0 -0
  50. {thailint-0.7.0 → thailint-0.8.0}/src/linters/dry/violation_builder.py +0 -0
  51. {thailint-0.7.0 → thailint-0.8.0}/src/linters/dry/violation_filter.py +0 -0
  52. {thailint-0.7.0 → thailint-0.8.0}/src/linters/file_header/__init__.py +0 -0
  53. {thailint-0.7.0 → thailint-0.8.0}/src/linters/file_header/atemporal_detector.py +0 -0
  54. {thailint-0.7.0 → thailint-0.8.0}/src/linters/file_header/base_parser.py +0 -0
  55. {thailint-0.7.0 → thailint-0.8.0}/src/linters/file_header/bash_parser.py +0 -0
  56. {thailint-0.7.0 → thailint-0.8.0}/src/linters/file_header/config.py +0 -0
  57. {thailint-0.7.0 → thailint-0.8.0}/src/linters/file_header/css_parser.py +0 -0
  58. {thailint-0.7.0 → thailint-0.8.0}/src/linters/file_header/field_validator.py +0 -0
  59. {thailint-0.7.0 → thailint-0.8.0}/src/linters/file_header/linter.py +0 -0
  60. {thailint-0.7.0 → thailint-0.8.0}/src/linters/file_header/markdown_parser.py +0 -0
  61. {thailint-0.7.0 → thailint-0.8.0}/src/linters/file_header/python_parser.py +0 -0
  62. {thailint-0.7.0 → thailint-0.8.0}/src/linters/file_header/typescript_parser.py +0 -0
  63. {thailint-0.7.0 → thailint-0.8.0}/src/linters/file_header/violation_builder.py +0 -0
  64. {thailint-0.7.0 → thailint-0.8.0}/src/linters/file_placement/__init__.py +0 -0
  65. {thailint-0.7.0 → thailint-0.8.0}/src/linters/file_placement/config_loader.py +0 -0
  66. {thailint-0.7.0 → thailint-0.8.0}/src/linters/file_placement/directory_matcher.py +0 -0
  67. {thailint-0.7.0 → thailint-0.8.0}/src/linters/file_placement/linter.py +0 -0
  68. {thailint-0.7.0 → thailint-0.8.0}/src/linters/file_placement/path_resolver.py +0 -0
  69. {thailint-0.7.0 → thailint-0.8.0}/src/linters/file_placement/pattern_matcher.py +0 -0
  70. {thailint-0.7.0 → thailint-0.8.0}/src/linters/file_placement/pattern_validator.py +0 -0
  71. {thailint-0.7.0 → thailint-0.8.0}/src/linters/file_placement/rule_checker.py +0 -0
  72. {thailint-0.7.0 → thailint-0.8.0}/src/linters/file_placement/violation_factory.py +0 -0
  73. {thailint-0.7.0 → thailint-0.8.0}/src/linters/magic_numbers/__init__.py +0 -0
  74. {thailint-0.7.0 → thailint-0.8.0}/src/linters/magic_numbers/config.py +0 -0
  75. {thailint-0.7.0 → thailint-0.8.0}/src/linters/magic_numbers/context_analyzer.py +0 -0
  76. {thailint-0.7.0 → thailint-0.8.0}/src/linters/magic_numbers/linter.py +0 -0
  77. {thailint-0.7.0 → thailint-0.8.0}/src/linters/magic_numbers/python_analyzer.py +0 -0
  78. {thailint-0.7.0 → thailint-0.8.0}/src/linters/magic_numbers/typescript_analyzer.py +0 -0
  79. {thailint-0.7.0 → thailint-0.8.0}/src/linters/magic_numbers/violation_builder.py +0 -0
  80. {thailint-0.7.0 → thailint-0.8.0}/src/linters/nesting/__init__.py +0 -0
  81. {thailint-0.7.0 → thailint-0.8.0}/src/linters/nesting/config.py +0 -0
  82. {thailint-0.7.0 → thailint-0.8.0}/src/linters/nesting/linter.py +0 -0
  83. {thailint-0.7.0 → thailint-0.8.0}/src/linters/nesting/python_analyzer.py +0 -0
  84. {thailint-0.7.0 → thailint-0.8.0}/src/linters/nesting/typescript_analyzer.py +0 -0
  85. {thailint-0.7.0 → thailint-0.8.0}/src/linters/nesting/typescript_function_extractor.py +0 -0
  86. {thailint-0.7.0 → thailint-0.8.0}/src/linters/nesting/violation_builder.py +0 -0
  87. {thailint-0.7.0 → thailint-0.8.0}/src/linters/print_statements/__init__.py +0 -0
  88. {thailint-0.7.0 → thailint-0.8.0}/src/linters/print_statements/config.py +0 -0
  89. {thailint-0.7.0 → thailint-0.8.0}/src/linters/print_statements/linter.py +0 -0
  90. {thailint-0.7.0 → thailint-0.8.0}/src/linters/print_statements/python_analyzer.py +0 -0
  91. {thailint-0.7.0 → thailint-0.8.0}/src/linters/print_statements/typescript_analyzer.py +0 -0
  92. {thailint-0.7.0 → thailint-0.8.0}/src/linters/print_statements/violation_builder.py +0 -0
  93. {thailint-0.7.0 → thailint-0.8.0}/src/linters/srp/__init__.py +0 -0
  94. {thailint-0.7.0 → thailint-0.8.0}/src/linters/srp/class_analyzer.py +0 -0
  95. {thailint-0.7.0 → thailint-0.8.0}/src/linters/srp/config.py +0 -0
  96. {thailint-0.7.0 → thailint-0.8.0}/src/linters/srp/heuristics.py +0 -0
  97. {thailint-0.7.0 → thailint-0.8.0}/src/linters/srp/linter.py +0 -0
  98. {thailint-0.7.0 → thailint-0.8.0}/src/linters/srp/metrics_evaluator.py +0 -0
  99. {thailint-0.7.0 → thailint-0.8.0}/src/linters/srp/python_analyzer.py +0 -0
  100. {thailint-0.7.0 → thailint-0.8.0}/src/linters/srp/typescript_analyzer.py +0 -0
  101. {thailint-0.7.0 → thailint-0.8.0}/src/linters/srp/typescript_metrics_calculator.py +0 -0
  102. {thailint-0.7.0 → thailint-0.8.0}/src/linters/srp/violation_builder.py +0 -0
  103. {thailint-0.7.0 → thailint-0.8.0}/src/orchestrator/__init__.py +0 -0
  104. {thailint-0.7.0 → thailint-0.8.0}/src/orchestrator/core.py +0 -0
  105. {thailint-0.7.0 → thailint-0.8.0}/src/orchestrator/language_detector.py +0 -0
  106. {thailint-0.7.0 → thailint-0.8.0}/src/templates/thailint_config_template.yaml +0 -0
  107. {thailint-0.7.0 → thailint-0.8.0}/src/utils/__init__.py +0 -0
  108. {thailint-0.7.0 → thailint-0.8.0}/src/utils/project_root.py +0 -0
@@ -1,13 +1,13 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: thailint
3
- Version: 0.7.0
3
+ Version: 0.8.0
4
4
  Summary: The AI Linter - Enterprise-grade linting and governance for AI-generated code across multiple languages
5
5
  License: MIT
6
6
  License-File: LICENSE
7
7
  Keywords: linter,ai,code-quality,static-analysis,file-placement,governance,multi-language,cli,docker,python
8
8
  Author: Steve Jackson
9
9
  Requires-Python: >=3.11,<4.0
10
- Classifier: Development Status :: 3 - Alpha
10
+ Classifier: Development Status :: 4 - Beta
11
11
  Classifier: Environment :: Console
12
12
  Classifier: Intended Audience :: Developers
13
13
  Classifier: License :: OSI Approved :: MIT License
@@ -37,7 +37,7 @@ Description-Content-Type: text/markdown
37
37
 
38
38
  [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
39
39
  [![Python 3.11+](https://img.shields.io/badge/python-3.11+-blue.svg)](https://www.python.org/downloads/)
40
- [![Tests](https://img.shields.io/badge/tests-571%2F571%20passing-brightgreen.svg)](tests/)
40
+ [![Tests](https://img.shields.io/badge/tests-682%2F682%20passing-brightgreen.svg)](tests/)
41
41
  [![Coverage](https://img.shields.io/badge/coverage-87%25-brightgreen.svg)](htmlcov/)
42
42
  [![Documentation Status](https://readthedocs.org/projects/thai-lint/badge/?version=latest)](https://thai-lint.readthedocs.io/en/latest/?badge=latest)
43
43
  [![SARIF 2.1.0](https://img.shields.io/badge/SARIF-2.1.0-orange.svg)](docs/sarif-output.md)
@@ -98,6 +98,12 @@ thailint complements your existing linting stack by catching the patterns AI too
98
98
  - Configurable thresholds (lines, tokens, occurrences)
99
99
  - Language-specific detection (Python, TypeScript, JavaScript)
100
100
  - False positive filtering (keyword args, imports)
101
+ - **Method Property Linting** - Detect methods that should be @property decorators
102
+ - Python AST-based detection
103
+ - get_* prefix detection (Java-style getters)
104
+ - Simple computed value detection
105
+ - Action verb exclusion (to_*, finalize, serialize)
106
+ - Test file detection
101
107
  - **Pluggable Architecture** - Easy to extend with custom linters
102
108
  - **Multi-Language Support** - Python, TypeScript, JavaScript, and more
103
109
  - **Flexible Configuration** - YAML/JSON configs with pattern matching
@@ -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-571%2F571%20passing-brightgreen.svg)](tests/)
5
+ [![Tests](https://img.shields.io/badge/tests-682%2F682%20passing-brightgreen.svg)](tests/)
6
6
  [![Coverage](https://img.shields.io/badge/coverage-87%25-brightgreen.svg)](htmlcov/)
7
7
  [![Documentation Status](https://readthedocs.org/projects/thai-lint/badge/?version=latest)](https://thai-lint.readthedocs.io/en/latest/?badge=latest)
8
8
  [![SARIF 2.1.0](https://img.shields.io/badge/SARIF-2.1.0-orange.svg)](docs/sarif-output.md)
@@ -63,6 +63,12 @@ thailint complements your existing linting stack by catching the patterns AI too
63
63
  - Configurable thresholds (lines, tokens, occurrences)
64
64
  - Language-specific detection (Python, TypeScript, JavaScript)
65
65
  - False positive filtering (keyword args, imports)
66
+ - **Method Property Linting** - Detect methods that should be @property decorators
67
+ - Python AST-based detection
68
+ - get_* prefix detection (Java-style getters)
69
+ - Simple computed value detection
70
+ - Action verb exclusion (to_*, finalize, serialize)
71
+ - Test file detection
66
72
  - **Pluggable Architecture** - Easy to extend with custom linters
67
73
  - **Multi-Language Support** - Python, TypeScript, JavaScript, and more
68
74
  - **Flexible Configuration** - YAML/JSON configs with pattern matching
@@ -17,7 +17,7 @@ build-backend = "poetry.core.masonry.api"
17
17
 
18
18
  [tool.poetry]
19
19
  name = "thailint"
20
- version = "0.7.0"
20
+ version = "0.8.0"
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"
@@ -38,7 +38,7 @@ keywords = [
38
38
  "python",
39
39
  ]
40
40
  classifiers = [
41
- "Development Status :: 3 - Alpha",
41
+ "Development Status :: 4 - Beta",
42
42
  "Intended Audience :: Developers",
43
43
  "License :: OSI Approved :: MIT License",
44
44
  "Programming Language :: Python :: 3",
@@ -11,7 +11,7 @@ Overview: Provides the main CLI application using Click decorators for command d
11
11
 
12
12
  Dependencies: click for CLI framework, logging for structured output, pathlib for file paths
13
13
 
14
- Exports: cli (main command group), hello command, config command group, file_placement command, dry command
14
+ Exports: cli (main command group), hello command, config command group, linter commands
15
15
 
16
16
  Interfaces: Click CLI commands, configuration context via Click ctx, logging integration
17
17
 
@@ -1778,5 +1778,122 @@ def _execute_file_header_lint( # pylint: disable=too-many-arguments,too-many-po
1778
1778
  sys.exit(1 if file_header_violations else 0)
1779
1779
 
1780
1780
 
1781
+ # =============================================================================
1782
+ # Method Property Linter Command
1783
+ # =============================================================================
1784
+
1785
+
1786
+ def _setup_method_property_orchestrator(
1787
+ path_objs: list[Path], config_file: str | None, verbose: bool, project_root: Path | None = None
1788
+ ):
1789
+ """Set up orchestrator for method-property command."""
1790
+ from src.orchestrator.core import Orchestrator
1791
+ from src.utils.project_root import get_project_root
1792
+
1793
+ if project_root is None:
1794
+ first_path = path_objs[0] if path_objs else Path.cwd()
1795
+ search_start = first_path if first_path.is_dir() else first_path.parent
1796
+ project_root = get_project_root(search_start)
1797
+
1798
+ orchestrator = Orchestrator(project_root=project_root)
1799
+
1800
+ if config_file:
1801
+ _load_config_file(orchestrator, config_file, verbose)
1802
+
1803
+ return orchestrator
1804
+
1805
+
1806
+ def _run_method_property_lint(orchestrator, path_objs: list[Path], recursive: bool):
1807
+ """Execute method-property lint on files or directories."""
1808
+ all_violations = _execute_linting_on_paths(orchestrator, path_objs, recursive)
1809
+ return [v for v in all_violations if "method-property" in v.rule_id]
1810
+
1811
+
1812
+ @cli.command("method-property")
1813
+ @click.argument("paths", nargs=-1, type=click.Path())
1814
+ @click.option("--config", "-c", "config_file", type=click.Path(), help="Path to config file")
1815
+ @format_option
1816
+ @click.option("--recursive/--no-recursive", default=True, help="Scan directories recursively")
1817
+ @click.pass_context
1818
+ def method_property(
1819
+ ctx,
1820
+ paths: tuple[str, ...],
1821
+ config_file: str | None,
1822
+ format: str,
1823
+ recursive: bool,
1824
+ ):
1825
+ """Check for methods that should be @property decorators.
1826
+
1827
+ Detects Python methods that could be converted to properties following
1828
+ Pythonic conventions:
1829
+ - Methods returning only self._attribute or self.attribute
1830
+ - get_* prefixed methods (Java-style getters)
1831
+ - Simple computed values with no side effects
1832
+
1833
+ PATHS: Files or directories to lint (defaults to current directory if none provided)
1834
+
1835
+ Examples:
1836
+
1837
+ \b
1838
+ # Check current directory (all files recursively)
1839
+ thai-lint method-property
1840
+
1841
+ \b
1842
+ # Check specific directory
1843
+ thai-lint method-property src/
1844
+
1845
+ \b
1846
+ # Check single file
1847
+ thai-lint method-property src/models.py
1848
+
1849
+ \b
1850
+ # Check multiple files
1851
+ thai-lint method-property src/models.py src/services.py
1852
+
1853
+ \b
1854
+ # Get JSON output
1855
+ thai-lint method-property --format json .
1856
+
1857
+ \b
1858
+ # Get SARIF output for CI/CD integration
1859
+ thai-lint method-property --format sarif src/
1860
+
1861
+ \b
1862
+ # Use custom config file
1863
+ thai-lint method-property --config .thailint.yaml src/
1864
+ """
1865
+ verbose = ctx.obj.get("verbose", False)
1866
+ project_root = _get_project_root_from_context(ctx)
1867
+
1868
+ if not paths:
1869
+ paths = (".",)
1870
+
1871
+ path_objs = [Path(p) for p in paths]
1872
+
1873
+ try:
1874
+ _execute_method_property_lint(
1875
+ path_objs, config_file, format, recursive, verbose, project_root
1876
+ )
1877
+ except Exception as e:
1878
+ _handle_linting_error(e, verbose)
1879
+
1880
+
1881
+ def _execute_method_property_lint( # pylint: disable=too-many-arguments,too-many-positional-arguments
1882
+ path_objs, config_file, format, recursive, verbose, project_root=None
1883
+ ):
1884
+ """Execute method-property lint."""
1885
+ _validate_paths_exist(path_objs)
1886
+ orchestrator = _setup_method_property_orchestrator(
1887
+ path_objs, config_file, verbose, project_root
1888
+ )
1889
+ method_property_violations = _run_method_property_lint(orchestrator, path_objs, recursive)
1890
+
1891
+ if verbose:
1892
+ logger.info(f"Found {len(method_property_violations)} method-property violation(s)")
1893
+
1894
+ format_violations(method_property_violations, format)
1895
+ sys.exit(1 if method_property_violations else 0)
1896
+
1897
+
1781
1898
  if __name__ == "__main__":
1782
1899
  cli()
@@ -19,7 +19,7 @@ Dependencies: PyYAML for YAML parsing with safe_load(), json (stdlib) for JSON p
19
19
  Exports: LinterConfigLoader class
20
20
 
21
21
  Interfaces: load(config_path: Path) -> dict[str, Any] for loading config files,
22
- get_defaults() -> dict[str, Any] for default configuration structure
22
+ defaults property -> dict[str, Any] for default configuration structure
23
23
 
24
24
  Implementation: Extension-based format detection (.yaml/.yml vs .json), yaml.safe_load()
25
25
  for security, empty dict handling for null YAML, ValueError for unsupported formats
@@ -51,12 +51,13 @@ class LinterConfigLoader:
51
51
  ConfigParseError: If file format is unsupported or parsing fails.
52
52
  """
53
53
  if not config_path.exists():
54
- return self.get_defaults()
54
+ return self.defaults
55
55
 
56
56
  return parse_config_file(config_path)
57
57
 
58
- def get_defaults(self) -> dict[str, Any]:
59
- """Get default configuration.
58
+ @property
59
+ def defaults(self) -> dict[str, Any]:
60
+ """Default configuration.
60
61
 
61
62
  Returns:
62
63
  Default configuration with empty rules and ignore lists.
@@ -53,9 +53,10 @@ class BaseBlockFilter(ABC):
53
53
  """
54
54
  pass
55
55
 
56
+ @property
56
57
  @abstractmethod
57
- def get_name(self) -> str:
58
- """Get filter name for configuration and logging."""
58
+ def name(self) -> str:
59
+ """Filter name for configuration and logging."""
59
60
  pass
60
61
 
61
62
 
@@ -152,8 +153,9 @@ class KeywordArgumentFilter(BaseBlockFilter):
152
153
  return False
153
154
  return True
154
155
 
155
- def get_name(self) -> str:
156
- """Get filter name."""
156
+ @property
157
+ def name(self) -> str:
158
+ """Filter name."""
157
159
  return "keyword_argument_filter"
158
160
 
159
161
 
@@ -184,8 +186,9 @@ class ImportGroupFilter(BaseBlockFilter):
184
186
 
185
187
  return True
186
188
 
187
- def get_name(self) -> str:
188
- """Get filter name."""
189
+ @property
190
+ def name(self) -> str:
191
+ """Filter name."""
189
192
  return "import_group_filter"
190
193
 
191
194
 
@@ -204,7 +207,7 @@ class BlockFilterRegistry:
204
207
  filter_instance: Filter to register
205
208
  """
206
209
  self._filters.append(filter_instance)
207
- self._enabled_filters.add(filter_instance.get_name())
210
+ self._enabled_filters.add(filter_instance.name)
208
211
 
209
212
  def enable_filter(self, filter_name: str) -> None:
210
213
  """Enable a specific filter by name.
@@ -233,7 +236,7 @@ class BlockFilterRegistry:
233
236
  True if block should be filtered out
234
237
  """
235
238
  for filter_instance in self._filters:
236
- if filter_instance.get_name() not in self._enabled_filters:
239
+ if filter_instance.name not in self._enabled_filters:
237
240
  continue
238
241
 
239
242
  if filter_instance.should_filter(block, file_content):
@@ -157,8 +157,9 @@ class DRYCache:
157
157
 
158
158
  return blocks
159
159
 
160
- def get_duplicate_hashes(self) -> list[int]:
161
- """Get all hash values that appear 2+ times.
160
+ @property
161
+ def duplicate_hashes(self) -> list[int]:
162
+ """Hash values that appear 2+ times.
162
163
 
163
164
  Returns:
164
165
  List of hash values with 2 or more occurrences
@@ -11,7 +11,7 @@ Dependencies: DRYCache, CodeBlock, Path
11
11
 
12
12
  Exports: DuplicateStorage class
13
13
 
14
- Interfaces: DuplicateStorage.add_blocks(file_path, blocks), get_duplicate_hashes(),
14
+ Interfaces: DuplicateStorage.add_blocks(file_path, blocks), duplicate_hashes property,
15
15
  get_blocks_for_hash(hash_value)
16
16
 
17
17
  Implementation: Delegates to SQLite cache for all storage operations
@@ -43,13 +43,14 @@ class DuplicateStorage:
43
43
  if blocks:
44
44
  self._cache.add_blocks(file_path, blocks)
45
45
 
46
- def get_duplicate_hashes(self) -> list[int]:
47
- """Get all hash values with 2+ occurrences from SQLite.
46
+ @property
47
+ def duplicate_hashes(self) -> list[int]:
48
+ """Hash values with 2+ occurrences from SQLite.
48
49
 
49
50
  Returns:
50
51
  List of hash values that appear in multiple blocks
51
52
  """
52
- return self._cache.get_duplicate_hashes()
53
+ return self._cache.duplicate_hashes
53
54
 
54
55
  def get_blocks_for_hash(self, hash_value: int) -> list[CodeBlock]:
55
56
  """Get all blocks with given hash value from SQLite.
@@ -55,7 +55,7 @@ class ViolationGenerator:
55
55
  Returns:
56
56
  List of violations filtered by ignore patterns and inline directives
57
57
  """
58
- duplicate_hashes = storage.get_duplicate_hashes()
58
+ duplicate_hashes = storage.duplicate_hashes
59
59
  violations = []
60
60
 
61
61
  for hash_value in duplicate_hashes:
@@ -0,0 +1,49 @@
1
+ """
2
+ Purpose: Package exports for method-should-be-property linter
3
+
4
+ Scope: Method property linter public API
5
+
6
+ Overview: Exports the MethodPropertyRule class and MethodPropertyConfig dataclass for use by
7
+ the orchestrator and external consumers. Provides a convenience lint() function for
8
+ standalone usage of the linter.
9
+
10
+ Dependencies: MethodPropertyRule from linter module, MethodPropertyConfig from config module
11
+
12
+ Exports: MethodPropertyRule, MethodPropertyConfig, lint function
13
+
14
+ Interfaces: lint(file_path, content, config) -> list[Violation] convenience function
15
+
16
+ Implementation: Simple re-exports from submodules with optional convenience wrapper
17
+ """
18
+
19
+ from .config import MethodPropertyConfig
20
+ from .linter import MethodPropertyRule
21
+
22
+ __all__ = ["MethodPropertyRule", "MethodPropertyConfig", "lint"]
23
+
24
+
25
+ def lint(
26
+ file_path: str,
27
+ content: str,
28
+ config: dict | None = None,
29
+ ) -> list:
30
+ """Lint a file for method-should-be-property violations.
31
+
32
+ Args:
33
+ file_path: Path to the file being linted
34
+ content: Content of the file
35
+ config: Optional configuration dictionary
36
+
37
+ Returns:
38
+ List of Violation objects
39
+ """
40
+ from unittest.mock import Mock
41
+
42
+ rule = MethodPropertyRule()
43
+ context = Mock()
44
+ context.file_path = file_path
45
+ context.file_content = content
46
+ context.language = "python"
47
+ context.config = config
48
+
49
+ return rule.check(context)
@@ -0,0 +1,135 @@
1
+ """
2
+ Purpose: Configuration schema for method-should-be-property linter
3
+
4
+ Scope: Method property linter configuration for Python files
5
+
6
+ Overview: Defines configuration schema for method-should-be-property linter. Provides
7
+ MethodPropertyConfig dataclass with enabled flag, max_body_statements threshold (default 3)
8
+ for determining when a method body is too complex to be a property candidate, and ignore
9
+ patterns list for excluding specific files or directories. Includes configurable action verb
10
+ exclusions (prefixes and names) with sensible defaults that can be extended or overridden.
11
+ Supports per-file and per-directory config overrides through from_dict class method.
12
+ Integrates with orchestrator's configuration system via .thailint.yaml.
13
+
14
+ Dependencies: dataclasses module for configuration structure, typing module for type hints
15
+
16
+ Exports: MethodPropertyConfig dataclass, DEFAULT_EXCLUDE_PREFIXES, DEFAULT_EXCLUDE_NAMES
17
+
18
+ Interfaces: from_dict(config, language) -> MethodPropertyConfig for configuration loading
19
+
20
+ Implementation: Dataclass with defaults matching Pythonic conventions and common use cases
21
+ """
22
+
23
+ from dataclasses import dataclass, field
24
+ from typing import Any
25
+
26
+ # Default action verb prefixes - methods starting with these are excluded
27
+ # These represent actions/transformations, not property access
28
+ DEFAULT_EXCLUDE_PREFIXES: tuple[str, ...] = (
29
+ "to_", # Transformation: to_dict, to_json, to_string
30
+ "dump_", # Serialization: dump_to_json, dump_to_apigw
31
+ "generate_", # Factory: generate_report, generate_html
32
+ "create_", # Factory: create_instance, create_config
33
+ "build_", # Construction: build_query, build_html
34
+ "make_", # Factory: make_request, make_connection
35
+ "render_", # Output: render_template, render_html
36
+ "compute_", # Calculation: compute_hash, compute_total
37
+ "calculate_", # Calculation: calculate_sum, calculate_average
38
+ )
39
+
40
+ # Default action verb names - exact method names that are excluded
41
+ # These are lifecycle hooks, display actions, and resource operations
42
+ DEFAULT_EXCLUDE_NAMES: frozenset[str] = frozenset(
43
+ {
44
+ "finalize", # Lifecycle hook
45
+ "serialize", # Transformation
46
+ "dump", # Serialization
47
+ "validate", # Validation action
48
+ "show", # Display action
49
+ "display", # Display action
50
+ "print", # Output action
51
+ "refresh", # Update action
52
+ "reset", # State action
53
+ "clear", # State action
54
+ "close", # Resource action
55
+ "open", # Resource action
56
+ "save", # Persistence action
57
+ "load", # Persistence action
58
+ "execute", # Action
59
+ "run", # Action
60
+ }
61
+ )
62
+
63
+
64
+ def _load_list_config(
65
+ config: dict[str, Any], key: str, override_key: str, default: tuple[str, ...]
66
+ ) -> tuple[str, ...]:
67
+ """Load a list config with extend/override semantics."""
68
+ if override_key in config and isinstance(config[override_key], list):
69
+ return tuple(config[override_key])
70
+ if key in config and isinstance(config[key], list):
71
+ return default + tuple(config[key])
72
+ return default
73
+
74
+
75
+ def _load_set_config(
76
+ config: dict[str, Any], key: str, override_key: str, default: frozenset[str]
77
+ ) -> frozenset[str]:
78
+ """Load a set config with extend/override semantics."""
79
+ if override_key in config and isinstance(config[override_key], list):
80
+ return frozenset(config[override_key])
81
+ if key in config and isinstance(config[key], list):
82
+ return default | frozenset(config[key])
83
+ return default
84
+
85
+
86
+ @dataclass
87
+ class MethodPropertyConfig: # thailint: ignore[dry]
88
+ """Configuration for method-should-be-property linter."""
89
+
90
+ enabled: bool = True
91
+ max_body_statements: int = 3
92
+ ignore: list[str] = field(default_factory=list)
93
+ ignore_methods: list[str] = field(default_factory=list)
94
+
95
+ # Action verb exclusions (extend defaults or override)
96
+ exclude_prefixes: tuple[str, ...] = DEFAULT_EXCLUDE_PREFIXES
97
+ exclude_names: frozenset[str] = DEFAULT_EXCLUDE_NAMES
98
+
99
+ # dry: ignore-block
100
+ @classmethod
101
+ def from_dict(
102
+ cls, config: dict[str, Any] | None, language: str | None = None
103
+ ) -> "MethodPropertyConfig":
104
+ """Load configuration from dictionary.
105
+
106
+ Args:
107
+ config: Dictionary containing configuration values, or None
108
+ language: Programming language (unused, for interface compatibility)
109
+
110
+ Returns:
111
+ MethodPropertyConfig instance with values from dictionary
112
+ """
113
+ if config is None:
114
+ return cls()
115
+
116
+ ignore_patterns = config.get("ignore", [])
117
+ if not isinstance(ignore_patterns, list):
118
+ ignore_patterns = []
119
+
120
+ ignore_methods = config.get("ignore_methods", [])
121
+ if not isinstance(ignore_methods, list):
122
+ ignore_methods = []
123
+
124
+ return cls(
125
+ enabled=config.get("enabled", True),
126
+ max_body_statements=config.get("max_body_statements", 3),
127
+ ignore=ignore_patterns,
128
+ ignore_methods=ignore_methods,
129
+ exclude_prefixes=_load_list_config(
130
+ config, "exclude_prefixes", "exclude_prefixes_override", DEFAULT_EXCLUDE_PREFIXES
131
+ ),
132
+ exclude_names=_load_set_config(
133
+ config, "exclude_names", "exclude_names_override", DEFAULT_EXCLUDE_NAMES
134
+ ),
135
+ )