-
Notifications
You must be signed in to change notification settings - Fork 564
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
script that transforms clang-tidy YAML output to GitHub annotations
- Loading branch information
Showing
1 changed file
with
120 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,120 @@ | ||
#!/usr/bin/env python3 | ||
|
||
""" | ||
(C) 2023 Jack Lloyd | ||
2023 René Meusel - Rohde & Schwarz Cybersecurity | ||
Botan is released under the Simplified BSD License (see license.txt) | ||
""" | ||
|
||
import argparse | ||
import glob | ||
import os | ||
import sys | ||
|
||
import yaml | ||
|
||
class FileLocation: | ||
def __init__(self, line : int, column : int, endline : int | None = None, endcolumn : int | None = None): | ||
self.line = line | ||
self.column = column | ||
self.endline = endline | ||
self.endcolumn = endcolumn | ||
|
||
def has_range(self): | ||
return self.endline is not None and self.endcolumn is not None | ||
|
||
|
||
class Diagnostic: | ||
@staticmethod | ||
def read_diagnostics_from(yaml_file : str): | ||
with open(yaml_file, encoding="utf-8") as yml: | ||
fixes = yaml.load(yml, Loader=yaml.FullLoader) | ||
if "Diagnostics" not in fixes: | ||
raise RuntimeError(f"No Diagnostics found in {yaml_file}") | ||
return [Diagnostic(diag) for diag in fixes["Diagnostics"]] | ||
|
||
|
||
def __map_file_path(self, file_path, base_path): # pylint: disable=unused-argument | ||
if file_path.endswith(".h"): | ||
raise RuntimeError(f"Header file {file_path} cannot be handled") | ||
# TODO: try to map include files (residing in build/include) onto their | ||
# origin path in src/lib etc. | ||
return file_path | ||
|
||
|
||
def __map_file_offset(self, offset : int) -> tuple[int, int]: | ||
""" For self.file determine the (line, column) given a byte offset """ | ||
with open(self.file, encoding="utf-8") as srcfile: | ||
readoffset = 0 | ||
lineoffset = 0 | ||
for l in srcfile.readlines(): | ||
readoffset += len(l) | ||
lineoffset += 1 | ||
if readoffset >= offset: | ||
coloffset = offset - readoffset + len(l) | ||
return (lineoffset, coloffset) | ||
raise RuntimeError(f"FileOffset {offset} out of range for {self.file}") | ||
|
||
|
||
def __map_file_location(self, msg): | ||
""" For self.file determine the specified error range of the message """ | ||
location = self.__map_file_offset(msg["FileOffset"]) | ||
if "Ranges" in msg and len(msg["Ranges"]) == 1: | ||
the_range = msg ["Ranges"][0] | ||
if the_range["FilePath"] == msg["FilePath"] and the_range["FileOffset"] == msg["FileOffset"]: | ||
endlocation = self.__map_file_offset(the_range["FileOffset"] + the_range["Length"]) | ||
return FileLocation(*location, *endlocation) | ||
return FileLocation(*location) | ||
|
||
|
||
def __init__(self, yaml_diag): | ||
self.name = yaml_diag["DiagnosticName"] | ||
msg = yaml_diag["DiagnosticMessage"] | ||
self.message = msg["Message"] | ||
self.file = self.__map_file_path(msg["FilePath"], yaml_diag["BuildDirectory"]) | ||
self.level = yaml_diag["Level"] | ||
self.location = self.__map_file_location(msg) | ||
|
||
|
||
def render_as_github_annotations(diagnostics : list[Diagnostic]): | ||
def map_level(level : str) -> str: | ||
if level == "Error": | ||
return "error" | ||
elif level == "Warning": | ||
return "warning" | ||
else: | ||
return "notice" # fallback: likely never used | ||
|
||
def render_location(location : FileLocation) -> str: | ||
linemarkers = [f"line={location.line}"] | ||
colmarkers = [f"col={location.column}"] | ||
if location.has_range(): | ||
linemarkers += [f"endLine={location.endline}"] | ||
colmarkers += [f"endColumn={location.endcolumn}"] | ||
return ','.join(linemarkers + colmarkers) | ||
|
||
def render_message(msg: str) -> str: | ||
return msg.replace("\n", " - ") | ||
|
||
for d in diagnostics: | ||
lvl = map_level(d.level) | ||
location = render_location(d.location) | ||
msg = render_message(d.message) | ||
print(f"::{lvl} file={d.file},{location}::{msg}") | ||
|
||
|
||
def main(): | ||
parser = argparse.ArgumentParser(prog="ClangTidy Decoder", | ||
description="Parses ClangTidy YAML output and emits GitHub Workflow commands") | ||
parser.add_argument('directory') | ||
args = parser.parse_args() | ||
|
||
diagnostics = [] | ||
for yml in glob.glob(os.path.join(args.directory, "*.yml")): | ||
diagnostics.extend(Diagnostic.read_diagnostics_from(yml)) | ||
|
||
render_as_github_annotations(diagnostics) | ||
|
||
if __name__ == '__main__': | ||
sys.exit(main()) |