thailint 0.5.0__py3-none-any.whl → 0.15.3__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/__init__.py +1 -0
- src/analyzers/__init__.py +4 -3
- src/analyzers/ast_utils.py +54 -0
- src/analyzers/rust_base.py +155 -0
- src/analyzers/rust_context.py +141 -0
- src/analyzers/typescript_base.py +4 -0
- src/cli/__init__.py +30 -0
- src/cli/__main__.py +22 -0
- src/cli/config.py +480 -0
- src/cli/config_merge.py +241 -0
- src/cli/linters/__init__.py +67 -0
- src/cli/linters/code_patterns.py +270 -0
- src/cli/linters/code_smells.py +342 -0
- src/cli/linters/documentation.py +83 -0
- src/cli/linters/performance.py +287 -0
- src/cli/linters/shared.py +331 -0
- src/cli/linters/structure.py +327 -0
- src/cli/linters/structure_quality.py +328 -0
- src/cli/main.py +120 -0
- src/cli/utils.py +395 -0
- src/cli_main.py +37 -0
- src/config.py +38 -25
- src/core/base.py +7 -2
- src/core/cli_utils.py +19 -2
- src/core/config_parser.py +5 -2
- src/core/constants.py +54 -0
- src/core/linter_utils.py +95 -6
- src/core/python_lint_rule.py +101 -0
- src/core/registry.py +1 -1
- src/core/rule_discovery.py +147 -84
- src/core/types.py +13 -0
- src/core/violation_builder.py +78 -15
- src/core/violation_utils.py +69 -0
- src/formatters/__init__.py +22 -0
- src/formatters/sarif.py +202 -0
- src/linter_config/directive_markers.py +109 -0
- src/linter_config/ignore.py +254 -395
- src/linter_config/loader.py +45 -12
- src/linter_config/pattern_utils.py +65 -0
- src/linter_config/rule_matcher.py +89 -0
- src/linters/collection_pipeline/__init__.py +90 -0
- src/linters/collection_pipeline/any_all_analyzer.py +281 -0
- src/linters/collection_pipeline/ast_utils.py +40 -0
- src/linters/collection_pipeline/config.py +75 -0
- src/linters/collection_pipeline/continue_analyzer.py +94 -0
- src/linters/collection_pipeline/detector.py +360 -0
- src/linters/collection_pipeline/filter_map_analyzer.py +402 -0
- src/linters/collection_pipeline/linter.py +420 -0
- src/linters/collection_pipeline/suggestion_builder.py +130 -0
- src/linters/cqs/__init__.py +54 -0
- src/linters/cqs/config.py +55 -0
- src/linters/cqs/function_analyzer.py +201 -0
- src/linters/cqs/input_detector.py +139 -0
- src/linters/cqs/linter.py +159 -0
- src/linters/cqs/output_detector.py +84 -0
- src/linters/cqs/python_analyzer.py +54 -0
- src/linters/cqs/types.py +82 -0
- src/linters/cqs/typescript_cqs_analyzer.py +61 -0
- src/linters/cqs/typescript_function_analyzer.py +192 -0
- src/linters/cqs/typescript_input_detector.py +203 -0
- src/linters/cqs/typescript_output_detector.py +117 -0
- src/linters/cqs/violation_builder.py +94 -0
- src/linters/dry/base_token_analyzer.py +16 -9
- src/linters/dry/block_filter.py +120 -20
- src/linters/dry/block_grouper.py +4 -0
- src/linters/dry/cache.py +104 -10
- src/linters/dry/cache_query.py +4 -0
- src/linters/dry/config.py +54 -11
- src/linters/dry/constant.py +92 -0
- src/linters/dry/constant_matcher.py +223 -0
- src/linters/dry/constant_violation_builder.py +98 -0
- src/linters/dry/duplicate_storage.py +5 -4
- src/linters/dry/file_analyzer.py +4 -2
- src/linters/dry/inline_ignore.py +7 -16
- src/linters/dry/linter.py +183 -48
- src/linters/dry/python_analyzer.py +60 -439
- src/linters/dry/python_constant_extractor.py +100 -0
- src/linters/dry/single_statement_detector.py +417 -0
- src/linters/dry/token_hasher.py +116 -112
- src/linters/dry/typescript_analyzer.py +68 -382
- src/linters/dry/typescript_constant_extractor.py +138 -0
- src/linters/dry/typescript_statement_detector.py +255 -0
- src/linters/dry/typescript_value_extractor.py +70 -0
- src/linters/dry/violation_builder.py +4 -0
- src/linters/dry/violation_filter.py +5 -4
- src/linters/dry/violation_generator.py +71 -14
- src/linters/file_header/atemporal_detector.py +68 -50
- src/linters/file_header/base_parser.py +93 -0
- src/linters/file_header/bash_parser.py +66 -0
- src/linters/file_header/config.py +90 -16
- src/linters/file_header/css_parser.py +70 -0
- src/linters/file_header/field_validator.py +36 -33
- src/linters/file_header/linter.py +140 -144
- src/linters/file_header/markdown_parser.py +130 -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/config_loader.py +3 -1
- src/linters/file_placement/directory_matcher.py +4 -0
- src/linters/file_placement/linter.py +66 -34
- src/linters/file_placement/pattern_matcher.py +41 -6
- src/linters/file_placement/pattern_validator.py +31 -12
- src/linters/file_placement/rule_checker.py +12 -7
- src/linters/lazy_ignores/__init__.py +43 -0
- src/linters/lazy_ignores/config.py +74 -0
- src/linters/lazy_ignores/directive_utils.py +164 -0
- src/linters/lazy_ignores/header_parser.py +177 -0
- src/linters/lazy_ignores/linter.py +158 -0
- src/linters/lazy_ignores/matcher.py +168 -0
- src/linters/lazy_ignores/python_analyzer.py +209 -0
- src/linters/lazy_ignores/rule_id_utils.py +180 -0
- src/linters/lazy_ignores/skip_detector.py +298 -0
- src/linters/lazy_ignores/types.py +71 -0
- src/linters/lazy_ignores/typescript_analyzer.py +146 -0
- src/linters/lazy_ignores/violation_builder.py +135 -0
- src/linters/lbyl/__init__.py +31 -0
- src/linters/lbyl/config.py +63 -0
- src/linters/lbyl/linter.py +67 -0
- src/linters/lbyl/pattern_detectors/__init__.py +53 -0
- src/linters/lbyl/pattern_detectors/base.py +63 -0
- src/linters/lbyl/pattern_detectors/dict_key_detector.py +107 -0
- src/linters/lbyl/pattern_detectors/division_check_detector.py +232 -0
- src/linters/lbyl/pattern_detectors/file_exists_detector.py +220 -0
- src/linters/lbyl/pattern_detectors/hasattr_detector.py +119 -0
- src/linters/lbyl/pattern_detectors/isinstance_detector.py +119 -0
- src/linters/lbyl/pattern_detectors/len_check_detector.py +173 -0
- src/linters/lbyl/pattern_detectors/none_check_detector.py +146 -0
- src/linters/lbyl/pattern_detectors/string_validator_detector.py +145 -0
- src/linters/lbyl/python_analyzer.py +215 -0
- src/linters/lbyl/violation_builder.py +354 -0
- src/linters/magic_numbers/context_analyzer.py +227 -225
- src/linters/magic_numbers/linter.py +28 -82
- src/linters/magic_numbers/python_analyzer.py +4 -16
- src/linters/magic_numbers/typescript_analyzer.py +9 -12
- src/linters/magic_numbers/typescript_ignore_checker.py +81 -0
- src/linters/method_property/__init__.py +49 -0
- src/linters/method_property/config.py +138 -0
- src/linters/method_property/linter.py +414 -0
- src/linters/method_property/python_analyzer.py +473 -0
- src/linters/method_property/violation_builder.py +119 -0
- src/linters/nesting/linter.py +24 -16
- src/linters/nesting/python_analyzer.py +4 -0
- src/linters/nesting/typescript_analyzer.py +6 -12
- src/linters/nesting/violation_builder.py +1 -0
- src/linters/performance/__init__.py +91 -0
- src/linters/performance/config.py +43 -0
- src/linters/performance/constants.py +49 -0
- src/linters/performance/linter.py +149 -0
- src/linters/performance/python_analyzer.py +365 -0
- src/linters/performance/regex_analyzer.py +312 -0
- src/linters/performance/regex_linter.py +139 -0
- src/linters/performance/typescript_analyzer.py +236 -0
- src/linters/performance/violation_builder.py +160 -0
- src/linters/print_statements/config.py +7 -12
- src/linters/print_statements/linter.py +26 -43
- src/linters/print_statements/python_analyzer.py +91 -93
- src/linters/print_statements/typescript_analyzer.py +15 -25
- src/linters/print_statements/violation_builder.py +12 -14
- src/linters/srp/class_analyzer.py +11 -7
- src/linters/srp/heuristics.py +56 -22
- src/linters/srp/linter.py +15 -16
- src/linters/srp/python_analyzer.py +55 -20
- src/linters/srp/typescript_metrics_calculator.py +110 -50
- src/linters/stateless_class/__init__.py +25 -0
- src/linters/stateless_class/config.py +58 -0
- src/linters/stateless_class/linter.py +349 -0
- src/linters/stateless_class/python_analyzer.py +290 -0
- src/linters/stringly_typed/__init__.py +36 -0
- src/linters/stringly_typed/config.py +189 -0
- src/linters/stringly_typed/context_filter.py +451 -0
- src/linters/stringly_typed/function_call_violation_builder.py +135 -0
- src/linters/stringly_typed/ignore_checker.py +100 -0
- src/linters/stringly_typed/ignore_utils.py +51 -0
- src/linters/stringly_typed/linter.py +376 -0
- src/linters/stringly_typed/python/__init__.py +33 -0
- src/linters/stringly_typed/python/analyzer.py +348 -0
- src/linters/stringly_typed/python/call_tracker.py +175 -0
- src/linters/stringly_typed/python/comparison_tracker.py +257 -0
- src/linters/stringly_typed/python/condition_extractor.py +134 -0
- src/linters/stringly_typed/python/conditional_detector.py +179 -0
- src/linters/stringly_typed/python/constants.py +21 -0
- src/linters/stringly_typed/python/match_analyzer.py +94 -0
- src/linters/stringly_typed/python/validation_detector.py +189 -0
- src/linters/stringly_typed/python/variable_extractor.py +96 -0
- src/linters/stringly_typed/storage.py +620 -0
- src/linters/stringly_typed/storage_initializer.py +45 -0
- src/linters/stringly_typed/typescript/__init__.py +28 -0
- src/linters/stringly_typed/typescript/analyzer.py +157 -0
- src/linters/stringly_typed/typescript/call_tracker.py +335 -0
- src/linters/stringly_typed/typescript/comparison_tracker.py +378 -0
- src/linters/stringly_typed/violation_generator.py +419 -0
- src/orchestrator/core.py +252 -14
- src/orchestrator/language_detector.py +5 -3
- src/templates/thailint_config_template.yaml +196 -0
- src/utils/project_root.py +3 -0
- thailint-0.15.3.dist-info/METADATA +187 -0
- thailint-0.15.3.dist-info/RECORD +226 -0
- thailint-0.15.3.dist-info/entry_points.txt +4 -0
- src/cli.py +0 -1665
- thailint-0.5.0.dist-info/METADATA +0 -1286
- thailint-0.5.0.dist-info/RECORD +0 -96
- thailint-0.5.0.dist-info/entry_points.txt +0 -4
- {thailint-0.5.0.dist-info → thailint-0.15.3.dist-info}/WHEEL +0 -0
- {thailint-0.5.0.dist-info → thailint-0.15.3.dist-info}/licenses/LICENSE +0 -0
src/core/violation_builder.py
CHANGED
|
@@ -13,13 +13,17 @@ Overview: Provides base classes and data structures for violation creation acros
|
|
|
13
13
|
|
|
14
14
|
Dependencies: dataclasses, src.core.types (Violation, Severity)
|
|
15
15
|
|
|
16
|
-
Exports: ViolationInfo dataclass,
|
|
16
|
+
Exports: ViolationInfo dataclass, build_violation function, build_violation_from_params function,
|
|
17
|
+
BaseViolationBuilder class (compat)
|
|
17
18
|
|
|
18
19
|
Interfaces: ViolationInfo(rule_id, file_path, line, message, column, severity),
|
|
19
|
-
|
|
20
|
+
build_violation(info: ViolationInfo) -> Violation
|
|
20
21
|
|
|
21
|
-
Implementation: Uses dataclass for type-safe violation info,
|
|
22
|
-
|
|
22
|
+
Implementation: Uses dataclass for type-safe violation info, functions provide build logic
|
|
23
|
+
that constructs Violation objects with proper defaults
|
|
24
|
+
|
|
25
|
+
Suppressions:
|
|
26
|
+
- too-many-arguments,too-many-positional-arguments: Violation fields as parameters
|
|
23
27
|
"""
|
|
24
28
|
|
|
25
29
|
from dataclasses import dataclass
|
|
@@ -50,14 +54,82 @@ class ViolationInfo:
|
|
|
50
54
|
suggestion: str | None = None
|
|
51
55
|
|
|
52
56
|
|
|
57
|
+
def build_violation(info: ViolationInfo) -> Violation:
|
|
58
|
+
"""Build a Violation from ViolationInfo.
|
|
59
|
+
|
|
60
|
+
Args:
|
|
61
|
+
info: ViolationInfo containing all violation details
|
|
62
|
+
|
|
63
|
+
Returns:
|
|
64
|
+
Violation object with all fields populated
|
|
65
|
+
"""
|
|
66
|
+
return Violation(
|
|
67
|
+
rule_id=info.rule_id,
|
|
68
|
+
file_path=info.file_path,
|
|
69
|
+
line=info.line,
|
|
70
|
+
column=info.column,
|
|
71
|
+
message=info.message,
|
|
72
|
+
severity=info.severity,
|
|
73
|
+
suggestion=info.suggestion,
|
|
74
|
+
)
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
def build_violation_from_params( # pylint: disable=too-many-arguments,too-many-positional-arguments
|
|
78
|
+
rule_id: str,
|
|
79
|
+
file_path: str,
|
|
80
|
+
line: int,
|
|
81
|
+
message: str,
|
|
82
|
+
column: int = 1,
|
|
83
|
+
severity: Severity = Severity.ERROR,
|
|
84
|
+
suggestion: str | None = None,
|
|
85
|
+
) -> Violation:
|
|
86
|
+
"""Build a Violation directly from parameters.
|
|
87
|
+
|
|
88
|
+
Note: Pylint too-many-arguments disabled. This convenience function mirrors the
|
|
89
|
+
ViolationInfo dataclass fields (7 parameters, 3 with defaults). The alternative
|
|
90
|
+
would require every caller to create ViolationInfo objects manually, reducing
|
|
91
|
+
readability.
|
|
92
|
+
|
|
93
|
+
Args:
|
|
94
|
+
rule_id: Identifier for the rule that was violated
|
|
95
|
+
file_path: Path to the file containing the violation
|
|
96
|
+
line: Line number where violation occurs (1-indexed)
|
|
97
|
+
message: Description of the violation
|
|
98
|
+
column: Column number where violation occurs (0-indexed, default=1)
|
|
99
|
+
severity: Severity level of the violation (default=ERROR)
|
|
100
|
+
suggestion: Optional suggestion for fixing the violation
|
|
101
|
+
|
|
102
|
+
Returns:
|
|
103
|
+
Violation object with all fields populated
|
|
104
|
+
"""
|
|
105
|
+
info = ViolationInfo(
|
|
106
|
+
rule_id=rule_id,
|
|
107
|
+
file_path=file_path,
|
|
108
|
+
line=line,
|
|
109
|
+
message=message,
|
|
110
|
+
column=column,
|
|
111
|
+
severity=severity,
|
|
112
|
+
suggestion=suggestion,
|
|
113
|
+
)
|
|
114
|
+
return build_violation(info)
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
# Legacy class wrapper for backward compatibility
|
|
53
118
|
class BaseViolationBuilder:
|
|
54
119
|
"""Base class for building violations with consistent structure.
|
|
55
120
|
|
|
56
121
|
Provides common build() method for creating Violation objects from ViolationInfo.
|
|
57
122
|
Linter-specific builders extend this class to add their domain-specific violation
|
|
58
123
|
creation methods while inheriting the common construction logic.
|
|
124
|
+
|
|
125
|
+
Note: This class is a thin wrapper around module-level functions
|
|
126
|
+
for backward compatibility.
|
|
59
127
|
"""
|
|
60
128
|
|
|
129
|
+
def __init__(self) -> None:
|
|
130
|
+
"""Initialize the builder."""
|
|
131
|
+
pass # No state needed
|
|
132
|
+
|
|
61
133
|
def build(self, info: ViolationInfo) -> Violation:
|
|
62
134
|
"""Build a Violation from ViolationInfo.
|
|
63
135
|
|
|
@@ -67,15 +139,7 @@ class BaseViolationBuilder:
|
|
|
67
139
|
Returns:
|
|
68
140
|
Violation object with all fields populated
|
|
69
141
|
"""
|
|
70
|
-
return
|
|
71
|
-
rule_id=info.rule_id,
|
|
72
|
-
file_path=info.file_path,
|
|
73
|
-
line=info.line,
|
|
74
|
-
column=info.column,
|
|
75
|
-
message=info.message,
|
|
76
|
-
severity=info.severity,
|
|
77
|
-
suggestion=info.suggestion,
|
|
78
|
-
)
|
|
142
|
+
return build_violation(info)
|
|
79
143
|
|
|
80
144
|
def build_from_params( # pylint: disable=too-many-arguments,too-many-positional-arguments
|
|
81
145
|
self,
|
|
@@ -110,7 +174,7 @@ class BaseViolationBuilder:
|
|
|
110
174
|
Returns:
|
|
111
175
|
Violation object with all fields populated
|
|
112
176
|
"""
|
|
113
|
-
|
|
177
|
+
return build_violation_from_params(
|
|
114
178
|
rule_id=rule_id,
|
|
115
179
|
file_path=file_path,
|
|
116
180
|
line=line,
|
|
@@ -119,4 +183,3 @@ class BaseViolationBuilder:
|
|
|
119
183
|
severity=severity,
|
|
120
184
|
suggestion=suggestion,
|
|
121
185
|
)
|
|
122
|
-
return self.build(info)
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Purpose: Shared utility functions for violation processing across linters
|
|
3
|
+
|
|
4
|
+
Scope: Common violation-related operations used by multiple linters
|
|
5
|
+
|
|
6
|
+
Overview: Provides shared utility functions for working with violations, including
|
|
7
|
+
extracting line text and checking for ignore directives. These patterns were
|
|
8
|
+
previously duplicated across multiple linter modules (magic_numbers, print_statements,
|
|
9
|
+
method_property). Centralizing them here improves maintainability and ensures
|
|
10
|
+
consistent behavior across all linters.
|
|
11
|
+
|
|
12
|
+
Dependencies: BaseLintContext, Violation types
|
|
13
|
+
|
|
14
|
+
Exports: get_violation_line, has_python_noqa, has_typescript_noqa
|
|
15
|
+
|
|
16
|
+
Interfaces:
|
|
17
|
+
get_violation_line(violation, context) -> str | None
|
|
18
|
+
has_python_noqa(line_text) -> bool
|
|
19
|
+
has_typescript_noqa(line_text) -> bool
|
|
20
|
+
|
|
21
|
+
Implementation: Simple text extraction and pattern matching
|
|
22
|
+
"""
|
|
23
|
+
|
|
24
|
+
from src.core.base import BaseLintContext
|
|
25
|
+
from src.core.types import Violation
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def get_violation_line(violation: Violation, context: BaseLintContext) -> str | None:
|
|
29
|
+
"""Get the line text for a violation, lowercased.
|
|
30
|
+
|
|
31
|
+
Args:
|
|
32
|
+
violation: Violation to get line for
|
|
33
|
+
context: Lint context with file content
|
|
34
|
+
|
|
35
|
+
Returns:
|
|
36
|
+
Lowercased line text, or None if not available
|
|
37
|
+
"""
|
|
38
|
+
if not context.file_content:
|
|
39
|
+
return None
|
|
40
|
+
|
|
41
|
+
lines = context.file_content.splitlines()
|
|
42
|
+
if violation.line <= 0 or violation.line > len(lines):
|
|
43
|
+
return None
|
|
44
|
+
|
|
45
|
+
return lines[violation.line - 1].lower()
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def has_python_noqa(line_text: str) -> bool:
|
|
49
|
+
"""Check if line has Python-style noqa directive.
|
|
50
|
+
|
|
51
|
+
Args:
|
|
52
|
+
line_text: Lowercased line text
|
|
53
|
+
|
|
54
|
+
Returns:
|
|
55
|
+
True if line has # noqa comment
|
|
56
|
+
"""
|
|
57
|
+
return "# noqa" in line_text
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def has_typescript_noqa(line_text: str) -> bool:
|
|
61
|
+
"""Check if line has TypeScript-style noqa directive.
|
|
62
|
+
|
|
63
|
+
Args:
|
|
64
|
+
line_text: Lowercased line text
|
|
65
|
+
|
|
66
|
+
Returns:
|
|
67
|
+
True if line has // noqa comment
|
|
68
|
+
"""
|
|
69
|
+
return "// noqa" in line_text
|
|
@@ -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
|
+
}
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Purpose: Ignore directive marker detection for thailint comments
|
|
3
|
+
|
|
4
|
+
Scope: Detection of thailint and design-lint ignore markers in source code
|
|
5
|
+
|
|
6
|
+
Overview: Provides functions for detecting various ignore directive markers in code
|
|
7
|
+
comments. Supports file-level ignores, line-level ignores, block ignores, and
|
|
8
|
+
next-line ignores. Works with both Python (#) and JavaScript (//) comment styles.
|
|
9
|
+
All checks are case-insensitive.
|
|
10
|
+
|
|
11
|
+
Dependencies: None (pure string operations)
|
|
12
|
+
|
|
13
|
+
Exports: Marker detection functions for various ignore directive types
|
|
14
|
+
|
|
15
|
+
Interfaces: has_*_marker(line) -> bool for each marker type
|
|
16
|
+
|
|
17
|
+
Implementation: String-based pattern detection with case-insensitive matching
|
|
18
|
+
"""
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def has_ignore_directive_marker(line: str) -> bool:
|
|
22
|
+
"""Check if line contains a file-level ignore directive marker.
|
|
23
|
+
|
|
24
|
+
Args:
|
|
25
|
+
line: Line of code to check
|
|
26
|
+
|
|
27
|
+
Returns:
|
|
28
|
+
True if line has ignore-file marker
|
|
29
|
+
"""
|
|
30
|
+
line_lower = line.lower()
|
|
31
|
+
return "# thailint: ignore-file" in line_lower or "# design-lint: ignore-file" in line_lower
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def has_line_ignore_marker(code: str) -> bool:
|
|
35
|
+
"""Check if code line has an inline ignore marker.
|
|
36
|
+
|
|
37
|
+
Args:
|
|
38
|
+
code: Line of code to check
|
|
39
|
+
|
|
40
|
+
Returns:
|
|
41
|
+
True if line has inline ignore marker
|
|
42
|
+
"""
|
|
43
|
+
code_lower = code.lower()
|
|
44
|
+
return (
|
|
45
|
+
"# thailint: ignore" in code_lower
|
|
46
|
+
or "# design-lint: ignore" in code_lower
|
|
47
|
+
or "// thailint: ignore" in code_lower
|
|
48
|
+
or "// design-lint: ignore" in code_lower
|
|
49
|
+
)
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def has_ignore_next_line_marker(line: str) -> bool:
|
|
53
|
+
"""Check if line has ignore-next-line marker.
|
|
54
|
+
|
|
55
|
+
Args:
|
|
56
|
+
line: Line of code to check
|
|
57
|
+
|
|
58
|
+
Returns:
|
|
59
|
+
True if line has ignore-next-line marker
|
|
60
|
+
"""
|
|
61
|
+
return "# thailint: ignore-next-line" in line or "# design-lint: ignore-next-line" in line
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def has_ignore_start_marker(line: str) -> bool:
|
|
65
|
+
"""Check if line has ignore-start comment marker.
|
|
66
|
+
|
|
67
|
+
Only matches actual comment lines (starting with # or //), not strings
|
|
68
|
+
containing the marker text.
|
|
69
|
+
|
|
70
|
+
Args:
|
|
71
|
+
line: Line of code to check
|
|
72
|
+
|
|
73
|
+
Returns:
|
|
74
|
+
True if line is a proper ignore-start comment
|
|
75
|
+
"""
|
|
76
|
+
stripped = line.strip().lower()
|
|
77
|
+
if not (stripped.startswith("#") or stripped.startswith("//")):
|
|
78
|
+
return False
|
|
79
|
+
return "ignore-start" in stripped and ("thailint:" in stripped or "design-lint:" in stripped)
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
def has_ignore_end_marker(line: str) -> bool:
|
|
83
|
+
"""Check if line has ignore-end comment marker.
|
|
84
|
+
|
|
85
|
+
Only matches actual comment lines (starting with # or //), not strings
|
|
86
|
+
containing the marker text.
|
|
87
|
+
|
|
88
|
+
Args:
|
|
89
|
+
line: Line of code to check
|
|
90
|
+
|
|
91
|
+
Returns:
|
|
92
|
+
True if line is a proper ignore-end comment
|
|
93
|
+
"""
|
|
94
|
+
stripped = line.strip().lower()
|
|
95
|
+
if not (stripped.startswith("#") or stripped.startswith("//")):
|
|
96
|
+
return False
|
|
97
|
+
return "ignore-end" in stripped and ("thailint:" in stripped or "design-lint:" in stripped)
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
def check_general_ignore(line: str) -> bool:
|
|
101
|
+
"""Check if line has general ignore directive (no specific rules).
|
|
102
|
+
|
|
103
|
+
Args:
|
|
104
|
+
line: Line containing ignore directive
|
|
105
|
+
|
|
106
|
+
Returns:
|
|
107
|
+
True if no specific rules are specified (not bracket syntax)
|
|
108
|
+
"""
|
|
109
|
+
return "ignore-file[" not in line
|