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.
- src/cli.py +236 -2
- src/core/cli_utils.py +16 -1
- src/core/registry.py +1 -1
- src/formatters/__init__.py +22 -0
- src/formatters/sarif.py +202 -0
- src/linter_config/loader.py +5 -4
- src/linters/dry/block_filter.py +11 -8
- src/linters/dry/cache.py +3 -2
- src/linters/dry/duplicate_storage.py +5 -4
- src/linters/dry/violation_generator.py +1 -1
- src/linters/file_header/atemporal_detector.py +11 -11
- src/linters/file_header/base_parser.py +89 -0
- src/linters/file_header/bash_parser.py +58 -0
- src/linters/file_header/config.py +76 -16
- src/linters/file_header/css_parser.py +70 -0
- src/linters/file_header/field_validator.py +35 -29
- src/linters/file_header/linter.py +113 -121
- src/linters/file_header/markdown_parser.py +124 -0
- src/linters/file_header/python_parser.py +14 -58
- src/linters/file_header/typescript_parser.py +73 -0
- src/linters/file_header/violation_builder.py +13 -12
- src/linters/file_placement/linter.py +9 -11
- src/linters/method_property/__init__.py +49 -0
- src/linters/method_property/config.py +135 -0
- src/linters/method_property/linter.py +419 -0
- src/linters/method_property/python_analyzer.py +472 -0
- src/linters/method_property/violation_builder.py +116 -0
- src/linters/print_statements/config.py +7 -12
- src/linters/print_statements/linter.py +13 -15
- src/linters/print_statements/python_analyzer.py +8 -14
- src/linters/print_statements/typescript_analyzer.py +9 -14
- src/linters/print_statements/violation_builder.py +12 -14
- {thailint-0.5.0.dist-info → thailint-0.8.0.dist-info}/METADATA +155 -3
- {thailint-0.5.0.dist-info → thailint-0.8.0.dist-info}/RECORD +37 -25
- {thailint-0.5.0.dist-info → thailint-0.8.0.dist-info}/WHEEL +0 -0
- {thailint-0.5.0.dist-info → thailint-0.8.0.dist-info}/entry_points.txt +0 -0
- {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,
|
|
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",
|
|
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 "
|
|
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
|
|
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"]
|
src/formatters/sarif.py
ADDED
|
@@ -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
|
+
}
|
src/linter_config/loader.py
CHANGED
|
@@ -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
|
-
|
|
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.
|
|
54
|
+
return self.defaults
|
|
55
55
|
|
|
56
56
|
return parse_config_file(config_path)
|
|
57
57
|
|
|
58
|
-
|
|
59
|
-
|
|
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.
|
src/linters/dry/block_filter.py
CHANGED
|
@@ -53,9 +53,10 @@ class BaseBlockFilter(ABC):
|
|
|
53
53
|
"""
|
|
54
54
|
pass
|
|
55
55
|
|
|
56
|
+
@property
|
|
56
57
|
@abstractmethod
|
|
57
|
-
def
|
|
58
|
-
"""
|
|
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
|
-
|
|
156
|
-
|
|
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
|
-
|
|
188
|
-
|
|
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.
|
|
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.
|
|
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
|
-
|
|
161
|
-
|
|
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),
|
|
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
|
-
|
|
47
|
-
|
|
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.
|
|
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.
|
|
58
|
+
duplicate_hashes = storage.duplicate_hashes
|
|
59
59
|
violations = []
|
|
60
60
|
|
|
61
61
|
for hash_value in duplicate_hashes:
|