thailint 0.5.0__py3-none-any.whl → 0.8.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 (37) hide show
  1. src/cli.py +236 -2
  2. src/core/cli_utils.py +16 -1
  3. src/core/registry.py +1 -1
  4. src/formatters/__init__.py +22 -0
  5. src/formatters/sarif.py +202 -0
  6. src/linter_config/loader.py +5 -4
  7. src/linters/dry/block_filter.py +11 -8
  8. src/linters/dry/cache.py +3 -2
  9. src/linters/dry/duplicate_storage.py +5 -4
  10. src/linters/dry/violation_generator.py +1 -1
  11. src/linters/file_header/atemporal_detector.py +11 -11
  12. src/linters/file_header/base_parser.py +89 -0
  13. src/linters/file_header/bash_parser.py +58 -0
  14. src/linters/file_header/config.py +76 -16
  15. src/linters/file_header/css_parser.py +70 -0
  16. src/linters/file_header/field_validator.py +35 -29
  17. src/linters/file_header/linter.py +113 -121
  18. src/linters/file_header/markdown_parser.py +124 -0
  19. src/linters/file_header/python_parser.py +14 -58
  20. src/linters/file_header/typescript_parser.py +73 -0
  21. src/linters/file_header/violation_builder.py +13 -12
  22. src/linters/file_placement/linter.py +9 -11
  23. src/linters/method_property/__init__.py +49 -0
  24. src/linters/method_property/config.py +135 -0
  25. src/linters/method_property/linter.py +419 -0
  26. src/linters/method_property/python_analyzer.py +472 -0
  27. src/linters/method_property/violation_builder.py +116 -0
  28. src/linters/print_statements/config.py +7 -12
  29. src/linters/print_statements/linter.py +13 -15
  30. src/linters/print_statements/python_analyzer.py +8 -14
  31. src/linters/print_statements/typescript_analyzer.py +9 -14
  32. src/linters/print_statements/violation_builder.py +12 -14
  33. {thailint-0.5.0.dist-info → thailint-0.8.0.dist-info}/METADATA +155 -3
  34. {thailint-0.5.0.dist-info → thailint-0.8.0.dist-info}/RECORD +37 -25
  35. {thailint-0.5.0.dist-info → thailint-0.8.0.dist-info}/WHEEL +0 -0
  36. {thailint-0.5.0.dist-info → thailint-0.8.0.dist-info}/entry_points.txt +0 -0
  37. {thailint-0.5.0.dist-info → thailint-0.8.0.dist-info}/licenses/LICENSE +0 -0
src/cli.py CHANGED
@@ -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
 
@@ -38,7 +38,11 @@ logger = logging.getLogger(__name__)
38
38
  def format_option(func):
39
39
  """Add --format option to a command for output format selection."""
40
40
  return click.option(
41
- "--format", "-f", type=click.Choice(["text", "json"]), default="text", help="Output format"
41
+ "--format",
42
+ "-f",
43
+ type=click.Choice(["text", "json", "sarif"]),
44
+ default="text",
45
+ help="Output format",
42
46
  )(func)
43
47
 
44
48
 
