lintro 0.6.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 (109) hide show
  1. lintro/__init__.py +1 -1
  2. lintro/cli.py +230 -14
  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/format.py +2 -2
  7. lintro/cli_utils/commands/init.py +361 -0
  8. lintro/cli_utils/commands/list_tools.py +180 -42
  9. lintro/cli_utils/commands/test.py +316 -0
  10. lintro/cli_utils/commands/versions.py +81 -0
  11. lintro/config/__init__.py +62 -0
  12. lintro/config/config_loader.py +420 -0
  13. lintro/config/lintro_config.py +189 -0
  14. lintro/config/tool_config_generator.py +403 -0
  15. lintro/enums/__init__.py +1 -0
  16. lintro/enums/darglint_strictness.py +10 -0
  17. lintro/enums/hadolint_enums.py +22 -0
  18. lintro/enums/tool_name.py +2 -0
  19. lintro/enums/tool_type.py +2 -0
  20. lintro/enums/yamllint_format.py +11 -0
  21. lintro/exceptions/__init__.py +1 -0
  22. lintro/formatters/__init__.py +1 -0
  23. lintro/formatters/core/__init__.py +1 -0
  24. lintro/formatters/core/output_style.py +11 -0
  25. lintro/formatters/core/table_descriptor.py +8 -0
  26. lintro/formatters/styles/csv.py +2 -0
  27. lintro/formatters/styles/grid.py +2 -0
  28. lintro/formatters/styles/html.py +2 -0
  29. lintro/formatters/styles/json.py +2 -0
  30. lintro/formatters/styles/markdown.py +2 -0
  31. lintro/formatters/styles/plain.py +2 -0
  32. lintro/formatters/tools/__init__.py +12 -0
  33. lintro/formatters/tools/black_formatter.py +27 -5
  34. lintro/formatters/tools/darglint_formatter.py +16 -1
  35. lintro/formatters/tools/eslint_formatter.py +108 -0
  36. lintro/formatters/tools/hadolint_formatter.py +13 -0
  37. lintro/formatters/tools/markdownlint_formatter.py +88 -0
  38. lintro/formatters/tools/prettier_formatter.py +15 -0
  39. lintro/formatters/tools/pytest_formatter.py +201 -0
  40. lintro/formatters/tools/ruff_formatter.py +26 -5
  41. lintro/formatters/tools/yamllint_formatter.py +14 -1
  42. lintro/models/__init__.py +1 -0
  43. lintro/models/core/__init__.py +1 -0
  44. lintro/models/core/tool_config.py +11 -7
  45. lintro/parsers/__init__.py +69 -9
  46. lintro/parsers/actionlint/actionlint_parser.py +1 -1
  47. lintro/parsers/bandit/__init__.py +6 -0
  48. lintro/parsers/bandit/bandit_issue.py +49 -0
  49. lintro/parsers/bandit/bandit_parser.py +99 -0
  50. lintro/parsers/black/black_issue.py +4 -0
  51. lintro/parsers/darglint/__init__.py +1 -0
  52. lintro/parsers/darglint/darglint_issue.py +11 -0
  53. lintro/parsers/eslint/__init__.py +6 -0
  54. lintro/parsers/eslint/eslint_issue.py +26 -0
  55. lintro/parsers/eslint/eslint_parser.py +63 -0
  56. lintro/parsers/markdownlint/__init__.py +6 -0
  57. lintro/parsers/markdownlint/markdownlint_issue.py +22 -0
  58. lintro/parsers/markdownlint/markdownlint_parser.py +113 -0
  59. lintro/parsers/prettier/__init__.py +1 -0
  60. lintro/parsers/prettier/prettier_issue.py +12 -0
  61. lintro/parsers/prettier/prettier_parser.py +1 -1
  62. lintro/parsers/pytest/__init__.py +21 -0
  63. lintro/parsers/pytest/pytest_issue.py +28 -0
  64. lintro/parsers/pytest/pytest_parser.py +483 -0
  65. lintro/parsers/ruff/ruff_parser.py +6 -2
  66. lintro/parsers/yamllint/__init__.py +1 -0
  67. lintro/tools/__init__.py +3 -1
  68. lintro/tools/core/__init__.py +1 -0
  69. lintro/tools/core/timeout_utils.py +112 -0
  70. lintro/tools/core/tool_base.py +286 -50
  71. lintro/tools/core/tool_manager.py +77 -24
  72. lintro/tools/core/version_requirements.py +482 -0
  73. lintro/tools/implementations/__init__.py +1 -0
  74. lintro/tools/implementations/pytest/pytest_command_builder.py +311 -0
  75. lintro/tools/implementations/pytest/pytest_config.py +200 -0
  76. lintro/tools/implementations/pytest/pytest_error_handler.py +128 -0
  77. lintro/tools/implementations/pytest/pytest_executor.py +122 -0
  78. lintro/tools/implementations/pytest/pytest_handlers.py +375 -0
  79. lintro/tools/implementations/pytest/pytest_option_validators.py +212 -0
  80. lintro/tools/implementations/pytest/pytest_output_processor.py +408 -0
  81. lintro/tools/implementations/pytest/pytest_result_processor.py +113 -0
  82. lintro/tools/implementations/pytest/pytest_utils.py +697 -0
  83. lintro/tools/implementations/tool_actionlint.py +106 -16
  84. lintro/tools/implementations/tool_bandit.py +34 -29
  85. lintro/tools/implementations/tool_black.py +236 -29
  86. lintro/tools/implementations/tool_darglint.py +183 -22
  87. lintro/tools/implementations/tool_eslint.py +374 -0
  88. lintro/tools/implementations/tool_hadolint.py +94 -25
  89. lintro/tools/implementations/tool_markdownlint.py +354 -0
  90. lintro/tools/implementations/tool_prettier.py +317 -24
  91. lintro/tools/implementations/tool_pytest.py +327 -0
  92. lintro/tools/implementations/tool_ruff.py +278 -84
  93. lintro/tools/implementations/tool_yamllint.py +448 -34
  94. lintro/tools/tool_enum.py +8 -0
  95. lintro/utils/__init__.py +1 -0
  96. lintro/utils/ascii_normalize_cli.py +5 -0
  97. lintro/utils/config.py +41 -18
  98. lintro/utils/console_logger.py +211 -25
  99. lintro/utils/path_utils.py +42 -0
  100. lintro/utils/tool_executor.py +339 -45
  101. lintro/utils/tool_utils.py +51 -24
  102. lintro/utils/unified_config.py +926 -0
  103. {lintro-0.6.2.dist-info → lintro-0.17.2.dist-info}/METADATA +172 -30
  104. lintro-0.17.2.dist-info/RECORD +134 -0
  105. lintro-0.6.2.dist-info/RECORD +0 -96
  106. {lintro-0.6.2.dist-info → lintro-0.17.2.dist-info}/WHEEL +0 -0
  107. {lintro-0.6.2.dist-info → lintro-0.17.2.dist-info}/entry_points.txt +0 -0
  108. {lintro-0.6.2.dist-info → lintro-0.17.2.dist-info}/licenses/LICENSE +0 -0
  109. {lintro-0.6.2.dist-info → lintro-0.17.2.dist-info}/top_level.txt +0 -0
