lintro 0.13.2__py3-none-any.whl → 0.17.2__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 (72) hide show
  1. lintro/__init__.py +1 -1
  2. lintro/cli.py +226 -16
  3. lintro/cli_utils/commands/__init__.py +8 -1
  4. lintro/cli_utils/commands/check.py +1 -0
  5. lintro/cli_utils/commands/config.py +325 -0
  6. lintro/cli_utils/commands/init.py +361 -0
  7. lintro/cli_utils/commands/list_tools.py +180 -42
  8. lintro/cli_utils/commands/test.py +316 -0
  9. lintro/cli_utils/commands/versions.py +81 -0
  10. lintro/config/__init__.py +62 -0
  11. lintro/config/config_loader.py +420 -0
  12. lintro/config/lintro_config.py +189 -0
  13. lintro/config/tool_config_generator.py +403 -0
  14. lintro/enums/tool_name.py +2 -0
  15. lintro/enums/tool_type.py +2 -0
  16. lintro/formatters/tools/__init__.py +12 -0
  17. lintro/formatters/tools/eslint_formatter.py +108 -0
  18. lintro/formatters/tools/markdownlint_formatter.py +88 -0
  19. lintro/formatters/tools/pytest_formatter.py +201 -0
  20. lintro/parsers/__init__.py +69 -9
  21. lintro/parsers/bandit/__init__.py +6 -0
  22. lintro/parsers/bandit/bandit_issue.py +49 -0
  23. lintro/parsers/bandit/bandit_parser.py +99 -0
  24. lintro/parsers/black/black_issue.py +4 -0
  25. lintro/parsers/eslint/__init__.py +6 -0
  26. lintro/parsers/eslint/eslint_issue.py +26 -0
  27. lintro/parsers/eslint/eslint_parser.py +63 -0
  28. lintro/parsers/markdownlint/__init__.py +6 -0
  29. lintro/parsers/markdownlint/markdownlint_issue.py +22 -0
  30. lintro/parsers/markdownlint/markdownlint_parser.py +113 -0
  31. lintro/parsers/pytest/__init__.py +21 -0
  32. lintro/parsers/pytest/pytest_issue.py +28 -0
  33. lintro/parsers/pytest/pytest_parser.py +483 -0
  34. lintro/tools/__init__.py +2 -0
  35. lintro/tools/core/timeout_utils.py +112 -0
  36. lintro/tools/core/tool_base.py +255 -45
  37. lintro/tools/core/tool_manager.py +77 -24
  38. lintro/tools/core/version_requirements.py +482 -0
  39. lintro/tools/implementations/pytest/pytest_command_builder.py +311 -0
  40. lintro/tools/implementations/pytest/pytest_config.py +200 -0
  41. lintro/tools/implementations/pytest/pytest_error_handler.py +128 -0
  42. lintro/tools/implementations/pytest/pytest_executor.py +122 -0
  43. lintro/tools/implementations/pytest/pytest_handlers.py +375 -0
  44. lintro/tools/implementations/pytest/pytest_option_validators.py +212 -0
  45. lintro/tools/implementations/pytest/pytest_output_processor.py +408 -0
  46. lintro/tools/implementations/pytest/pytest_result_processor.py +113 -0
  47. lintro/tools/implementations/pytest/pytest_utils.py +697 -0
  48. lintro/tools/implementations/tool_actionlint.py +106 -16
  49. lintro/tools/implementations/tool_bandit.py +23 -7
  50. lintro/tools/implementations/tool_black.py +236 -29
  51. lintro/tools/implementations/tool_darglint.py +180 -21
  52. lintro/tools/implementations/tool_eslint.py +374 -0
  53. lintro/tools/implementations/tool_hadolint.py +94 -25
  54. lintro/tools/implementations/tool_markdownlint.py +354 -0
  55. lintro/tools/implementations/tool_prettier.py +313 -26
  56. lintro/tools/implementations/tool_pytest.py +327 -0
  57. lintro/tools/implementations/tool_ruff.py +247 -70
  58. lintro/tools/implementations/tool_yamllint.py +448 -34
  59. lintro/tools/tool_enum.py +6 -0
  60. lintro/utils/config.py +41 -18
  61. lintro/utils/console_logger.py +211 -25
  62. lintro/utils/path_utils.py +42 -0
  63. lintro/utils/tool_executor.py +336 -39
  64. lintro/utils/tool_utils.py +38 -2
  65. lintro/utils/unified_config.py +926 -0
  66. {lintro-0.13.2.dist-info → lintro-0.17.2.dist-info}/METADATA +131 -29
  67. lintro-0.17.2.dist-info/RECORD +134 -0
  68. lintro-0.13.2.dist-info/RECORD +0 -96
  69. {lintro-0.13.2.dist-info → lintro-0.17.2.dist-info}/WHEEL +0 -0
  70. {lintro-0.13.2.dist-info → lintro-0.17.2.dist-info}/entry_points.txt +0 -0
  71. {lintro-0.13.2.dist-info → lintro-0.17.2.dist-info}/licenses/LICENSE +0 -0
  72. {lintro-0.13.2.dist-info → lintro-0.17.2.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,201 @@
1
+ """Formatter for pytest issues."""
2
+
3
+ from lintro.formatters.core.table_descriptor import TableDescriptor
4
+ from lintro.formatters.styles.csv import CsvStyle
5
+ from lintro.formatters.styles.grid import GridStyle
6
+ from lintro.formatters.styles.html import HtmlStyle
7
+ from lintro.formatters.styles.json import JsonStyle
8
+ from lintro.formatters.styles.markdown import MarkdownStyle
9
+ from lintro.formatters.styles.plain import PlainStyle
10
+ from lintro.parsers.pytest.pytest_issue import PytestIssue
11
+ from lintro.utils.path_utils import normalize_file_path_for_display
12
+
13
+ # Maximum message length before truncation (reasonable for terminal widths)
14
+ MAX_MESSAGE_LENGTH: int = 100
15
+
16
+ FORMAT_MAP = {
17
+ "plain": PlainStyle(),
18
+ "grid": GridStyle(),
19
+ "markdown": MarkdownStyle(),
20
+ "html": HtmlStyle(),
21
+ "json": JsonStyle(),
22
+ "csv": CsvStyle(),
23
+ }
24
+
25
+
26
+ class PytestFailuresTableDescriptor(TableDescriptor):
27
+ """Describe columns and rows for pytest failed/error issues."""
28
+
29
+ def get_columns(self) -> list[str]:
30
+ """Return ordered column headers for the pytest failures table.
31
+
32
+ Returns:
33
+ list[str]: Column names for the formatted table.
34
+ """
35
+ return ["File", "Status", "Error"]
36
+
37
+ def get_rows(
38
+ self,
39
+ issues: list[PytestIssue],
40
+ ) -> list[list[str]]:
41
+ """Return rows for the pytest failures table.
42
+
43
+ Args:
44
+ issues: Parsed pytest issues to render.
45
+
46
+ Returns:
47
+ list[list[str]]: Table rows with normalized file path and fields.
48
+ """
49
+ rows = []
50
+ for issue in issues:
51
+ # Only show failed/error tests
52
+ if issue.test_status not in ("FAILED", "ERROR"):
53
+ continue
54
+
55
+ message = str(issue.message) if issue.message is not None else ""
56
+ truncated_message = (
57
+ f"{message[:MAX_MESSAGE_LENGTH]}..."
58
+ if len(message) > MAX_MESSAGE_LENGTH + 3
59
+ else message
60
+ )
61
+
62
+ status_emoji = "❌ FAIL" if issue.test_status == "FAILED" else "⚠️ ERROR"
63
+
64
+ rows.append(
65
+ [
66
+ normalize_file_path_for_display(issue.file),
67
+ status_emoji,
68
+ truncated_message,
69
+ ],
70
+ )
71
+ return rows
72
+
73
+
74
+ class PytestSkippedTableDescriptor(TableDescriptor):
75
+ """Describe columns and rows for pytest skipped issues."""
76
+
77
+ def get_columns(self) -> list[str]:
78
+ """Return ordered column headers for the pytest skipped table.
79
+
80
+ Returns:
81
+ list[str]: Column names for the formatted table.
82
+ """
83
+ return ["File", "Test", "Skip Reason"]
84
+
85
+ def get_rows(
86
+ self,
87
+ issues: list[PytestIssue],
88
+ ) -> list[list[str]]:
89
+ """Return rows for the pytest skipped table.
90
+
91
+ Args:
92
+ issues: Parsed pytest issues to render.
93
+
94
+ Returns:
95
+ list[list[str]]: Table rows with normalized file path and fields.
96
+ """
97
+ rows = []
98
+ for issue in issues:
99
+ # Only show skipped tests
100
+ if issue.test_status != "SKIPPED":
101
+ continue
102
+
103
+ message = str(issue.message) if issue.message is not None else ""
104
+ truncated_message = (
105
+ f"{message[:MAX_MESSAGE_LENGTH]}..."
106
+ if len(message) > MAX_MESSAGE_LENGTH + 3
107
+ else message
108
+ )
109
+
110
+ rows.append(
111
+ [
112
+ normalize_file_path_for_display(issue.file),
113
+ issue.test_name or "Unknown",
114
+ truncated_message,
115
+ ],
116
+ )
117
+ return rows
118
+
119
+
120
+ def format_pytest_failures(
121
+ issues: list[PytestIssue],
122
+ format: str = "grid",
123
+ ) -> str:
124
+ """Format pytest failures and errors into a table.
125
+
126
+ Args:
127
+ issues: List of pytest issues to format.
128
+ format: Output format (plain, grid, markdown, html, json, csv).
129
+
130
+ Returns:
131
+ str: Formatted string with pytest failures table.
132
+ """
133
+ descriptor = PytestFailuresTableDescriptor()
134
+ formatter = FORMAT_MAP.get(format, GridStyle())
135
+
136
+ columns = descriptor.get_columns()
137
+ rows = descriptor.get_rows(issues)
138
+
139
+ # Always return a table structure, even if empty
140
+ return formatter.format(
141
+ columns=columns,
142
+ rows=rows,
143
+ )
144
+
145
+
146
+ def format_pytest_skipped(
147
+ issues: list[PytestIssue],
148
+ format: str = "grid",
149
+ ) -> str:
150
+ """Format pytest skipped tests into a table.
151
+
152
+ Args:
153
+ issues: List of pytest issues to format.
154
+ format: Output format (plain, grid, markdown, html, json, csv).
155
+
156
+ Returns:
157
+ str: Formatted string with pytest skipped tests table.
158
+ """
159
+ descriptor = PytestSkippedTableDescriptor()
160
+ formatter = FORMAT_MAP.get(format, GridStyle())
161
+
162
+ columns = descriptor.get_columns()
163
+ rows = descriptor.get_rows(issues)
164
+
165
+ # Always return a table structure, even if empty
166
+ return formatter.format(
167
+ columns=columns,
168
+ rows=rows,
169
+ )
170
+
171
+
172
+ def format_pytest_issues(
173
+ issues: list[PytestIssue],
174
+ format: str = "grid",
175
+ ) -> str:
176
+ """Format pytest issues into tables for failures and skipped tests.
177
+
178
+ Args:
179
+ issues: List of pytest issues to format.
180
+ format: Output format (plain, grid, markdown, html, json, csv).
181
+
182
+ Returns:
183
+ str: Formatted string with pytest issues tables.
184
+ """
185
+ output_parts = []
186
+
187
+ # Format failures table
188
+ failures_table = format_pytest_failures(issues, format)
189
+ if failures_table.strip():
190
+ output_parts.append("Test Failures:")
191
+ output_parts.append(failures_table)
192
+
193
+ # Format skipped tests table
194
+ skipped_table = format_pytest_skipped(issues, format)
195
+ if skipped_table.strip():
196
+ if output_parts:
197
+ output_parts.append("") # Add blank line between tables
198
+ output_parts.append("Skipped Tests:")
199
+ output_parts.append(skipped_table)
200
+
201
+ return "\n".join(output_parts)
@@ -1,21 +1,81 @@
1
1
  """Parser modules for Lintro tools."""
2
2
 
3
- from . import (
4
- actionlint,
5
- bandit,
6
- darglint,
7
- hadolint,
8
- prettier,
9
- ruff,
10
- yamllint,
11
- )
3
+ from __future__ import annotations
4
+
5
+ from importlib import import_module
6
+ from typing import TYPE_CHECKING
7
+
8
+ if TYPE_CHECKING:
9
+ # Type checking imports
10
+ from lintro.parsers import (
11
+ actionlint,
12
+ bandit,
13
+ darglint,
14
+ eslint,
15
+ hadolint,
16
+ markdownlint,
17
+ prettier,
18
+ pytest,
19
+ ruff,
20
+ yamllint,
21
+ )
12
22
 
13
23
  __all__ = [
14
24
  "actionlint",
15
25
  "bandit",
16
26
  "darglint",
27
+ "eslint",
17
28
  "hadolint",
29
+ "markdownlint",
18
30
  "prettier",
31
+ "pytest",
19
32
  "ruff",
20
33
  "yamllint",
21
34
  ]
35
+
36
+ # Lazy-load parser submodules to avoid circular imports
37
+ _SUBMODULES = {
38
+ "actionlint",
39
+ "bandit",
40
+ "darglint",
41
+ "eslint",
42
+ "hadolint",
43
+ "markdownlint",
44
+ "prettier",
45
+ "pytest",
46
+ "ruff",
47
+ "yamllint",
48
+ }
49
+
50
+
51
+ def __getattr__(name: str) -> object:
52
+ """Lazy-load parser submodules to avoid circular import issues.
53
+
54
+ This function is called when an attribute is accessed that doesn't exist
55
+ in the module. It allows accessing parser submodules without eagerly
56
+ importing them all at package initialization time.
57
+
58
+ Args:
59
+ name: The name of the attribute being accessed.
60
+
61
+ Returns:
62
+ The imported submodule.
63
+
64
+ Raises:
65
+ AttributeError: If the requested name is not a known submodule.
66
+ """
67
+ if name in _SUBMODULES:
68
+ module = import_module(f".{name}", __package__)
69
+ # Cache the module in this module's namespace for future access
70
+ globals()[name] = module
71
+ return module
72
+ raise AttributeError(f"module {__name__!r} has no attribute {name!r}")
73
+
74
+
75
+ def __dir__() -> list[str]:
76
+ """Return list of available attributes for this module.
77
+
78
+ Returns:
79
+ List of submodule names and other module attributes.
80
+ """
81
+ return list(__all__)
@@ -0,0 +1,6 @@
1
+ """Bandit parser module."""
2
+
3
+ from lintro.parsers.bandit.bandit_issue import BanditIssue
4
+ from lintro.parsers.bandit.bandit_parser import parse_bandit_output
5
+
6
+ __all__ = ["BanditIssue", "parse_bandit_output"]
@@ -0,0 +1,49 @@
1
+ """Bandit issue model for security vulnerabilities."""
2
+
3
+ from dataclasses import dataclass
4
+ from typing import Any
5
+
6
+
7
+ @dataclass
8
+ class BanditIssue:
9
+ """Represents a security issue found by Bandit.
10
+
11
+ Attributes:
12
+ file: str: Path to the file containing the issue.
13
+ line: int: Line number where the issue was found.
14
+ col_offset: int: Column offset of the issue.
15
+ issue_severity: str: Severity level (LOW, MEDIUM, HIGH).
16
+ issue_confidence: str: Confidence level (LOW, MEDIUM, HIGH).
17
+ test_id: str: Bandit test ID (e.g., B602, B301).
18
+ test_name: str: Name of the test that found the issue.
19
+ issue_text: str: Description of the security issue.
20
+ more_info: str: URL with more information about the issue.
21
+ cwe: dict[str, Any] | None: CWE (Common Weakness Enumeration) information.
22
+ code: str: Code snippet containing the issue.
23
+ line_range: list[int]: Range of lines containing the issue.
24
+ """
25
+
26
+ file: str
27
+ line: int
28
+ col_offset: int
29
+ issue_severity: str
30
+ issue_confidence: str
31
+ test_id: str
32
+ test_name: str
33
+ issue_text: str
34
+ more_info: str
35
+ cwe: dict[str, Any] | None = None
36
+ code: str | None = None
37
+ line_range: list[int] | None = None
38
+
39
+ @property
40
+ def message(self) -> str:
41
+ """Get a human-readable message for the issue.
42
+
43
+ Returns:
44
+ str: Formatted issue message.
45
+ """
46
+ return (
47
+ f"[{self.test_id}:{self.test_name}] {self.issue_severity} severity, "
48
+ f"{self.issue_confidence} confidence: {self.issue_text}"
49
+ )
@@ -0,0 +1,99 @@
1
+ """Bandit output parser for security issues."""
2
+
3
+ from typing import Any
4
+
5
+ from loguru import logger
6
+
7
+ from lintro.parsers.bandit.bandit_issue import BanditIssue
8
+
9
+
10
+ def parse_bandit_output(bandit_data: dict[str, Any]) -> list[BanditIssue]:
11
+ """Parse Bandit JSON output into BanditIssue objects.
12
+
13
+ Args:
14
+ bandit_data: dict[str, Any]: JSON data from Bandit output.
15
+
16
+ Returns:
17
+ list[BanditIssue]: List of parsed security issues.
18
+
19
+ Raises:
20
+ ValueError: If the bandit data structure is invalid.
21
+ """
22
+ if not isinstance(bandit_data, dict):
23
+ raise ValueError("Bandit data must be a dictionary")
24
+
25
+ results = bandit_data.get("results", [])
26
+ if not isinstance(results, list):
27
+ raise ValueError("Bandit results must be a list")
28
+
29
+ issues: list[BanditIssue] = []
30
+
31
+ for result in results:
32
+ if not isinstance(result, dict):
33
+ continue
34
+
35
+ try:
36
+ filename = result.get("filename", "")
37
+ line_number = result.get("line_number", 0)
38
+ col_offset = result.get("col_offset", 0)
39
+ issue_severity = result.get("issue_severity", "UNKNOWN")
40
+ issue_confidence = result.get("issue_confidence", "UNKNOWN")
41
+ test_id = result.get("test_id", "")
42
+ test_name = result.get("test_name", "")
43
+ issue_text = result.get("issue_text", "")
44
+ more_info = result.get("more_info", "")
45
+ cwe = result.get("issue_cwe")
46
+ code = result.get("code")
47
+ line_range = result.get("line_range")
48
+
49
+ # Validate critical fields; skip malformed entries
50
+ if not isinstance(filename, str):
51
+ logger.warning("Skipping issue with non-string filename")
52
+ continue
53
+ if not isinstance(line_number, int):
54
+ logger.warning("Skipping issue with non-integer line_number")
55
+ continue
56
+ if not isinstance(col_offset, int):
57
+ col_offset = 0
58
+
59
+ sev = (
60
+ str(issue_severity).upper() if issue_severity is not None else "UNKNOWN"
61
+ )
62
+ conf = (
63
+ str(issue_confidence).upper()
64
+ if issue_confidence is not None
65
+ else "UNKNOWN"
66
+ )
67
+
68
+ test_id = test_id if isinstance(test_id, str) else ""
69
+ test_name = test_name if isinstance(test_name, str) else ""
70
+ issue_text = issue_text if isinstance(issue_text, str) else ""
71
+ more_info = more_info if isinstance(more_info, str) else ""
72
+
73
+ # Normalize line_range to list[int] when provided
74
+ if isinstance(line_range, list):
75
+ line_range = [x for x in line_range if isinstance(x, int)] or None
76
+ else:
77
+ line_range = None
78
+
79
+ issue = BanditIssue(
80
+ file=filename,
81
+ line=line_number,
82
+ col_offset=col_offset,
83
+ issue_severity=sev,
84
+ issue_confidence=conf,
85
+ test_id=test_id,
86
+ test_name=test_name,
87
+ issue_text=issue_text,
88
+ more_info=more_info,
89
+ cwe=cwe if isinstance(cwe, dict) else None,
90
+ code=code if isinstance(code, str) else None,
91
+ line_range=line_range,
92
+ )
93
+ issues.append(issue)
94
+ except (KeyError, TypeError, ValueError) as e:
95
+ # Log warning but continue processing other issues
96
+ logger.warning(f"Failed to parse bandit issue: {e}")
97
+ continue
98
+
99
+ return issues
@@ -16,7 +16,11 @@ class BlackIssue:
16
16
  Attributes:
17
17
  file: Path to the file with a formatting difference.
18
18
  message: Short human-readable description (e.g., "Would reformat file").
19
+ fixable: Whether this issue can be auto-fixed by Black. Defaults to True
20
+ for standard formatting issues. Set to False for line length violations
21
+ that Black cannot safely wrap.
19
22
  """
20
23
 
21
24
  file: str
22
25
  message: str
26
+ fixable: bool = True
@@ -0,0 +1,6 @@
1
+ """Parsing utilities and types for ESLint output."""
2
+
3
+ from lintro.parsers.eslint.eslint_issue import EslintIssue
4
+ from lintro.parsers.eslint.eslint_parser import parse_eslint_output
5
+
6
+ __all__ = ["EslintIssue", "parse_eslint_output"]
@@ -0,0 +1,26 @@
1
+ """Typed structure representing a single ESLint issue."""
2
+
3
+ from dataclasses import dataclass
4
+
5
+
6
+ @dataclass
7
+ class EslintIssue:
8
+ """Simple container for ESLint findings.
9
+
10
+ Attributes:
11
+ file: File path where the issue occurred.
12
+ line: Line number where the issue occurred.
13
+ column: Column number where the issue occurred.
14
+ code: Rule ID that triggered the issue (e.g., 'no-unused-vars').
15
+ message: Human-readable description of the issue.
16
+ severity: Severity level (1=warning, 2=error).
17
+ fixable: Whether this issue can be auto-fixed.
18
+ """
19
+
20
+ file: str
21
+ line: int
22
+ column: int
23
+ code: str
24
+ message: str
25
+ severity: int
26
+ fixable: bool = False
@@ -0,0 +1,63 @@
1
+ """Parser for ESLint JSON output.
2
+
3
+ Handles ESLint JSON format output from --format json flag.
4
+ """
5
+
6
+ import json
7
+ from typing import Any
8
+
9
+ from lintro.parsers.eslint.eslint_issue import EslintIssue
10
+
11
+
12
+ def parse_eslint_output(output: str) -> list[EslintIssue]:
13
+ """Parse ESLint JSON output into a list of EslintIssue objects.
14
+
15
+ Args:
16
+ output: The raw JSON output from ESLint.
17
+
18
+ Returns:
19
+ List of EslintIssue objects.
20
+ """
21
+ issues: list[EslintIssue] = []
22
+
23
+ if not output:
24
+ return issues
25
+
26
+ try:
27
+ # ESLint JSON format is an array of file results
28
+ # Each file result has: filePath, messages[], errorCount, warningCount
29
+ eslint_data: list[dict[str, Any]] = json.loads(output)
30
+ except json.JSONDecodeError:
31
+ # If output is not valid JSON, return empty list
32
+ return issues
33
+
34
+ for file_result in eslint_data:
35
+ file_path: str = file_result.get("filePath", "")
36
+ messages: list[dict[str, Any]] = file_result.get("messages", [])
37
+
38
+ for msg in messages:
39
+ # Extract issue details from ESLint message format
40
+ line: int = msg.get("line", 1)
41
+ column: int = msg.get("column", 1)
42
+ rule_id: str = msg.get("ruleId", "unknown")
43
+ message_text: str = msg.get("message", "")
44
+ severity: int = msg.get("severity", 2) # Default to error (2)
45
+ fixable: bool = msg.get("fix") is not None # Has fix object if fixable
46
+
47
+ # Skip messages without a ruleId (usually syntax errors)
48
+ if not rule_id:
49
+ continue
50
+
51
+ issues.append(
52
+ EslintIssue(
53
+ file=file_path,
54
+ line=line,
55
+ column=column,
56
+ code=rule_id,
57
+ message=message_text,
58
+ severity=severity,
59
+ fixable=fixable,
60
+ ),
61
+ )
62
+
63
+ return issues
@@ -0,0 +1,6 @@
1
+ """Parser for markdownlint-cli2 output."""
2
+
3
+ from lintro.parsers.markdownlint.markdownlint_issue import MarkdownlintIssue
4
+ from lintro.parsers.markdownlint.markdownlint_parser import parse_markdownlint_output
5
+
6
+ __all__ = ["MarkdownlintIssue", "parse_markdownlint_output"]
@@ -0,0 +1,22 @@
1
+ """Markdownlint issue model."""
2
+
3
+ from dataclasses import dataclass
4
+
5
+
6
+ @dataclass
7
+ class MarkdownlintIssue:
8
+ """Represents an issue found by markdownlint-cli2.
9
+
10
+ Attributes:
11
+ file: File path where the issue was found
12
+ line: Line number where the issue occurs
13
+ code: Rule code that was violated (e.g., MD013, MD041)
14
+ message: Description of the issue
15
+ column: Column number where the issue occurs (if available)
16
+ """
17
+
18
+ file: str
19
+ line: int
20
+ code: str
21
+ message: str
22
+ column: int | None = None