mirror of
https://github.com/affaan-m/ECC.git
synced 2026-09-18 15:50:25 +02:00
fix(hooks): support Windows linter paths and ESLint 9 (#3076)
pre-bash-commit-quality spawned Windows .cmd/.bat linters unquoted, so a spaced path failed, and passed --format compact, which ESLint 9 removed (#3075). Batch executables now run through cmd.exe with each argument carried in an env token and quoted, with quote, NUL, CR and LF rejected before spawn; non-batch Windows and POSIX paths keep direct argv spawn with shell false. ESLint uses its bundled default formatter, present on 8, 9 and 10. Regression tests cover the batch, non-batch and POSIX branches and the formatter change. Independent exact-head review passed with no P0/P1; CI 44/44 at the head.
This commit is contained in:
@@ -259,20 +259,83 @@ function resolveCommand(command) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const LINTER_TIMEOUT_MS = 30000;
|
||||
const UNSAFE_CMD_TOKEN = /["\0\r\n]/;
|
||||
const CMD_TOKEN_ENV_PREFIX = 'ECC_LINTER_TOKEN_';
|
||||
|
||||
function validateCmdToken(value) {
|
||||
const token = String(value);
|
||||
if (UNSAFE_CMD_TOKEN.test(token)) {
|
||||
throw new Error(`Unsafe character in Windows linter argument: ${JSON.stringify(token)}`);
|
||||
}
|
||||
return token;
|
||||
}
|
||||
|
||||
function getLinterInvocation(command, args, platform = process.platform) {
|
||||
const useCmd = platform === 'win32' && /\.(?:cmd|bat)$/i.test(command);
|
||||
|
||||
if (useCmd) {
|
||||
const environment = { ...process.env };
|
||||
for (const name of Object.keys(environment)) {
|
||||
if (name.toUpperCase().startsWith(CMD_TOKEN_ENV_PREFIX)) {
|
||||
delete environment[name];
|
||||
}
|
||||
}
|
||||
|
||||
// Keep untrusted values out of cmd.exe source. Percent expansion is
|
||||
// non-recursive, so percent signs introduced by these environment values
|
||||
// stay literal. Disabling delayed expansion likewise preserves exclamation
|
||||
// marks. Quotes and line controls remain invalid because they could escape
|
||||
// the quoted token boundary or create another command line.
|
||||
const tokenReferences = [command, ...args].map((value, index) => {
|
||||
const name = `${CMD_TOKEN_ENV_PREFIX}${index}`;
|
||||
environment[name] = validateCmdToken(value);
|
||||
return `"%${name}%"`;
|
||||
});
|
||||
const commandLine = tokenReferences.join(' ');
|
||||
return {
|
||||
command: process.env.ComSpec || process.env.COMSPEC || 'cmd.exe',
|
||||
args: ['/d', '/v:off', '/s', '/c', `"${commandLine}"`],
|
||||
options: {
|
||||
encoding: 'utf8',
|
||||
stdio: ['pipe', 'pipe', 'pipe'],
|
||||
timeout: LINTER_TIMEOUT_MS,
|
||||
shell: false,
|
||||
windowsVerbatimArguments: true,
|
||||
env: environment
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
command,
|
||||
args,
|
||||
options: {
|
||||
encoding: 'utf8',
|
||||
stdio: ['pipe', 'pipe', 'pipe'],
|
||||
timeout: LINTER_TIMEOUT_MS,
|
||||
shell: false
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
function runLinterCommand(command, args) {
|
||||
const useShell = process.platform === 'win32' && /\.(?:cmd|bat)$/i.test(command);
|
||||
return spawnSync(command, args, {
|
||||
encoding: 'utf8',
|
||||
stdio: ['pipe', 'pipe', 'pipe'],
|
||||
timeout: 30000,
|
||||
shell: useShell
|
||||
});
|
||||
try {
|
||||
const invocation = getLinterInvocation(command, args);
|
||||
return spawnSync(invocation.command, invocation.args, invocation.options);
|
||||
} catch (error) {
|
||||
return { status: null, stdout: '', stderr: '', error };
|
||||
}
|
||||
}
|
||||
|
||||
function commandOutput(result) {
|
||||
return result.stdout || result.stderr || result.error?.message || '';
|
||||
}
|
||||
|
||||
function golintSucceeded(result) {
|
||||
return result.status === 0 && !result.error && (!result.stdout || result.stdout.trim() === '');
|
||||
}
|
||||
|
||||
/**
|
||||
* Run linter on staged files
|
||||
* @param {string[]} files
|
||||
@@ -294,7 +357,7 @@ function runLinter(files) {
|
||||
const eslintBin = process.platform === 'win32' ? 'eslint.cmd' : 'eslint';
|
||||
const eslintPath = path.join(process.cwd(), 'node_modules', '.bin', eslintBin);
|
||||
if (fs.existsSync(eslintPath)) {
|
||||
const result = runLinterCommand(eslintPath, ['--format', 'compact', ...jsFiles]);
|
||||
const result = runLinterCommand(eslintPath, jsFiles);
|
||||
results.eslint = {
|
||||
success: result.status === 0,
|
||||
output: commandOutput(result)
|
||||
@@ -329,7 +392,7 @@ function runLinter(files) {
|
||||
} else {
|
||||
const result = runLinterCommand(golintPath, goFiles);
|
||||
results.golint = {
|
||||
success: !result.stdout || result.stdout.trim() === '',
|
||||
success: golintSucceeded(result),
|
||||
output: commandOutput(result)
|
||||
};
|
||||
}
|
||||
@@ -481,4 +544,13 @@ if (require.main === module) {
|
||||
});
|
||||
}
|
||||
|
||||
module.exports = { run, evaluate, validateCommitMessage, findFileIssues, isPlaceholderSecret };
|
||||
module.exports = {
|
||||
run,
|
||||
evaluate,
|
||||
validateCommitMessage,
|
||||
findFileIssues,
|
||||
isPlaceholderSecret,
|
||||
getLinterInvocation,
|
||||
golintSucceeded,
|
||||
runLinter
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user