@@ -1,21 +1,81 @@
1
1
  """Parser modules for Lintro tools."""
2
2
 
3
- from lintro.parsers 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__)
@@ -8,7 +8,7 @@ uniform tables and reports across styles.
8
8
  from __future__ import annotations
9
9
 
10
10
  import re
11
- from typing import Iterable
11
+ from collections.abc import Iterable
12
12
 
13
13
  from lintro.parsers.actionlint.actionlint_issue import ActionlintIssue
14
14
 
@@ -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 @@
1
+ """Parsing utilities and types for Darglint output."""
@@ -1,8 +1,19 @@
1
+ """Typed structure representing a single Darglint issue."""
2
+
1
3
  from dataclasses import dataclass
2
4
 
3
5
 
4
6
  @dataclass
5
7
  class DarglintIssue:
8
+ """Simple container for Darglint findings.
9
+
10
+ Attributes:
11
+ file: File path where the issue occurred.
12
+ line: Line number of the issue.
13
+ code: Darglint error code.
14
+ message: Human-readable description of the issue.
15
+ """
16
+
6
17
  file: str
7
18
  line: int
8
19
  code: str
@@ -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
@@ -0,0 +1,113 @@
1
+ """Parser for markdownlint-cli2 output."""
2
+
3
+ import re
4
+
5
+ from lintro.parsers.markdownlint.markdownlint_issue import MarkdownlintIssue
6
+
7
+
8
+ def parse_markdownlint_output(output: str) -> list[MarkdownlintIssue]:
9
+ """Parse markdownlint-cli2 output into a list of MarkdownlintIssue objects.
10
+
11
+ Markdownlint-cli2 default formatter outputs lines like:
12
+ file:line:column MD###/rule-name Message [Context: "..."]
13
+ or
14
+ file:line MD###/rule-name Message [Context: "..."]
15
+
16
+ Example outputs:
17
+ dir/about.md:1:1 MD021/no-multiple-space-closed-atx Multiple spaces
18
+ inside hashes on closed atx style heading [Context: "# About #"]
19
+ dir/about.md:4 MD032/blanks-around-lists Lists should be surrounded
20
+ by blank lines [Context: "1. List"]
21
+ viewme.md:3:10 MD009/no-trailing-spaces Trailing spaces
22
+ [Expected: 0 or 2; Actual: 1]
23
+
24
+ Args:
25
+ output: The raw output from markdownlint-cli2
26
+
27
+ Returns:
28
+ List of MarkdownlintIssue objects
29
+ """
30
+ issues: list[MarkdownlintIssue] = []
31
+
32
+ # Skip empty output
33
+ if not output.strip():
34
+ return issues
35
+
36
+ lines: list[str] = output.splitlines()
37
+
38
+ # Pattern for markdownlint-cli2 default formatter:
39
+ # file:line[:column] [error] MD###/rule-name Message [Context: "..."]
40
+ # Column is optional, "error" keyword is optional, and Context is optional
41
+ # Also handles variations like: file:line MD### Message
42
+ # [Expected: ...; Actual: ...]
43
+ pattern: re.Pattern[str] = re.compile(
44
+ r"^([^:]+):(\d+)(?::(\d+))?\s+(?:error\s+)?(MD\d+)(?:/[^:\s]+)?(?::\s*)?"
45
+ r"(.+?)(?:\s+\[(?:Context|Expected|Actual):.*?\])?$",
46
+ )
47
+
48
+ i = 0
49
+ while i < len(lines):
50
+ line = lines[i]
51
+
52
+ # Skip empty lines
53
+ if not line.strip():
54
+ i += 1
55
+ continue
56
+
57
+ # Skip metadata lines (version, Finding, Linting, Summary)
58
+ stripped_line = line.strip()
59
+ if (
60
+ stripped_line.startswith("markdownlint-cli2")
61
+ or stripped_line.startswith("Finding:")
62
+ or stripped_line.startswith("Linting:")
63
+ or stripped_line.startswith("Summary:")
64
+ ):
65
+ i += 1
66
+ continue
67
+
68
+ # Try to match the pattern on the current line
69
+ match: re.Match[str] | None = pattern.match(stripped_line)
70
+ if match:
71
+ filename: str
72
+ line_num: str
73
+ column: str | None
74
+ code: str
75
+ message: str
76
+ filename, line_num, column, code, message = match.groups()
77
+
78
+ # Collect continuation lines (lines that start with whitespace)
79
+ # These are part of the multi-line message
80
+ i += 1
81
+ continuation_lines: list[str] = []
82
+ while i < len(lines):
83
+ next_line = lines[i]
84
+ # Continuation lines start with whitespace (indentation)
85
+ # Empty lines break the continuation
86
+ if not next_line.strip():
87
+ break
88
+ if next_line[0].isspace():
89
+ continuation_lines.append(next_line.strip())
90
+ i += 1
91
+ else:
92
+ # Next line doesn't start with whitespace, stop collecting
93
+ break
94
+
95
+ # Combine main message with continuation lines
96
+ full_message = message.strip()
97
+ if continuation_lines:
98
+ full_message = " ".join([full_message] + continuation_lines)
99
+
100
+ issues.append(
101
+ MarkdownlintIssue(
102
+ file=filename,
103
+ line=int(line_num),
104
+ column=int(column) if column else None,
105
+ code=code,
106
+ message=full_message,
107
+ ),
108
+ )
109
+ else:
110
+ # Line doesn't match pattern, skip it
111
+ i += 1
112
+
113
+ return issues
@@ -0,0 +1 @@
1
+ """Parsing utilities and types for Prettier output."""
@@ -1,8 +1,20 @@
1
+ """Typed structure representing a single Prettier issue."""
2
+
1
3
  from dataclasses import dataclass
