Coverage for lobster/tools/cpp/cpp.py: 0%

103 statements  

« prev     ^ index     » next       coverage.py v7.10.2, created at 2025-08-06 09:51 +0000

1#!/usr/bin/env python3 

2# 

3# lobster_cpp - Extract C/C++ tracing tags for LOBSTER 

4# Copyright (C) 2023-2025 Bayerische Motoren Werke Aktiengesellschaft (BMW AG) 

5# 

6# This program is free software: you can redistribute it and/or modify 

7# it under the terms of the GNU Affero General Public License as 

8# published by the Free Software Foundation, either version 3 of the 

9# License, or (at your option) any later version. 

10# 

11# This program is distributed in the hope that it will be useful, but 

12# WITHOUT ANY WARRANTY; without even the implied warranty of 

13# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU 

14# Affero General Public License for more details. 

15# 

16# You should have received a copy of the GNU Affero General Public 

17# License along with this program. If not, see 

18# <https://www.gnu.org/licenses/>. 

19 

20from argparse import Namespace 

21import sys 

22import os.path 

23import subprocess 

24import re 

25from typing import Optional 

26 

27from lobster.items import Tracing_Tag, Implementation 

28from lobster.io import lobster_write 

29from lobster.tools.cpp.implementation_builder import ImplementationBuilder 

30from lobster.meta_data_tool_base import MetaDataToolBase 

31 

32FILE_LINE_PATTERN = r"(.*):(\d+):\d+:" 

33KIND_PATTERN = r"(function|main function|method)" 

34NAME_PATTERN = r"([a-zA-Z0-9_:~^()&\s<>,=*!+-.|[\]/\"]+)" 

35PREFIX = "^%s warning:" % FILE_LINE_PATTERN 

36SUFFIX = r"\[lobster-tracing\]$" 

37 

38RE_NOTAGS = (PREFIX + " " + 

39 r"%s %s has no tracing tags" % (KIND_PATTERN, 

40 NAME_PATTERN) + 

41 " " + SUFFIX) 

42RE_TAGS = (PREFIX + " " + 

43 r"%s %s traces to +([^,\n]+(?:\s*,\s*[^,\n]+)*) +" % (KIND_PATTERN, 

44 NAME_PATTERN) + 

45 SUFFIX) 

46RE_JUST = (PREFIX + " " + 

47 r"%s %s exempt from tracing: +(.+) +" % (KIND_PATTERN, 

48 NAME_PATTERN) + 

49 SUFFIX) 

50 

51 

52def extract_clang_finding_name(line: str) -> Optional[str]: 

53 """extracts the name of the clang finding from the end of the line""" 

54 if line.endswith("]") and ("[" in line): 

55 return line.split("[")[-1].rstrip("]") 

56 return None 

57 

58 

59class CppTool(MetaDataToolBase): 

60 def __init__(self): 

61 super().__init__( 

62 name = "cpp", 

63 description = "Extract tracing tags from C++ using clang-tidy", 

64 official = True, 

65 ) 

66 

67 self._argument_parser.add_argument( 

68 "files", 

69 nargs="+", 

70 metavar="FILE|DIR", 

71 ) 

72 self._argument_parser.add_argument( 

73 "--clang-tidy", 

74 default="clang-tidy", 

75 metavar="FILE", 

76 help=("use the specified clang-tidy; by default we" 

77 " pick the one on PATH"), 

78 ) 

79 self._argument_parser.add_argument( 

80 "--compile-commands", 

81 metavar="FILE", 

82 default=None, 

83 help=("Path to the compile command database for all targets for " 

84 "'clang tidy', or none to use the default behavior of " 

85 "'clang tidy'. This is equal to calling 'clang tidy' " 

86 "directly with its '-p' option. Refer to its official " 

87 "documentation for more details."), 

88 ) 

89 self._argument_parser.add_argument( 

90 "--skip-clang-errors", 

91 default=[], 

92 nargs="*", 

93 metavar="FINDINGS", 

94 help="List of all clang-tidy errors to ignore.", 

95 ) 

96 self._argument_parser.add_argument( 

97 "--out", 

98 default=None, 

99 help="write output to this file; otherwise output to to stdout", 

100 ) 

