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

1"""Parser for shfmt output. 

2 

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. 

6 

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""" 

16 

17from __future__ import annotations 

18 

19import re 

20 

21from loguru import logger 

22 

23from lintro.parsers.shfmt.shfmt_issue import ShfmtIssue 

24 

25# Pattern for diff file header: --- path or +++ path 

26_DIFF_FILE_HEADER = re.compile(r"^(?:---|\+\+\+)\s+(.+?)(?:\.orig)?$") 

27 

28# Pattern for diff hunk header: @@ -start,count +start,count @@ 

29_DIFF_HUNK_HEADER = re.compile(r"^@@\s+-(\d+)(?:,\d+)?\s+\+(\d+)(?:,\d+)?\s+@@") 

30 

31 

32def parse_shfmt_output(output: str | None) -> list[ShfmtIssue]: 

33 """Parse shfmt diff output into a list of ShfmtIssue objects. 

34 

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 

40 

41 Args: 

42 output: Raw stdout from shfmt -d invocation. May be None or empty 

43 if no formatting issues were found. 

44 

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 [] 

51 

52 issues: list[ShfmtIssue] = [] 

53 current_file: str | None = None 

54 current_line: int = 0 

55 current_diff_lines: list[str] = [] 

56 

57 try: 

58 lines = output.splitlines() 

59 i = 0 

60 while i < len(lines): 

61 line = lines[i] 

62 

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 

88 

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 

98 

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) 

107 

108 i += 1 

109 

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 ) 

122 

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}") 

126 

127 return issues