Coverage for /builds/svesch/cikit/src/cikit/macros/ruff.py: 100%

54 statements  

« prev     ^ index     » next       coverage.py v7.14.1, created at 2026-10-09 14:17 +0000

1# SPDX-License-Identifier: GPL-3.0-or-later 

2"""Ruff macro helpers for cikit. 

3 

4This module executes Ruff lint checks, stores lint metrics in cikit data, 

5generates badges and converts JSON lint reports into generated Sphinx 

6RST pages. 

7 

8Author: 

9 Sven Schlender 

10Date: 

11 2026-05-28 

12""" 

13 

14import json 

15import logging 

16from pathlib import Path 

17from tempfile import TemporaryDirectory 

18 

19from cikit.badge import write_badge 

20from cikit.command import Command, run_command 

21from cikit.artifacts import Artifact, ArtifactType 

22from cikit.env import write_file 

23from cikit.errors import CiToolsError 

24from cikit.lint_to_rst import LintIssue, read_lint_issues, write_lint_rst 

25from cikit.rst_tools import RstDocument, timeline_artifacts_block 

26 

27from .common import resolve_name_label 

28 

29 

30LOGGER = logging.getLogger(__name__) 

31RST_TOC_DEPTH = 2 

32 

33 

34def get_ruff_check_file_path(env) -> Path: 

35 """Return the registered Ruff JSON report path. 

36 

37 Args: 

38 env: Resolved CI run environment. 

39 

40 Returns: 

41 Absolute path to the Ruff JSON report artifact. 

42 """ 

43 return env.get_run_artifact("ruff.json", ArtifactType.LINT).path 

44 

45 

46def ruff(env, paths): 

47 """Run Ruff lint checks. 

48 

49 The generated JSON report is registered as a lint artifact and the number 

50 of violations is stored in the current run data context. 

51 

52 Args: 

53 env: Resolved CI run environment. 

54 paths: Paths passed to Ruff. 

55 

56 Raises: 

57 CiToolsError: If a lint path does not exist. 

58 """ 

59 for path in paths: 

60 if not Path(path).exists(): 

61 raise CiToolsError(f"Path '{path}' to lint does not exist!") 

62 

63 cmd = Command("ruff", use_exec_tool=True) 

64 cmd.add_subcommand("check", ["check", "--output-format=json", "--exit-zero"]) 

65 argv = env.argv(cmd, "check", extra_args=paths) 

66 result = run_command(argv, check_exit_code=True, live_output=True, cwd=env["PROJECT_ROOT"]) 

67 

68 violations = json.loads(result.stdout or "[]") 

69 ruff_count = len(violations) 

70 

71 with TemporaryDirectory() as temp_dir: 

72 json_file = Path(temp_dir) / "ruff.json" 

73 write_file(json_file, result.stdout or "") 

74 env.copy_artifact(json_file, ArtifactType.LINT) 

75 

76 env.data_set("violations", ruff_count) 

77 

78 

79def gitlab_code_quality(env, paths): 

80 """Generate GitLab Code Quality output for Ruff. 

81 

82 Args: 

83 env: Resolved CI run environment. 

84 paths: Paths passed to Ruff. 

85 """ 

86 with TemporaryDirectory() as temp_dir: 

87 json_file = Path(temp_dir) / "ruff-codequality.json" 

88 

89 cmd = Command("ruff", use_exec_tool=True) 

90 cmd.add_subcommand("check_gitlab", ["check", "--output-format=gitlab", "-o", str(json_file), 

91 "--exit-zero"]) 

92 argv = env.argv(cmd, "check_gitlab", extra_args=paths) 

93 run_command(argv, check_exit_code=True, live_output=True, cwd=env["PROJECT_ROOT"]) 

94 

95 env.copy_artifact(json_file, ArtifactType.LINT) 

96 

97 

98def badge(env, name: str = None, label: str = None) -> Artifact: 

99 """Generate the Ruff violations badge. 

100 

101 Args: 

102 env: Resolved CI run environment. 

103 name: Basename of the badge file. If omitted, the run name is used. 

104 label: Label of the badge. If omitted, the run name is used. 

105 

106 Returns: 

107 Created and registered badge artifact. 

108 """ 

109 name, label = resolve_name_label(env, name, label) 

110 return write_badge(env=env, name=name, label=label, value=env.data.get("violations", 0), 

111 thresholds={1: "green", 3: "yellow", 5: "orange", 10: "red", 9999: "red"}) 

112 

113 

114def _ruff_issue_from_dict(entry: dict) -> LintIssue: 

115 """Convert one Ruff JSON entry into a LintIssue. 

116 

117 Args: 

118 entry: Ruff JSON issue entry. 

119 

120 Returns: 

121 Converted lint issue. 

122 """ 

123 return LintIssue(code=entry.get("code") or entry.get("rule") or "unknown", 

124 file_path=Path(entry.get("filename")).resolve(), 

125 line=entry.get("noqa_row") or entry.get("location", {}).get("row") or 0, 

126 message=entry.get("message") or "", 

127 severity="warning") 

128 

129 

130def write_rst(env, name: str = None, report_title: str = None, filter_code: str = None) -> Artifact: 

131 """Create an RST report from Ruff JSON output. 

132 

133 Args: 

134 env: Resolved CI run environment. 

135 name: Output file basename without extension. If omitted, the run name 

136 is used. 

137 report_title: Report title. If omitted, it is derived from the current 

138 violation count. 

139 filter_code: Optional Ruff rule code filter. 

140 

141 Returns: 

142 Created and registered RST artifact. 

143 """ 

144 if name is None: 

145 name = env.name 

146 

147 if report_title is None: 

148 report_title = f"Ruff Report (Violations {env.data.get('violations')})" 

149 

150 issues = read_lint_issues(get_ruff_check_file_path(env), _ruff_issue_from_dict) 

151 

152 document = RstDocument(report_title, toc_depth=RST_TOC_DEPTH) 

153 

154 if not filter_code: 

155 document.add_block(timeline_artifacts_block(env.get_run_artifacts(ArtifactType.TIMELINE))) 

156 

157 return write_lint_rst(env, document, issues, name, filter_code)