@@ -1661,5 +1665,235 @@ def _execute_print_statements_lint( # pylint: disable=too-many-arguments,too-ma
1661
1665
  sys.exit(1 if print_statements_violations else 0)
1662
1666
 
1663
1667
 
1668
+ # File Header Command Helper Functions
1669
+
1670
+
1671
+ def _setup_file_header_orchestrator(
1672
+ path_objs: list[Path], config_file: str | None, verbose: bool, project_root: Path | None = None
1673
+ ):
1674
+ """Set up orchestrator for file-header command."""
1675
+ from src.orchestrator.core import Orchestrator
1676
+ from src.utils.project_root import get_project_root
1677
+
1678
+ # Use provided project_root or fall back to auto-detection
1679
+ if project_root is None:
1680
+ first_path = path_objs[0] if path_objs else Path.cwd()
1681
+ search_start = first_path if first_path.is_dir() else first_path.parent
1682
+ project_root = get_project_root(search_start)
1683
+
1684
+ orchestrator = Orchestrator(project_root=project_root)
1685
+
1686
+ if config_file:
1687
+ _load_config_file(orchestrator, config_file, verbose)
1688
+
1689
+ return orchestrator
1690
+
1691
+
1692
+ def _run_file_header_lint(orchestrator, path_objs: list[Path], recursive: bool):
1693
+ """Execute file-header lint on files or directories."""
1694
+ all_violations = _execute_linting_on_paths(orchestrator, path_objs, recursive)
1695
+ return [v for v in all_violations if "file-header" in v.rule_id]
1696
+
1697
+
1698
+ @cli.command("file-header")
1699
+ @click.argument("paths", nargs=-1, type=click.Path())
1700
+ @click.option("--config", "-c", "config_file", type=click.Path(), help="Path to config file")
1701
+ @format_option
1702
+ @click.option("--recursive/--no-recursive", default=True, help="Scan directories recursively")
1703
+ @click.pass_context
1704
+ def file_header(
1705
+ ctx,
1706
+ paths: tuple[str, ...],
1707
+ config_file: str | None,
1708
+ format: str,
1709
+ recursive: bool,
1710
+ ):
1711
+ """Check file headers for mandatory fields and atemporal language.
1712
+
1713
+ Validates that source files have proper documentation headers containing
1714
+ required fields (Purpose, Scope, Overview, etc.) and don't use temporal
1715
+ language (dates, "currently", "now", etc.).
1716
+
1717
+ Supports Python, TypeScript, JavaScript, Bash, Markdown, and CSS files.
1718
+
1719
+ PATHS: Files or directories to lint (defaults to current directory if none provided)
1720
+
1721
+ Examples:
1722
+
1723
+ \b
1724
+ # Check current directory (all files recursively)
1725
+ thai-lint file-header
1726
+
1727
+ \b
1728
+ # Check specific directory
1729
+ thai-lint file-header src/
1730
+
1731
+ \b
1732
+ # Check single file
1733
+ thai-lint file-header src/cli.py
1734
+
1735
+ \b
1736
+ # Check multiple files
1737
+ thai-lint file-header src/cli.py src/api.py tests/
1738
+
1739
+ \b
1740
+ # Get JSON output
1741
+ thai-lint file-header --format json .
1742
+
1743
+ \b
1744
+ # Get SARIF output for CI/CD integration
1745
+ thai-lint file-header --format sarif src/
1746
+
1747
+ \b
1748
+ # Use custom config file
1749
+ thai-lint file-header --config .thailint.yaml src/
1750
+ """
1751
+ verbose = ctx.obj.get("verbose", False)
1752
+ project_root = _get_project_root_from_context(ctx)
1753
+
1754
+ # Default to current directory if no paths provided
1755
+ if not paths:
1756
+ paths = (".",)
1757
+
1758
+ path_objs = [Path(p) for p in paths]
1759
+
1760
+ try:
1761
+ _execute_file_header_lint(path_objs, config_file, format, recursive, verbose, project_root)
1762
+ except Exception as e:
1763
+ _handle_linting_error(e, verbose)
1764
+
1765
+
1766
+ def _execute_file_header_lint( # pylint: disable=too-many-arguments,too-many-positional-arguments
1767
+ path_objs, config_file, format, recursive, verbose, project_root=None
1768
+ ):
1769
+ """Execute file-header lint."""
1770
+ _validate_paths_exist(path_objs)
1771
+ orchestrator = _setup_file_header_orchestrator(path_objs, config_file, verbose, project_root)
1772
+ file_header_violations = _run_file_header_lint(orchestrator, path_objs, recursive)
1773
+
1774
+ if verbose:
1775
+ logger.info(f"Found {len(file_header_violations)} file header violation(s)")
1776
+
1777
+ format_violations(file_header_violations, format)
1778
+ sys.exit(1 if file_header_violations else 0)
1779
+
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
+
1664
1898
  if __name__ == "__main__":
1665
1899
  cli()
src/core/cli_utils.py CHANGED
@@ -146,10 +146,12 @@ def format_violations(violations: list, output_format: str) -> None:
146
146
 
147
147
  Args:
148
148
  violations: List of violation objects with rule_id, file_path, line, column, message, severity
149
- output_format: Output format ("text" or "json")
149
+ output_format: Output format ("text", "json", or "sarif")
150
150
  """
151
151
  if output_format == "json":
152
152
  _output_json(violations)
153
+ elif output_format == "sarif":
154
+ _output_sarif(violations)
153
155
  else:
154
156
  _output_text(violations)
155
157
 
@@ -177,6 +179,19 @@ def _output_json(violations: list) -> None:
177
179
  click.echo(json.dumps(output, indent=2))
178
180
 
179
181
 
182
+ def _output_sarif(violations: list) -> None:
183
+ """Output violations in SARIF v2.1.0 format.
184
+
185
+ Args:
186
+ violations: List of violation objects
187
+ """
188
+ from src.formatters.sarif import SarifFormatter
189
+
190
+ formatter = SarifFormatter()
191
+ sarif_doc = formatter.format(violations)
192
+ click.echo(json.dumps(sarif_doc, indent=2))
193
+
194
+
180
195
  def _output_text(violations: list) -> None:
181
196
  """Output violations in human-readable text format.
182
197
 
src/core/registry.py CHANGED
@@ -6,7 +6,7 @@ Scope: Dynamic rule management and discovery across all linter plugin packages
6
6
  Overview: Implements rule registry that maintains a collection of registered linting rules indexed
7
7
  by rule_id. Provides methods to register individual rules, retrieve rules by identifier, list
8
8
  all available rules, and discover rules from packages using the RuleDiscovery helper. Enables
9
- the extensible plugin architecture by allowing rules to be added dynamically without framework
9
+ the extensible plugin architecture by allowing dynamic rule registration without framework
10
10
  modifications. Validates rule uniqueness and handles registration errors gracefully.
11
11
 
12
12
  Dependencies: BaseLintRule, RuleDiscovery
@@ -0,0 +1,22 @@
1
+ """
2
+ Purpose: SARIF formatter package for thai-lint output
3
+
4
+ Scope: SARIF v2.1.0 formatter implementation and package exports
5
+
6
+ Overview: Formatters package providing SARIF (Static Analysis Results Interchange Format) v2.1.0
7
+ output generation from thai-lint Violation objects. Enables integration with GitHub Code
8
+ Scanning, Azure DevOps, VS Code SARIF Viewer, and other industry-standard CI/CD platforms.
9
+ Provides the SarifFormatter class for converting violations to SARIF JSON documents.
10
+
11
+ Dependencies: sarif module for SarifFormatter class
12
+
13
+ Exports: SarifFormatter class from sarif.py module
14
+
15
+ Interfaces: from src.formatters.sarif import SarifFormatter
16
+
17
+ Implementation: Package initialization with SarifFormatter export
18
+ """
19
+
20
+ from src.formatters.sarif import SarifFormatter
21
+
22
+ __all__ = ["SarifFormatter"]
@@ -0,0 +1,202 @@
1
+ """
2
+ Purpose: SARIF v2.1.0 formatter for converting Violation objects to SARIF JSON documents
3
+
4
+ Scope: SARIF document generation, tool metadata, result conversion, location mapping
5
+
6
+ Overview: Implements SarifFormatter class that converts thai-lint Violation objects to SARIF
7
+ (Static Analysis Results Interchange Format) v2.1.0 compliant JSON documents. Produces
8
+ output compatible with GitHub Code Scanning, Azure DevOps, VS Code SARIF Viewer, and
9
+ other industry-standard static analysis tools. Handles proper field mapping including
10
+ 1-indexed column conversion, rule metadata deduplication, and tool versioning.
11
+
12
+ Dependencies: src (for __version__), src.core.types (Violation, Severity)
13
+
14
+ Exports: SarifFormatter class with format() method
15
+
16
+ Interfaces: SarifFormatter.format(violations: list[Violation]) -> dict
17
+
18
+ Implementation: Converts Violation objects to SARIF structure with proper indexing and metadata
19
+ """
20
+
21
+ from typing import Any
22
+
23
+ from src import __version__
24
+ from src.core.types import Violation
25
+
26
+
27
+ class SarifFormatter:
28
+ """Formats Violation objects as SARIF v2.1.0 JSON documents.
29
+
30
+ SARIF (Static Analysis Results Interchange Format) is the OASIS standard
31
+ for static analysis tool output, enabling integration with GitHub Code
32
+ Scanning, Azure DevOps, and other CI/CD platforms.
33
+
34
+ Attributes:
35
+ tool_name: Name of the tool in SARIF output (default: "thai-lint")
36
+ tool_version: Version string for the tool (default: package version)
37
+ information_uri: URL for tool documentation
38
+ """
39
+
40
+ SARIF_VERSION = "2.1.0"
41
+ SARIF_SCHEMA = (
42
+ "https://raw.githubusercontent.com/oasis-tcs/sarif-spec/"
43
+ "main/sarif-2.1/schema/sarif-schema-2.1.0.json"
44
+ )
45
+ DEFAULT_INFORMATION_URI = "https://github.com/be-wise-be-kind/thai-lint"
46
+
47
+ def __init__(
48
+ self,
49
+ tool_name: str = "thai-lint",
50
+ tool_version: str | None = None,
51
+ information_uri: str | None = None,
52
+ ) -> None:
53
+ """Initialize SarifFormatter with tool metadata.
54
+
55
+ Args:
56
+ tool_name: Name of the tool (default: "thai-lint")
57
+ tool_version: Version string (default: package __version__)
58
+ information_uri: URL for tool documentation
59
+ """
60
+ self.tool_name = tool_name
61
+ self.tool_version = tool_version or __version__
62
+ self.information_uri = information_uri or self.DEFAULT_INFORMATION_URI
63
+
64
+ def format(self, violations: list[Violation]) -> dict[str, Any]:
65
+ """Convert violations to SARIF v2.1.0 document.
66
+
67
+ Args:
68
+ violations: List of Violation objects to format
69
+
70
+ Returns:
71
+ SARIF v2.1.0 compliant dictionary ready for JSON serialization
72
+ """
73
+ return {
74
+ "version": self.SARIF_VERSION,
75
+ "$schema": self.SARIF_SCHEMA,
76
+ "runs": [self._create_run(violations)],
77
+ }
78
+
79
+ def _create_run(self, violations: list[Violation]) -> dict[str, Any]:
80
+ """Create a SARIF run object containing tool and results.
81
+
82
+ Args:
83
+ violations: List of violations for this run
84
+
85
+ Returns:
86
+ SARIF run object with tool and results
87
+ """
88
+ return {
89
+ "tool": self._create_tool(violations),
90
+ "results": [self._create_result(v) for v in violations],
91
+ }
92
+
93
+ def _create_tool(self, violations: list[Violation]) -> dict[str, Any]:
94
+ """Create SARIF tool object with driver metadata.
95
+
96
+ Args:
97
+ violations: List of violations to extract rule metadata from
98
+
99
+ Returns:
100
+ SARIF tool object with driver
101
+ """
102
+ return {
103
+ "driver": {
104
+ "name": self.tool_name,
105
+ "version": self.tool_version,
106
+ "informationUri": self.information_uri,
107
+ "rules": self._create_rules(violations),
108
+ }
109
+ }
110
+
111
+ def _create_rules(self, violations: list[Violation]) -> list[dict[str, Any]]:
112
+ """Create deduplicated SARIF rules array from violations.
113
+
114
+ Args:
115
+ violations: List of violations to extract unique rules from
116
+
117
+ Returns:
118
+ List of SARIF rule objects with unique IDs
119
+ """
120
+ seen_rule_ids: set[str] = set()
121
+ rules: list[dict[str, Any]] = []
122
+
123
+ for violation in violations:
124
+ if violation.rule_id not in seen_rule_ids:
125
+ seen_rule_ids.add(violation.rule_id)
126
+ rules.append(self._create_rule(violation))
127
+
128
+ return rules
129
+
130
+ def _create_rule(self, violation: Violation) -> dict[str, Any]:
131
+ """Create SARIF rule object from violation.
132
+
133
+ Args:
134
+ violation: Violation to extract rule metadata from
135
+
136
+ Returns:
137
+ SARIF rule object with id and shortDescription
138
+ """
139
+ # Extract rule category from rule_id (e.g., "nesting" from "nesting.excessive-depth")
140
+ parts = violation.rule_id.split(".")
141
+ category = parts[0] if parts else violation.rule_id
142
+
143
+ descriptions: dict[str, str] = {
144
+ "file-placement": "File placement violation",
145
+ "nesting": "Nesting depth violation",
146
+ "srp": "Single Responsibility Principle violation",
147
+ "dry": "Don't Repeat Yourself violation",
148
+ "magic-number": "Magic number violation",
149
+ "magic-numbers": "Magic number violation",
150
+ "file-header": "File header violation",
151
+ "print-statements": "Print statement violation",
152
+ }
153
+
154
+ description = descriptions.get(category, f"Rule: {violation.rule_id}")
155
+
156
+ return {
157
+ "id": violation.rule_id,
158
+ "shortDescription": {
159
+ "text": description,
160
+ },
161
+ }
162
+
163
+ def _create_result(self, violation: Violation) -> dict[str, Any]:
164
+ """Create SARIF result object from violation.
165
+
166
+ Args:
167
+ violation: Violation to convert to SARIF result
168
+
169
+ Returns:
170
+ SARIF result object with ruleId, level, message, locations
171
+ """
172
+ # thai-lint uses binary severity (ERROR only), map all to "error" level
173
+ return {
174
+ "ruleId": violation.rule_id,
175
+ "level": "error",
176
+ "message": {
177
+ "text": violation.message,
178
+ },
179
+ "locations": [self._create_location(violation)],
180
+ }
181
+
182
+ def _create_location(self, violation: Violation) -> dict[str, Any]:
183
+ """Create SARIF location object from violation.
184
+
185
+ Args:
186
+ violation: Violation with location information
187
+
188
+ Returns:
189
+ SARIF location object with physicalLocation
190
+ """
191
+ return {
192
+ "physicalLocation": {
193
+ "artifactLocation": {
194
+ "uri": violation.file_path,
195
+ },
196
+ "region": {
197
+ "startLine": violation.line,
198
+ # SARIF uses 1-indexed columns, Violation uses 0-indexed
199
+ "startColumn": violation.column + 1,
200
+ },
201
+ }
202
+ }
@@ -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):
src/linters/dry/cache.py CHANGED
@@ -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: