lint: canonicalize clang-tidy diagnostic paths and filter to repo include/lib in normalize-clang-tidy.py

v3.0-lint
Rene Cannao 1 month ago
parent f5d77bb095
commit 656d310996

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

@ -0,0 +1 @@
107605 warnings generated.

File diff suppressed because it is too large Load Diff

@ -0,0 +1,3 @@
/usr/include/openssl/macros.h:139:0: error: #error "The requested API level higher than the configured API compatibility level" [preprocessorErrorDirective]
# error "The requested API level higher than the configured API compatibility level"
^

@ -1,70 +1,135 @@
#!/usr/bin/env python3
import sys
import yaml
import os
"""
Normalizer for clang-tidy outputs.
normalize-clang-tidy.py
Normalize clang-tidy outputs (export-fixes YAML or textual stderr/stdout)
and emit lines in the form:
Behavior:
- If passed an export-fixes YAML (the old behavior), parse it and emit normalized lines.
- If passed a textual clang-tidy stderr/stdout file (from running clang-tidy without export-fixes), parse those diagnostics as well.
<absolute-canonical-file>:<line>: <check> - <message>
Emitted line format:
<file>:<line>: <check> - <message>
Only diagnostics whose canonical file path is under <repo_root>/include/ or
<repo_root>/lib/ are emitted. This prevents diagnostics originating from
deps/ (e.g., include/../deps/...) from slipping through.
"""
import sys
import os
import re
import yaml
import subprocess
def get_repo_root():
try:
out = subprocess.check_output(["git", "rev-parse", "--show-toplevel"], stderr=subprocess.DEVNULL)
return os.path.realpath(out.decode().strip())
except Exception:
return os.path.realpath(os.getcwd())
if len(sys.argv) != 2:
print("Usage: normalize-clang-tidy.py <export-fixes.yaml-or-raw-output>")
sys.exit(2)
path = sys.argv[1]
if not os.path.exists(path):
# No diagnostics
sys.exit(0)
content = open(path, 'r', errors='ignore').read()
diagnostics = set()
# Try YAML first
try:
data = yaml.safe_load(content)
if isinstance(data, dict) and 'Diagnostics' in data:
for diag in data.get('Diagnostics', []):
msg = diag.get('DiagnosticMessage', {})
file = msg.get('FilePath', '<unknown>')
offset = msg.get('FileOffset', 0)
# Map offset to line if possible
try:
with open(file, 'rb') as fh:
b = fh.read()
line_no = b[:offset].count(b"\n") + 1
except Exception:
line_no = 0
check = diag.get('CheckName', '')
message = msg.get('Message', '').strip()
# Only emit diagnostics that are within the repository include/ or lib/
# paths to avoid noise from deps/ headers. This mirrors the header
# filter behavior used when running clang-tidy.
if '/include/' in file or '/lib/' in file:
diagnostics.add(f"{file}:{line_no}: {check} - {message}")
else:
raise Exception("not yaml diagnostics")
except Exception:
# Fallback: parse clang-tidy textual output lines
# Typical clang-tidy message format:
# /path/to/file:123:45: warning: message [check-name]
# We capture file, line, message and check-name
for line in content.splitlines():
m = re.match(r"(?P<file>[^:]+):(?P<line>\d+):(?P<col>\d+:)?\s*(?P<kind>warning|error|note):?\s*(?P<msg>.*)\s*\[(?P<check>[^\]]+)\]$", line)
if m:
file = m.group('file')
def canonical_path(path, repo_root):
if not path:
return None
# Ignore placeholders like <built-in> or <unknown>
if path.startswith("<") and path.endswith(">"):
return None
if os.path.isabs(path):
return os.path.realpath(path)
# If relative, interpret relative to repo root
return os.path.realpath(os.path.join(repo_root, path))
def is_in_repo_include_or_lib(cpath, repo_root):
if not cpath:
return False
inc = os.path.realpath(os.path.join(repo_root, "include"))
lib = os.path.realpath(os.path.join(repo_root, "lib"))
try:
# os.path.commonpath raises ValueError if paths are on different drives
common_inc = os.path.commonpath([cpath, inc])
if common_inc == inc:
return True
except Exception:
pass
try:
common_lib = os.path.commonpath([cpath, lib])
if common_lib == lib:
return True
except Exception:
pass
return False
def offset_to_line(cpath, offset):
try:
with open(cpath, 'rb') as fh:
data = fh.read()
# offset is a byte offset; count newlines before it
return data[:offset].count(b"\n") + 1
except Exception:
return 0
def main():
if len(sys.argv) != 2:
print("Usage: normalize-clang-tidy.py <export-fixes.yaml-or-raw-output>")
return 2
path = sys.argv[1]
if not os.path.exists(path):
# Nothing to normalize
return 0
repo_root = get_repo_root()
content = open(path, 'r', errors='ignore').read()
diagnostics = set()
# Try parsing as export-fixes YAML first
try:
data = yaml.safe_load(content)
if isinstance(data, dict) and 'Diagnostics' in data:
for diag in data.get('Diagnostics', []):
msg = diag.get('DiagnosticMessage', {}) or {}
raw_file = msg.get('FilePath')
cpath = canonical_path(raw_file, repo_root)
if not cpath:
continue
if not is_in_repo_include_or_lib(cpath, repo_root):
continue
offset = msg.get('FileOffset', None)
if offset is None:
line_no = msg.get('FileLine', 0) or 0
else:
line_no = offset_to_line(cpath, int(offset))
check = diag.get('CheckName') or diag.get('DiagnosticName') or ''
message = (msg.get('Message') or '').strip()
diagnostics.add(f"{cpath}:{line_no}: {check} - {message}")
else:
# Not the expected YAML structure; fall back to textual parsing
raise ValueError("not yaml diagnostics")
except Exception:
# Fallback: parse clang-tidy textual output lines
# Typical clang-tidy message format:
# /path/to/file:123:45: warning: message [check-name]
pat = re.compile(r"(?P<file>[^:]+):(?P<line>\d+):(?P<col>\d+:)?\s*(?P<kind>warning|error|note):?\s*(?P<msg>.*)\s*\[(?P<check>[^\]]+)\]$")
for line in content.splitlines():
m = pat.match(line)
if not m:
continue
raw_file = m.group('file')
cpath = canonical_path(raw_file, repo_root)
if not cpath:
continue
if not is_in_repo_include_or_lib(cpath, repo_root):
continue
line_no = m.group('line')
check = m.group('check')
message = m.group('msg').strip()
diagnostics.add(f"{file}:{line_no}: {check} - {message}")
diagnostics.add(f"{cpath}:{line_no}: {check} - {message}")
for l in sorted(diagnostics):
print(l)
for l in sorted(diagnostics):
print(l)
if __name__ == '__main__':
sys.exit(main())

Loading…
Cancel
Save