Coverage for lintro / parsers / shfmt / shfmt_parser.py: 93%
46 statements
« prev ^ index » next coverage.py v7.13.0, created at 2026-04-03 18:53 +0000
« prev ^ index » next coverage.py v7.13.0, created at 2026-04-03 18:53 +0000
1"""Parser for shfmt output.
3Shfmt outputs in unified diff format when run with the -d flag. This parser
4extracts file and line information from diff headers and creates ShfmtIssue
5objects for each file that needs formatting.
7Example shfmt diff output:
8--- script.sh.orig
9+++ script.sh
10@@ -1,3 +1,3 @@
11-if [ "$foo" = "bar" ]; then
12+if [ "$foo" = "bar" ]; then
13 echo "match"
14 fi
15"""
17from __future__ import annotations
19import re
21from loguru import logger
23from lintro.parsers.shfmt.shfmt_issue import ShfmtIssue
25# Pattern for diff file header: --- path or +++ path
26_DIFF_FILE_HEADER = re.compile(r"^(?:---|\+\+\+)\s+(.+?)(?:\.orig)?$")
28# Pattern for diff hunk header: @@ -start,count +start,count @@
29_DIFF_HUNK_HEADER = re.compile(r"^@@\s+-(\d+)(?:,\d+)?\s+\+(\d+)(?:,\d+)?\s+@@")
32def parse_shfmt_output(output: str | None) -> list[ShfmtIssue]:
33 """Parse shfmt diff output into a list of ShfmtIssue objects.
35 Shfmt outputs unified diff format when run with -d flag. This function
36 parses the diff output and extracts:
37 - File path from diff headers
38 - Line numbers from hunk headers
39 - Diff content for context
41 Args:
42 output: Raw stdout from shfmt -d invocation. May be None or empty
43 if no formatting issues were found.
45 Returns:
46 List of ShfmtIssue objects, one per file that needs formatting.
47 Returns empty list if output is None, empty, or contains no diffs.
48 """
49 if not output or not output.strip():
50 return []
52 issues: list[ShfmtIssue] = []
53 current_file: str | None = None
54 current_line: int = 0
55 current_diff_lines: list[str] = []
57 try:
58 lines = output.splitlines()
59 i = 0
60 while i < len(lines):
61 line = lines[i]
63 # Check for diff file header (--- or +++)
64 file_match = _DIFF_FILE_HEADER.match(line)
65 if file_match:
66 # When we see a new --- header, save any pending issue
67 if line.startswith("---"):
68 if current_file is not None and current_diff_lines:
69 issues.append(
70 ShfmtIssue(
71 file=current_file,
72 line=current_line if current_line > 0 else 1,
73 column=0,
74 message="Needs formatting",
75 diff_content="\n".join(current_diff_lines),
76 fixable=True,
77 ),
78 )
79 # Start tracking new file
80 current_file = file_match.group(1)
81 current_line = 0
82 current_diff_lines = [line]
83 elif line.startswith("+++") and current_file is not None:
84 # Add +++ line to current diff
85 current_diff_lines.append(line)
86 i += 1
87 continue
89 # Check for hunk header
90 hunk_match = _DIFF_HUNK_HEADER.match(line)
91 if hunk_match and current_file is not None:
92 # Use the first line number from the hunk if not set
93 if current_line == 0:
94 current_line = int(hunk_match.group(2))
95 current_diff_lines.append(line)
96 i += 1
97 continue
99 # Collect diff content lines (starting with -, +, or space)
100 if current_file is not None and (
101 line.startswith("-")
102 or line.startswith("+")
103 or line.startswith(" ")
104 or line == ""
105 ):
106 current_diff_lines.append(line)
108 i += 1
110 # Don't forget the last file
111 if current_file is not None and current_diff_lines:
112 issues.append(
113 ShfmtIssue(
114 file=current_file,
115 line=current_line if current_line > 0 else 1,
116 column=0,
117 message="Needs formatting",
118 diff_content="\n".join(current_diff_lines),
119 fixable=True,
120 ),
121 )
123 except (ValueError, AttributeError, IndexError) as e:
124 output_len = len(output) if output else 0
125 logger.debug(f"Error parsing shfmt output ({output_len} chars): {e}")
127 return issues