-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathchecker.py
More file actions
264 lines (218 loc) · 7.95 KB
/
checker.py
File metadata and controls
264 lines (218 loc) · 7.95 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
#!/usr/bin/env -S python3
import sys, os, subprocess, shutil
from typing import List, Tuple
import difflib
# ANSI color codes
class Colors:
RESET = '\033[0m'
BOLD = '\033[1m'
DIM = '\033[2m'
# Colors
RED = '\033[91m'
GREEN = '\033[92m'
YELLOW = '\033[93m'
BLUE = '\033[94m'
MAGENTA = '\033[95m'
CYAN = '\033[96m'
WHITE = '\033[97m'
GRAY = '\033[90m'
@staticmethod
def disable():
"""Disable colors for non-TTY environments"""
for attr in dir(Colors):
if not attr.startswith('_') and attr.isupper() and attr not in ['disable']:
setattr(Colors, attr, '')
# Disable colors if not outputting to a terminal
if not sys.stdout.isatty():
Colors.disable()
# Modern Unicode icons
ICON_SUCCESS = f"{Colors.GREEN}●{Colors.RESET}"
ICON_ERROR = f"{Colors.RED}●{Colors.RESET}"
ICON_WARNING = f"{Colors.YELLOW}●{Colors.RESET}"
ICON_SKIP = f"{Colors.GRAY}○{Colors.RESET}"
ICON_SCAN = f"{Colors.CYAN}⊙{Colors.RESET}"
# Parse command line arguments
args = sys.argv[1:]
if '--' in args:
idx = args.index('--')
ROOTS = args[:idx] or ['.']
EXTRA_FLAGS = args[idx+1:]
else:
ROOTS = args or ['.']
EXTRA_FLAGS = []
def which(name):
return shutil.which(name)
def run(cmd):
p = subprocess.run(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True)
return p.returncode, p.stdout + p.stderr
def print_header():
"""Print a minimal, modern header"""
print(f"\n{Colors.BOLD}{Colors.CYAN}C/C++ Code Quality{Colors.RESET} {Colors.DIM}({len(find_sources(ROOTS))} files){Colors.RESET}")
def print_section(tool_name: str):
"""Print a clean section header for each tool"""
# Pad tool name to align results
padded = f"{tool_name:<15}"
print(f" {Colors.DIM}{padded}{Colors.RESET}", end=" ")
def print_success():
"""Print inline success indicator"""
print(f"{ICON_SUCCESS}")
def print_error(count: int):
"""Print inline error indicator with count"""
print(f"{ICON_ERROR} {Colors.RED}{count} issue{'s' if count != 1 else ''}{Colors.RESET}")
def print_skip():
"""Print inline skip indicator"""
print(f"{ICON_SKIP} {Colors.DIM}skipped{Colors.RESET}")
def print_file_issue(filename: str, details: str):
"""Print file-specific issues with minimal formatting"""
print(f"\n {Colors.DIM}├─{Colors.RESET} {Colors.WHITE}{filename}{Colors.RESET}")
for line in details.split('\n'):
if line.strip():
print(f" {Colors.DIM}│{Colors.RESET} {Colors.DIM}{line}{Colors.RESET}")
print(f" {Colors.DIM}╰─{Colors.RESET}")
def print_summary(total_checks: int, passed: int, failed: int, skipped: int):
"""Print a compact summary"""
status = f"{Colors.GREEN}{passed}{Colors.RESET}"
if failed > 0:
status += f" {Colors.DIM}/{Colors.RESET} {Colors.RED}{failed}{Colors.RESET}"
if skipped > 0:
status += f" {Colors.DIM}/{Colors.RESET} {Colors.GRAY}{skipped}{Colors.RESET}"
print(f"{status} {Colors.DIM}passed/failed/skipped{Colors.RESET}\n")
def find_sources(roots: List[str]) -> List[str]:
"""Find C/C++ source files in the given roots"""
exts = ('.c', '.cpp', '.cc', '.cxx', '.h', '.hpp', '.hh')
files = []
for root in roots:
if os.path.isfile(root):
if root.lower().endswith(exts):
files.append(root)
continue
for dp, _, fnames in os.walk(root):
for f in fnames:
if f.lower().endswith(exts):
files.append(os.path.join(dp, f))
return files
def check_clang_format(files: List[str]) -> Tuple[int, int]:
"""Check code formatting with clang-format"""
print_section("clang-format")
if not which('clang-format'):
print_skip()
return 0, 1
issues = []
for f in files:
rc, out = run(['clang-format', '--output-replacements-xml', f])
if '<replacement ' in out:
issues.append(f)
if issues:
print_error(len(issues))
for f in issues:
# produce a unified diff between original and clang-format output
try:
with open(f, 'r', encoding='utf-8', errors='replace') as fh:
original = fh.read().splitlines()
except Exception as e:
print_file_issue(f, f"Could not read file: {e}")
continue
rc2, formatted = run(['clang-format', f])
formatted_lines = formatted.splitlines()
diff_lines = list(difflib.unified_diff(original, formatted_lines,
fromfile=f,
tofile=f + " (formatted)",
lineterm=''))
if diff_lines:
print_file_issue(f, "\n".join(diff_lines[:20])) # Limit diff output
return 1, 0
print_success()
return 0, 0
def check_clang_tidy(files: List[str]) -> Tuple[int, int]:
"""Check code with clang-tidy"""
print_section("clang-tidy")
if not which('clang-tidy'):
print_skip()
return 0, 1
msgs = []
flags = ['--'] + EXTRA_FLAGS if EXTRA_FLAGS else []
for f in files:
rc, out = run(['clang-tidy', f] + flags)
if rc != 0 or out.strip():
msgs.append((f, out.strip()))
if msgs:
print_error(len(msgs))
for f, details in msgs:
# Truncate long outputs
lines = details.split('\n')
truncated = '\n'.join(lines[:15])
if len(lines) > 15:
truncated += f"\n... ({len(lines) - 15} more lines)"
print_file_issue(f, truncated)
return 1, 0
print_success()
return 0, 0
def check_cppcheck(roots: List[str]) -> Tuple[int, int]:
"""Check code with cppcheck"""
print_section("cppcheck")
if not which('cppcheck'):
print_skip()
return 0, 1
cmd = ['cppcheck', '--enable=all', '--quiet'] + roots
rc, out = run(cmd)
if rc != 0:
print_error(1)
print_file_issue("cppcheck", out.strip())
return 1, 0
print_success()
return 0, 0
def check_cpplint(files: List[str]) -> Tuple[int, int]:
"""Check code style with cpplint"""
print_section("cpplint")
if not which('cpplint'):
print_skip()
return 0, 1
msgs = []
for f in files:
rc, out = run(['cpplint', '--filter=-build/include_subdir, -legal/copyright', f])
filtered_lines = [
line for line in out.splitlines()
if line.strip() and not line.strip().startswith('Done processing')
]
filtered = "\n".join(filtered_lines).strip()
if rc != 0 or filtered:
msgs.append((f, filtered if filtered else out.strip()))
if msgs:
print_error(len(msgs))
for f, details in msgs:
print_file_issue(f, details)
return 1, 0
print_success()
return 0, 0
def main():
# Find source files
files = find_sources(ROOTS)
if not files:
print(f"\n{ICON_ERROR} {Colors.RED}No C/C++ source files found{Colors.RESET}\n")
return 1
print_header()
# Run all checks
checks = [
(check_clang_format, files),
(check_clang_tidy, files),
(check_cppcheck, ROOTS),
(check_cpplint, files),
]
failed = 0
passed = 0
skipped = 0
for check_func, check_arg in checks:
rc, skip = check_func(check_arg)
if skip:
skipped += 1
elif rc == 0:
passed += 1
else:
failed += 1
# Print summary
print()
total = len(checks)
print_summary(total, passed, failed, skipped)
return 1 if failed > 0 else 0
if __name__ == "__main__":
sys.exit(main())