101 

102 def _run_impl(self, options: Namespace) -> int: 

103 options = self._argument_parser.parse_args() 

104 

105 file_list = [] 

106 for item in options.files: 

107 if os.path.isfile(item): 

108 file_list.append(item) 

109 elif os.path.isdir(item): 

110 for path, _, files in os.walk(item): 

111 for filename in files: 

112 _, ext = os.path.splitext(filename) 

113 if ext in (".cpp", ".cc", ".c", ".h"): 

114 file_list.append(os.path.join(path, filename)) 

115 else: 

116 self._argument_parser.error(f"{item} is not a file or directory") 

117 

118 clang_tidy_path = os.path.expanduser(options.clang_tidy) 

119 

120 # Test if the clang-tidy can be used 

121 rv = subprocess.run( 

122 [ 

123 clang_tidy_path, 

124 "-checks=-*,lobster-tracing", 

125 "--list-checks", 

126 ], 

127 stdout=subprocess.PIPE, 

128 stderr=subprocess.PIPE, 

129 encoding="UTF-8", 

130 check=False, 

131 ) 

132 

133 if "No checks enabled." in rv.stderr: 

134 print("The provided clang-tidy does include the lobster-tracing check") 

135 print("> Please build from " 

136 "https://github.com/bmw-software-engineering/llvm-project") 

137 print("> Or make sure to provide the " 

138 "correct binary using the --clang-tidy flag") 

139 return 1 

140 

141 subprocess_args = [ 

142 clang_tidy_path, 

143 "-checks=-*,lobster-tracing", 

144 ] 

145 if options.compile_commands: 

146 subprocess_args.append("-p") 

147 subprocess_args.append(options.compile_commands) 

148 

149 subprocess_args += file_list 

150 

151 rv = subprocess.run( 

152 subprocess_args, 

153 stdout=subprocess.PIPE, 

154 stderr=subprocess.PIPE, 

155 encoding="UTF-8", 

156 check=False, 

157 ) 

158 

159 if rv.returncode != 0: 

160 print(rv.stdout) 

161 print() 

162 print(rv.stderr) 

163 

164 for line in rv.stdout.splitlines(): 

165 if "error: " in line: 

166 clang_error = extract_clang_finding_name(line) 

167 if clang_error and (clang_error in options.skip_clang_errors): 

168 print(f"Ignoring clang-tidy error {clang_error}") 

169 else: 

170 print("Found clang-tidy error: ", clang_error) 

171 return 1 

172 

173 db = {} 

174 implementation_builder = ImplementationBuilder() 

175 

176 for line in rv.stdout.splitlines(): 

177 if not line.endswith("[lobster-tracing]"): 

178 continue 

179 

180 match = re.match(RE_NOTAGS, line) 

181 if match: 

182 impl = implementation_builder.from_match(match) 

183 assert impl.tag.key() not in db 

184 db[impl.tag.key()] = impl 

185 continue 

186 

187 match = re.match(RE_JUST, line) 

188 if match: 

189 impl = implementation_builder.from_match_if_new(db, match) 

190 impl.just_up.append( 

191 match.group(implementation_builder.REASON_GROUP_NUM), 

192 ) 

193 continue 

194 

195 match = re.match(RE_TAGS, line) 

196 if match: 

197 impl = implementation_builder.from_match_if_new(db, match) 

198 all_tags = match.group(implementation_builder.REFERENCE_GROUP_NUM) 

199 for tag in re.split(r"[, ]+", all_tags.strip()): 

200 if tag: 

201 impl.add_tracing_target( 

202 Tracing_Tag( 

203 "req", 

204 tag, 

205 ), 

206 ) 

207 continue 

208 

209 print("could not parse line") 

210 print(">", line) 

211 return 1 

212 

213 if options.out: 

214 with open(options.out, "w", encoding="UTF-8") as fd: 

215 lobster_write(fd, Implementation, "lobster_cpp", db.values()) 

216 print(f"Written output for {len(db)} items to {options.out}") 

217 

218 else: 

219 lobster_write(sys.stdout, Implementation, "lobster_cpp", db.values()) 

220 print() 

221 

222 return 0 

223 

224 

225def main() -> int: 

226 return CppTool().run()