|
import subprocess |
|
import sys |
|
from pathlib import Path |
|
from clang.cindex import CursorKind, Index |
|
|
|
# argv チェック |
|
if len(sys.argv) == 1: |
|
print(f"Usage: python3 {sys.argv[0]} <filename> [compile options]") |
|
sys.exit(1) |
|
|
|
filename = Path(sys.argv[1]).absolute() |
|
user_options = sys.argv[2:] |
|
|
|
# コンパイルできることを確認 |
|
subprocess.check_call(["clang++", "-fsyntax-only", "-std=gnu++2b"] + user_options + [str(filename)]) |
|
|
|
# ファイルを読み込む |
|
with open(filename, "rb") as f: |
|
program = f.read().splitlines(keepends=True) |
|
|
|
#line を削除 |
|
program = [line for line in program if not line.startswith(b"#line")] |
|
|
|
#include をコメントアウト |
|
prefix = b"//cleanup_Sq9XpeQ6" |
|
for i, line in enumerate(program): |
|
if line.startswith(b"#include"): |
|
program[i] = prefix + line |
|
|
|
# clang でマクロを展開 |
|
p = subprocess.run(["clang++", "-std=gnu++23", "-w", "-E", "-C", "-P", "-xc++", "-"] + user_options, input=b"".join(program), stdout=subprocess.PIPE) |
|
program = p.stdout.splitlines(keepends=True) |
|
|
|
#include を元に戻す |
|
for i, line in enumerate(program): |
|
if line.startswith(prefix): |
|
program[i] = line.removeprefix(prefix) |
|
|
|
# GCC でコメントを削除 |
|
p = subprocess.run(["gcc", "-std=gnu++23", "-w", "-fpreprocessed", "-E", "-P", "-xc++", "-"] + user_options, input=b"".join(program), stdout=subprocess.PIPE) |
|
program = p.stdout.splitlines(keepends=True) |
|
|
|
# bytearray に変換 |
|
program = [bytearray(line) for line in program] |
|
|
|
# 指定された範囲を空白に変換 |
|
def erase(extent): |
|
l1 = extent.start.line - 1 |
|
c1 = extent.start.column - 1 |
|
l2 = extent.end.line - 1 |
|
c2 = extent.end.column - 1 |
|
while l1 < l2 or (l1 == l2 and c1 < c2): |
|
if program[l1][c1] == ord('\n'): |
|
c1 = 0 |
|
l1 += 1 |
|
continue |
|
program[l1][c1] = ord(' ') |
|
c1 += 1 |
|
|
|
options = ( |
|
f""" |
|
-resource-dir /Applications/Xcode.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/lib/clang/16 |
|
-isysroot /Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX.sdk |
|
-I /usr/local/include |
|
-I /Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX.sdk/usr/include/c++/v1 |
|
-I /Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX.sdk/usr/local/include |
|
-I /Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX.sdk/usr/include |
|
-I /Applications/Xcode.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/lib/clang/16/include |
|
-I /Applications/Xcode.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/include |
|
-I {filename.parent} |
|
-fcxx-exceptions |
|
-fsyntax-only |
|
-std=gnu++2b |
|
-Wno-everything |
|
-Wunused |
|
-Wunused-template |
|
-Wunused-macros |
|
-Wunused-member-function |
|
""".split() + user_options |
|
) |
|
program_fix = b"".join(program) |
|
def cleanup(): |
|
# ファイルをパースして構文木を取得 |
|
index = Index.create() |
|
tu = index.parse(path="main.cpp", args=options, unsaved_files=[("main.cpp", "".join(str(line, 'utf-8') for line in program))]) |
|
warnings = list(tu.diagnostics) |
|
if not warnings: |
|
return False |
|
|
|
# 未使用のマクロ,変数,関数を列挙する |
|
unused_macros = set() |
|
unused_variables = set() |
|
unused_functions = set() |
|
|
|
for w in warnings: |
|
match w.option: |
|
case "-Wunused-macros": |
|
unused_macros.add(w.location.line) |
|
case "-Wunused-variable" | "-Wunused-const-variable" | "-Wunused-local-typedefs" | "-Wunused-private-field": # VAR_DECL | TYPE_ALIAS_DECL | FIELD_DECL |
|
unused_variables.add((w.location.line, w.location.column)) |
|
case "-Wunused-function" | "-Wunneeded-internal-declaration" | "-Wunused-template" | "-Wunused-member-function" | "-Wunneeded-member-function": # FUNCTION_DECL | FUNCTION_TEMPLATE | CXX_METHOD |
|
unused_functions.add((w.location.line, w.location.column)) |
|
|
|
# 構文木を DFS, 未使用の関数・変数を削除する |
|
def dfs(node, depth=0): |
|
if node.location.file is not None and node.location.file.name != "main.cpp": |
|
return |
|
# print(f"{node.location.file} {node.extent.start.line:03}:{node.extent.start.column:02}-{node.extent.end.line:03}:{node.extent.end.column:02}\tdep={depth}\t{str(node.kind)[11:]:20}\t{node.displayname}", file=sys.stderr) |
|
|
|
match node.kind: |
|
case CursorKind.FUNCTION_DECL | CursorKind.FUNCTION_TEMPLATE | CursorKind.CXX_METHOD | CursorKind.CONSTRUCTOR | CursorKind.DESTRUCTOR: |
|
loc = node.location |
|
if (loc.line, loc.column) in unused_functions: |
|
# print(f"[UNUSED FUNCTION] {node.extent.start.line:>3}:{node.extent.start.column:<3}-{node.extent.end.line:>4}:{node.extent.end.column:<3}\t{node.displayname}", file=sys.stderr) |
|
erase(node.extent) |
|
return |
|
case CursorKind.VAR_DECL | CursorKind.TYPE_ALIAS_DECL | CursorKind.FIELD_DECL: |
|
loc = node.location |
|
if (loc.line, loc.column) in unused_variables: |
|
# print(f"[UNUSED VARIABLE] {node.extent.start.line:>3}:{node.extent.start.column:<3}-{node.extent.end.line:>4}:{node.extent.end.column:<3}\t{node.displayname}", file=sys.stderr) |
|
erase(node.extent) |
|
return |
|
for child in node.get_children(): |
|
dfs(child, depth + 1) |
|
|
|
dfs(tu.cursor) |
|
|
|
# 未使用のマクロを削除する |
|
for l in unused_macros: |
|
l -= 1 |
|
line = program[l] |
|
while len(line) >= 2 and line[-2] == ord('\\'): |
|
line[:] = b"\n" |
|
l += 1 |
|
if l >= len(program): |
|
break |
|
line = program[l] |
|
line[:] = b"\n" |
|
|
|
# 標準入力から変更後のプログラムを入れ,コンパイルできるか確認 |
|
program_ = b"".join(program) |
|
p = subprocess.run(["clang++", "-fsyntax-only", "-std=gnu++2b", "-x", "c++", "-"] + user_options, input=program_, stdout=subprocess.PIPE, stderr=subprocess.PIPE) |
|
print(p.stderr.decode(), file=sys.stderr) |
|
if p.returncode: |
|
return False |
|
global program_fix |
|
if program_fix == program_: |
|
return False |
|
program_fix = program_ |
|
return True |
|
|
|
# 5 回まで cleanup を試行 |
|
for _ in range(5): |
|
if not cleanup(): |
|
break |
|
|
|
# clang-format で整形する |
|
p = subprocess.Popen(["clang-format"], stdin=subprocess.PIPE, stdout=subprocess.PIPE) |
|
stdout, _ = p.communicate(input=b"".join(program)) |
|
|
|
# ";" のみの行を削除 |
|
program = stdout.splitlines(keepends=True) |
|
program = [line for line in program if line != b";\n"] |
|
|
|
# 結果を出力 |
|
sys.stdout.buffer.write(b"".join(program)) |