2
4
 
3
5
 
4
6
  @dataclass
5
7
  class PrettierIssue:
8
+ """Simple container for Prettier findings.
9
+
10
+ Attributes:
11
+ file: File path where the issue occurred.
12
+ line: Line number, if provided by Prettier.
13
+ code: Tool-specific code identifying the rule.
14
+ message: Human-readable description of the issue.
15
+ column: Column number, if provided by Prettier.
16
+ """
17
+
6
18
  file: str
7
19
  line: int | None
8
20
  code: str
@@ -36,7 +36,7 @@ def parse_prettier_output(output: str) -> list[PrettierIssue]:
36
36
 
37
37
  lines = normalized_output.splitlines()
38
38
 
39
- for i, line in enumerate(lines):
39
+ for _i, line in enumerate(lines):
40
40
  line = line.strip()
41
41
  if not line:
42
42
  continue
@@ -0,0 +1,21 @@
1
+ """Pytest parser module."""
2
+
3
+ from lintro.parsers.pytest.pytest_issue import PytestIssue
4
+ from lintro.parsers.pytest.pytest_parser import (
5
+ PytestSummary,
6
+ extract_pytest_summary,
7
+ parse_pytest_json_output,
8
+ parse_pytest_junit_xml,
9
+ parse_pytest_output,
10
+ parse_pytest_text_output,
11
+ )
12
+
13
+ __all__ = [
14
+ "PytestIssue",
15
+ "PytestSummary",
16
+ "extract_pytest_summary",
17
+ "parse_pytest_json_output",
18
+ "parse_pytest_junit_xml",
19
+ "parse_pytest_output",
20
+ "parse_pytest_text_output",
21
+ ]
@@ -0,0 +1,28 @@
1
+ """Models for pytest issues."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from dataclasses import dataclass
6
+
7
+
8
+ @dataclass
9
+ class PytestIssue:
10
+ """Represents a pytest test result (failure, error, or skip).
11
+
12
+ Attributes:
13
+ file: File path where the test issue occurred.
14
+ line: Line number where the issue occurred.
15
+ test_name: Name of the test.
16
+ message: Error message, failure description, or skip reason.
17
+ test_status: Status of the test (FAILED, ERROR, SKIPPED, etc.).
18
+ duration: Duration of the test in seconds.
19
+ node_id: Full node ID of the test.
20
+ """
21
+
22
+ file: str
23
+ line: int
24
+ test_name: str
25
+ message: str
26
+ test_status: str
27
+ duration: float | None = None
28
+ node_id: str | None = None