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
« 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/>.
20from argparse import Namespace
21import sys
22import os.path
23import subprocess
24import re
25from typing import Optional
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
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\]$"
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)
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
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 )
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 )
102 def _run_impl(self, options: Namespace) -> int:
103 options = self._argument_parser.parse_args()
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")
118 clang_tidy_path = os.path.expanduser(options.clang_tidy)
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 )
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
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)
149 subprocess_args += file_list
151 rv = subprocess.run(
152 subprocess_args,
153 stdout=subprocess.PIPE,
154 stderr=subprocess.PIPE,
155 encoding="UTF-8",
156 check=False,
157 )
159 if rv.returncode != 0:
160 print(rv.stdout)
161 print()
162 print(rv.stderr)
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
173 db = {}
174 implementation_builder = ImplementationBuilder()
176 for line in rv.stdout.splitlines():
177 if not line.endswith("[lobster-tracing]"):
178 continue
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
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
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
209 print("could not parse line")
210 print(">", line)
211 return 1
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}")
218 else:
219 lobster_write(sys.stdout, Implementation, "lobster_cpp", db.values())
220 print()
222 return 0
225def main() -> int:
226 return CppTool().run()