Coverage for lintro / formatters / styles / github.py: 96%

52 statements  

« prev     ^ index     » next       coverage.py v7.13.0, created at 2026-04-03 18:53 +0000

1"""GitHub Actions workflow-command output style. 

2 

3Emits ``::error``, ``::warning``, and ``::notice`` annotations that GitHub 

4Actions surfaces inline on pull-request diffs. 

5 

6Reference: https://docs.github.com/en/actions/writing-workflows/choosing-what-your-workflow-does/workflow-commands-for-github-actions#setting-a-warning-message 

7""" 

8 

9from __future__ import annotations 

10 

11import contextlib 

12from typing import Any 

13 

14from lintro.enums.severity_level import SeverityLevel, normalize_severity_level 

15from lintro.formatters.core.format_registry import OutputStyle 

16 

17_SEVERITY_TO_COMMAND: dict[SeverityLevel, str] = { 

18 SeverityLevel.ERROR: "error", 

19 SeverityLevel.WARNING: "warning", 

20 SeverityLevel.INFO: "notice", 

21} 

22 

23 

24def _escape(value: str) -> str: 

25 """Escape special characters for GitHub Actions workflow commands. 

26 

27 Args: 

28 value: Raw string to escape. 

29 

30 Returns: 

31 Escaped string safe for workflow command messages. 

32 """ 

33 return value.replace("%", "%25").replace("\r", "%0D").replace("\n", "%0A") 

34 

35 

36def _cell( 

37 name: str, 

38 col_index: dict[str, int], 

39 row: list[Any], 

40) -> str: 

41 """Extract a cell value from a row by column name. 

42 

43 Args: 

44 name: Lowercase column name to look up. 

45 col_index: Mapping of column names to indices. 

46 row: The current row of values. 

47 

48 Returns: 

49 Cell value as string, or empty string if not found. 

50 """ 

51 idx = col_index.get(name) 

52 if idx is None or idx >= len(row): 

53 return "" 

54 return str(row[idx]) if row[idx] else "" 

55 

56 

57class GitHubStyle(OutputStyle): 

58 """Output style that emits GitHub Actions annotation commands.""" 

59 

60 def format( 

61 self, 

62 columns: list[str], 

63 rows: list[list[Any]], 

64 tool_name: str | None = None, 

65 **kwargs: Any, 

66 ) -> str: 

67 """Format rows as GitHub Actions annotation commands. 

68 

69 Args: 

70 columns: List of column header names. 

71 rows: List of row values (each row is a list of cell values). 

72 tool_name: Name of the tool that generated the data. 

73 **kwargs: Extra options (ignored). 

74 

75 Returns: 

76 One annotation command per line. 

77 """ 

78 if not rows: 

79 return "" 

80 

81 # Build a case-insensitive column-name → index map 

82 col_index: dict[str, int] = { 

83 col.lower().replace(" ", "_"): i for i, col in enumerate(columns) 

84 } 

85 

86 lines: list[str] = [] 

87 for row in rows: 

88 file_val = _cell("file", col_index, row) 

89 line_val = _cell("line", col_index, row) 

90 col_val = _cell("column", col_index, row) 

91 code_val = _cell("code", col_index, row) 

92 severity_val = _cell("severity", col_index, row) 

93 message_val = _cell("message", col_index, row) 

94 

95 # Resolve severity → GitHub command level 

96 level = "warning" # default 

97 if severity_val: 

98 with contextlib.suppress(ValueError, KeyError): 

99 level = _SEVERITY_TO_COMMAND[normalize_severity_level(severity_val)] 

100 

101 # Build the properties portion 

102 props: list[str] = [] 

103 if file_val: 

104 props.append(f"file={file_val}") 

105 if line_val and line_val != "-": 

106 props.append(f"line={line_val}") 

107 if col_val and col_val != "-": 

108 props.append(f"col={col_val}") 

109 

110 # Title: "tool(CODE)" or just "tool" 

111 title_parts: list[str] = [] 

112 if tool_name: 

113 title_parts.append(tool_name) 

114 if code_val: 

115 if title_parts: 

116 title_parts[-1] += f"({code_val})" 

117 else: 

118 title_parts.append(code_val) 

119 if title_parts: 

120 props.append(f"title={title_parts[0]}") 

121 

122 props_str = ",".join(props) 

123 escaped_msg = _escape(message_val) 

124 

125 if props_str: 

126 lines.append(f"::{level} {props_str}::{escaped_msg}") 

127 else: 

128 lines.append(f"::{level}::{escaped_msg}") 

129 

130 return "\n".join(lines)