[Fix] cleanup
This commit is contained in:
Executable
+149
@@ -0,0 +1,149 @@
|
||||
#!/usr/bin/env python3
|
||||
"""PostToolUse hook: format an edited file with the right formatter.
|
||||
|
||||
Reads the hook JSON from stdin, extracts the edited file path, and runs a
|
||||
language-appropriate formatter -- but, to avoid imposing a style on a project
|
||||
that never asked for it, opinionated formatters (stylua, ruff, prettier, taplo)
|
||||
only run when the project opts in via a config file discoverable by walking up
|
||||
from the edited file. Canonical single-style toolchains run on sight: gofmt and
|
||||
rustfmt on their go.mod / Cargo.toml marker; fish_indent and shfmt whenever the
|
||||
tool itself is installed (shell and fish have one de-facto style, no config).
|
||||
|
||||
Best-effort: always exits 0 so a format hiccup never blocks an edit. jq is not
|
||||
guaranteed on the host, hence python for the stdin JSON parse.
|
||||
"""
|
||||
import json
|
||||
import os
|
||||
import shutil
|
||||
import subprocess
|
||||
import sys
|
||||
|
||||
|
||||
def find_up(start, names):
|
||||
"""Nearest ancestor dir (incl. the file's own) containing any of `names`."""
|
||||
d = os.path.dirname(os.path.abspath(start))
|
||||
while True:
|
||||
if any(os.path.exists(os.path.join(d, n)) for n in names):
|
||||
return d
|
||||
parent = os.path.dirname(d)
|
||||
if parent == d:
|
||||
return None
|
||||
d = parent
|
||||
|
||||
|
||||
def has_toml_section(start, filename, section):
|
||||
"""True if the nearest ancestor `filename` contains `section`."""
|
||||
d = find_up(start, [filename])
|
||||
if not d:
|
||||
return False
|
||||
try:
|
||||
with open(os.path.join(d, filename), errors="ignore") as fh:
|
||||
return section in fh.read()
|
||||
except Exception:
|
||||
return False
|
||||
|
||||
|
||||
def stylua_cmd(path):
|
||||
exe = os.path.expanduser("~/.local/share/nvim/mason/bin/stylua")
|
||||
if not os.access(exe, os.X_OK):
|
||||
exe = shutil.which("stylua")
|
||||
if exe and find_up(path, [".stylua.toml", "stylua.toml"]):
|
||||
return [exe, path]
|
||||
return None
|
||||
|
||||
|
||||
def ruff_cmd(path):
|
||||
exe = shutil.which("ruff")
|
||||
if exe and (find_up(path, ["ruff.toml", ".ruff.toml"])
|
||||
or has_toml_section(path, "pyproject.toml", "[tool.ruff")):
|
||||
return [exe, "format", path]
|
||||
return None
|
||||
|
||||
|
||||
_PRETTIER_CFGS = [
|
||||
".prettierrc", ".prettierrc.json", ".prettierrc.yaml", ".prettierrc.yml",
|
||||
".prettierrc.json5", ".prettierrc.js", ".prettierrc.cjs", ".prettierrc.mjs",
|
||||
".prettierrc.toml", "prettier.config.js", "prettier.config.cjs",
|
||||
"prettier.config.mjs",
|
||||
]
|
||||
|
||||
|
||||
def prettier_cmd(path):
|
||||
exe = shutil.which("prettier")
|
||||
if exe and find_up(path, _PRETTIER_CFGS):
|
||||
return [exe, "--write", path]
|
||||
return None
|
||||
|
||||
|
||||
def gofmt_cmd(path):
|
||||
exe = shutil.which("gofmt")
|
||||
if exe and find_up(path, ["go.mod"]): # one canonical style
|
||||
return [exe, "-w", path]
|
||||
return None
|
||||
|
||||
|
||||
def rustfmt_cmd(path):
|
||||
exe = shutil.which("rustfmt")
|
||||
if exe and find_up(path, ["Cargo.toml", "rustfmt.toml", ".rustfmt.toml"]):
|
||||
return [exe, path]
|
||||
return None
|
||||
|
||||
|
||||
def fish_cmd(path):
|
||||
exe = shutil.which("fish_indent") # ships with fish, one canonical style
|
||||
return [exe, "-w", path] if exe else None
|
||||
|
||||
|
||||
def shfmt_cmd(path):
|
||||
exe = shutil.which("shfmt") # de-facto shell formatter, sane defaults
|
||||
return [exe, "-w", path] if exe else None
|
||||
|
||||
|
||||
def taplo_cmd(path):
|
||||
exe = shutil.which("taplo")
|
||||
# opt-in: TOML reflow/align is intrusive, so only where the project asked.
|
||||
if exe and find_up(path, ["taplo.toml", ".taplo.toml"]):
|
||||
return [exe, "format", path]
|
||||
return None
|
||||
|
||||
|
||||
HANDLERS = {
|
||||
".lua": stylua_cmd,
|
||||
".py": ruff_cmd,
|
||||
".js": prettier_cmd, ".jsx": prettier_cmd, ".mjs": prettier_cmd,
|
||||
".cjs": prettier_cmd, ".ts": prettier_cmd, ".tsx": prettier_cmd,
|
||||
".css": prettier_cmd, ".scss": prettier_cmd, ".less": prettier_cmd,
|
||||
".html": prettier_cmd, ".vue": prettier_cmd, ".json": prettier_cmd,
|
||||
".md": prettier_cmd, ".yaml": prettier_cmd, ".yml": prettier_cmd,
|
||||
".go": gofmt_cmd,
|
||||
".rs": rustfmt_cmd,
|
||||
".fish": fish_cmd,
|
||||
".sh": shfmt_cmd, ".bash": shfmt_cmd,
|
||||
".toml": taplo_cmd,
|
||||
}
|
||||
|
||||
|
||||
def main():
|
||||
try:
|
||||
data = json.load(sys.stdin)
|
||||
except Exception:
|
||||
return
|
||||
ti = data.get("tool_input") or {}
|
||||
tr = data.get("tool_response") or {}
|
||||
path = ti.get("file_path") or tr.get("filePath") or ""
|
||||
if not path or not os.path.isfile(path):
|
||||
return
|
||||
handler = HANDLERS.get(os.path.splitext(path)[1].lower())
|
||||
if not handler:
|
||||
return
|
||||
cmd = handler(path)
|
||||
if not cmd:
|
||||
return
|
||||
try:
|
||||
subprocess.run(cmd, timeout=15, capture_output=True)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
Reference in New Issue
Block a user