clang-format-launcher
Version:
clang-format launcher with simple configuration
400 lines (352 loc) • 10.2 kB
text/typescript
/**
* Licensed under the MIT License.
*/
import async from "async";
import path from "path";
import fs from "fs";
import {
SpawnSyncOptions,
StdioOptions,
execSync,
spawn,
spawnSync,
} from "child_process";
const config: Config = {
includeEndsWith: [],
excludePathContains: [],
excludePathEndsWith: [],
excludePathStartsWith: [],
style: "--style=file",
clangFormatBinPath: "",
doGitStatusCheck: false,
};
const resolveClangFormat = () => {
if (!config.clangFormatBinPath || config.clangFormatBinPath.length === 0) {
// @ts-ignore (no typings for clang-format)
const lib = require("clang-format");
return lib.getNativeBinary();
}
return config.clangFormatBinPath;
};
let verbose = false;
const VERIFY_FLAG = "-verify";
const RAW_FLAG = "-raw";
const CONFIG_FILE = "clang.format.json";
const VERBOSE_FLAG = "--verbose";
const HELP_FLAG = "--help";
const VERSION_FLAG = "--version";
const LAUNCHER = "clang-format-launcher";
interface Config {
includeEndsWith?: string[];
excludePathContains?: string[];
excludePathEndsWith?: string[];
excludePathStartsWith?: string[];
style?: string;
clangFormatBinPath?: string;
doGitStatusCheck?: boolean;
}
const resolveGitRoot = () => {
const result = git(["rev-parse", "--show-toplevel"], {
cwd: process.cwd(),
});
if (result.success) {
return result.stdout;
} else {
console.warn("Can't get gitroot: " + result.stderr);
process.exit(1);
}
};
let gitRoot: string = undefined;
const hasPackageJsonLauncherKey = (packagePath: string) => {
const content = fs.readFileSync(packagePath, "utf8");
try {
return LAUNCHER in JSON.parse(content);
} catch {
// If package is not a valid JSON
return false;
}
};
const resolveConfigPath = (cwd: string) => {
const candidates = [
path.resolve(cwd, "package.json"),
path.resolve(cwd, CONFIG_FILE),
path.resolve(__dirname, "../", CONFIG_FILE),
];
return candidates.filter((name) => {
if (fs.existsSync(name)) {
if (path.basename(name) === "package.json") {
return hasPackageJsonLauncherKey(name);
}
return true;
}
return false;
})[0];
};
function main() {
const verify = process.argv.indexOf(VERIFY_FLAG) !== -1;
verbose = process.argv.indexOf(VERBOSE_FLAG) !== -1;
let useRaw = process.argv.indexOf(RAW_FLAG) !== -1;
useRaw = useRaw || process.argv.indexOf(VERSION_FLAG) !== -1;
const help = process.argv.indexOf(HELP_FLAG) !== -1;
useRaw = useRaw || help;
if (help) {
printHelp();
}
let args = process.argv
.slice(2)
.filter((_) => _ !== VERIFY_FLAG && _ !== RAW_FLAG);
if (!useRaw) {
gitRoot = resolveGitRoot();
console.log("gitRoot: " + gitRoot);
loadConfig();
if (verify) {
args = ["-Werror", "--dry-run", ...args];
} else {
args = ["-Werror", "-i", ...args];
}
if (config.style) {
args = [config.style, ...args];
}
}
const handleDone = (checkGitStatus: boolean) => (error?: Error) => {
if (error) {
process.stdout.write(error.message);
process.exit(3);
}
if (checkGitStatus) {
queryNoOpenFiles();
}
};
// Run clang-format.
try {
// Pass all arguments to clang-format, including e.g. -version etc.
if (useRaw) {
spawnClangFormatRaw(args, handleDone(false), "inherit");
} else {
spawnClangFormat(
args,
handleDone(verify && config.doGitStatusCheck),
"inherit"
);
}
} catch (e) {
process.stdout.write((e as Error).message);
process.exit(1);
}
}
function verboseLog(s: string) {
if (verbose) {
console.log(s);
}
}
function loadConfig() {
const cwd = process.cwd();
const resolvedPath: string = resolveConfigPath(cwd);
verboseLog("Using conf file: " + resolvedPath);
try {
const jsonString = fs.readFileSync(resolvedPath, {
encoding: "utf8",
flag: "r",
});
const json = JSON.parse(jsonString);
let conf: Config = LAUNCHER in json ? json[LAUNCHER] : json;
config.includeEndsWith = conf.includeEndsWith ?? config.includeEndsWith;
config.excludePathContains =
conf.excludePathContains ?? config.excludePathContains;
config.excludePathEndsWith =
conf.excludePathEndsWith ?? config.excludePathEndsWith;
config.excludePathStartsWith =
conf.excludePathStartsWith ?? config.excludePathStartsWith;
config.style = conf.style ?? config.style;
config.clangFormatBinPath =
conf.clangFormatBinPath ?? config.clangFormatBinPath;
config.doGitStatusCheck = conf.doGitStatusCheck ?? config.doGitStatusCheck;
} catch (e) {
console.log("Fail to parse conf file");
console.log((e as Error).message);
process.exit(1);
}
verboseLog("Resolved Config:");
verboseLog(JSON.stringify(config));
}
function queryNoOpenFiles() {
const opened = execSync("git status -s").toString();
if (opened) {
console.error(
"The following files have incorrect formatting or not committed:"
);
console.error(opened);
process.exit(2);
}
}
function errorFromExitCode(exitCode: number) {
return new Error(`clang-format exited with exit code ${exitCode}.`);
}
function git(args: string[], options: SpawnSyncOptions) {
verboseLog("git: " + JSON.stringify(args) + " " + JSON.stringify(options));
const results = spawnSync("git", args, options);
if (results.status === 0) {
return {
stderr: results.stderr.toString().trim(),
stdout: results.stdout.toString().trim(),
success: true,
};
} else {
return {
stderr: results.stderr.toString().trim(),
stdout: results.stdout.toString().trim(),
success: false,
};
}
}
function listAllTrackedFiles(cwd: string) {
const results = git(["ls-tree", "-r", "--name-only", "--full-tree", "HEAD"], {
cwd,
});
if (results.success) {
return results.stdout.split("\n");
}
return [];
}
/**
* Spawn the clang-format binary with given arguments.
*/
function spawnClangFormat(
args: string[],
done: (Error?: any) => void,
stdio: StdioOptions
) {
// WARNING: This function's interface should stay stable across versions for the cross-version
// loading below to work.
let nativeBinary: string;
try {
nativeBinary = resolveClangFormat();
verboseLog("native Clang-format: " + nativeBinary);
} catch (e) {
setImmediate(done.bind(e));
return;
}
let files = listAllTrackedFiles(gitRoot);
// Apply file filters from constants
files = files.filter(
(file) =>
config.includeEndsWith.some((_) => file.endsWith(_)) &&
!config.excludePathContains.some((_) => file.indexOf(_) >= 0) &&
!config.excludePathEndsWith.some((_) => file.endsWith(_)) &&
!config.excludePathStartsWith.some((_) => file.startsWith(_))
);
// split file array into chunks of 30
let i: number;
let j: number;
const chunks = [];
const chunkSize = 30;
for (i = 0, j = files.length; i < j; i += chunkSize) {
chunks.push(files.slice(i, i + chunkSize));
}
// launch a new process for each chunk
async.series<number, Error>(
chunks.map((chunk) => {
return function (callback) {
const clangFormatProcess = spawn(nativeBinary, args.concat(chunk), {
cwd: gitRoot,
stdio: stdio,
});
clangFormatProcess.on("close", (exit) => {
if (exit !== 0) {
callback(errorFromExitCode(exit!));
} else {
callback();
}
});
};
}),
(err) => {
if (err) {
done(err);
return;
}
console.log("\n");
console.log(
`ran clang-format on ${files.length} ${
files.length === 1 ? "file" : "files"
}`
);
done();
}
);
}
/**
* Spawn the clang-format binary with given arguments.
*/
function spawnClangFormatRaw(
args: string[],
done: (any?: Error) => void,
stdio: StdioOptions
) {
// WARNING: This function's interface should stay stable across versions for the cross-version
// loading below to work.
let nativeBinary: string;
try {
nativeBinary = resolveClangFormat();
verboseLog("native Clang-format: " + nativeBinary);
} catch (e) {
setImmediate(done.bind(e));
return;
}
verboseLog("clang-format " + JSON.stringify(args));
const clangFormatProcess = spawn(nativeBinary, args, {
cwd: gitRoot,
stdio: stdio,
});
clangFormatProcess.on("close", (exit) => {
if (exit !== 0) {
done(errorFromExitCode(exit!));
} else {
done();
}
});
}
function printHelp() {
console.log(
`
clang-format-launcher is a clang-format wrapper.
It uses 'git ls-tree' to speed up the file lookup and reduce the noise, then filters the files by the rule which is defined in clang.format.json or package.json.
It looks cwd/package.json, cwd/clang.format.json and finally fallback to node_modules/clang-format-launcher/clang.format.json.
Usage:
npx clang-format-launcher [options] [other options]
Options:
-raw
-verify
npx clang-format-launcher [other options]
equal to 'npx clang-format --style=file -Werror -i [other options] [Files after filter]'
npx clang-format-launcher -verify [other options]
equal to 'npx clang-format --style=file -Werror --dry-run [other options] [Files after filter]'
npx clang-format-launcher -raw [other options]
equal to 'npx clang-format [other options]
npx clang-format-launcher --verbose
npx clang-format-launcher --help
clang.format.json example:
{
"includeEndsWith": [".h",".cpp"],
"excludePathContains": ["/ios/", "/nodejs/", "/android/"],
"excludePathEndsWith": [".g.h",".g.cpp"],
"excludePathStartsWith": [],
"style": "--style=file",
"clangFormatBinPath": "clang-format",
"doGitStatusCheck": true
}
package.json example:
{
"clang-format-launcher": {
"includeEndsWith": [".h",".cpp"],
"excludePathContains": ["/ios/", "/nodejs/", "/android/"],
"excludePathEndsWith": [".g.h",".g.cpp"],
"excludePathStartsWith": [],
"style": "--style=file",
"clangFormatBinPath": ""
}
}
`
);
}
main();