Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
30 commits
Select commit Hold shift + click to select a range
21dc586
build: remove unmaintained `cross-spawn` package.
yCodeTech Oct 1, 2025
eba6c33
remove: the redundant `encoding` and `tty` spawn options from sniffer.
yCodeTech Oct 2, 2025
a30b23f
fix: `std*` possibly being null TS errors by checking if they're truthy.
yCodeTech Oct 2, 2025
359ddc5
Improve Error Reporting (#171)
yCodeTech Oct 2, 2025
facb4cb
fix: prevent the process from hanging if we can't write to `stdin`.
yCodeTech Oct 2, 2025
e10798c
refactor: fixer's `determineNodeError` function and move into utils.
yCodeTech Oct 4, 2025
bc5980e
fix: remove unused import
yCodeTech Oct 4, 2025
5993f33
refactor: extract platform check into new dedicated `isWin` function.
yCodeTech Oct 10, 2025
7b62982
feat: add Windows ENOENT error handling for fixer and sniffer processes
yCodeTech Oct 10, 2025
6e40824
fix: ts error `error TS2315: Type 'Buffer' is not generic.`
yCodeTech Oct 10, 2025
7e64111
refactor: organise utility files into a new directory and renaming files
yCodeTech Oct 12, 2025
6e3d7a9
refactor: `getArgs` function in sniffer and fixer into 1 helper function
yCodeTech Oct 12, 2025
2368fb5
fix: ts error `error TS2304: Cannot find name 'TextDocument'.`
yCodeTech Oct 12, 2025
1374d6c
refactor: integrate path normalization and joining in settings resolver.
yCodeTech Oct 24, 2025
6d9a74d
refactor: `getArgs` to pass the `fileName` as `filePath` arg.
yCodeTech Oct 25, 2025
ac2e9b3
refactor: remove double quotes from the args in the `getArgs` function.
yCodeTech Oct 25, 2025
5cdab03
fix: ENOENT handling to use original command info instead of `parsed`.
yCodeTech Oct 25, 2025
789f2ca
feat: add `parseArgs` helper function for command line argument handling
yCodeTech Oct 29, 2025
9ff2043
feat: add validation for standards, blocking command injection attacks.
yCodeTech Oct 30, 2025
3287939
refactor: `additionalArgument` filtering into new `validateAdditional…
yCodeTech Oct 31, 2025
b16e5be
refactor: `validate` settings function for DRYer code and warn the user.
yCodeTech Nov 9, 2025
c89a626
feat: validate the arguments against a whitelist.
yCodeTech Nov 10, 2025
5bcfec4
fix: the arguments whitelist docblock types.
yCodeTech Nov 10, 2025
8b43573
fix: the import path resolution.
yCodeTech Nov 10, 2025
8c79ebe
fix: Node 24 spawn deprecation of passing args as array when using shell
yCodeTech Nov 11, 2025
d55b7ec
refactor: user messages in `validateAdditionalArguments` to be clearer.
yCodeTech Nov 12, 2025
a3e13e7
fix: the `ts-watch` script to use the proper `--watch` flag
yCodeTech Nov 12, 2025
c1cce59
fix: `isStandardValid` function to check if it's an allowed xml ruleset.
yCodeTech Nov 12, 2025
2642308
fix: warnings should only appear if args were invalid.
yCodeTech Nov 13, 2025
f852ec0
fix: `activateSniffer` shouldn't be checking if sniffer enabled or not.
yCodeTech Nov 15, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 1 addition & 3 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -152,14 +152,12 @@
"lint": "eslint src --ext ts",
"pretest": "npm run compile && npm run lint",
"test": "vscode-test",
"ts-watch": "tsc -watch -p ./"
"ts-watch": "tsc --watch -p ./"
},
"dependencies": {
"cross-spawn": "^7.0.3",
"lodash": "^4.17.21"
},
"devDependencies": {
"@types/cross-spawn": "^6.0.6",
"@types/lodash": "^4.14.202",
"@types/mocha": "^10.0.6",
"@types/node": "^20.11.20",
Expand Down
2 changes: 1 addition & 1 deletion src/extension.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import { activateFixer, registerFixerAsDocumentProvider } from './fixer';
import { disposeLogger, logger } from './logger';
import { loadSettings } from './settings';
import { activateSniffer, disposeSniffer } from './sniffer';
import { getExtensionInfo, setExtensionInfo } from './utils';
import { getExtensionInfo, setExtensionInfo } from './utils/helpers';

/**
* Activate Extension
Expand Down
216 changes: 116 additions & 100 deletions src/fixer.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
import spawn from 'cross-spawn';
import { SpawnSyncOptions } from 'node:child_process';
import { spawnSync, SpawnSyncOptions } from 'node:child_process';
import {
ConfigurationChangeEvent,
Disposable,
Expand All @@ -16,6 +15,13 @@ import { Settings } from './interfaces/settings';
import { logger } from './logger';
import { createStandardsPathResolver } from './resolvers/standards-path-resolver';
import { loadSettings } from './settings';
import { addWindowsEnoentError } from './utils/error-handling/windows-enoent-error';
import {
constructCommandString,
determineNodeError,
getArgs,
parseArgs,
} from './utils/helpers';

let settingsCache: Settings;

Expand All @@ -30,58 +36,12 @@ const getSettings = async () => {
* Load Configuration from editor
*/
const reloadSettings = async (event: ConfigurationChangeEvent) => {
if (
!event.affectsConfiguration('phpsab') &&
!event.affectsConfiguration('editor.formatOnSaveTimeout')
) {
if (!event.affectsConfiguration('phpsab')) {
return;
}
settingsCache = await loadSettings();
};

/**
* Build the arguments needed to execute fixer
* @param fileName
* @param standard
*/
const getArgs = (
document: TextDocument,
standard: string,
additionalArguments: string[],
) => {
// Process linting paths.
let filePath = document.fileName;

let args = [];
args.push('-q');

/**
* Important Note as explained in PR #155:
*
* For the fixer to work properly, we don't add `shell: true` to spawn.sync's options,
* so spawn runs with the default of `shell: false`. This is important because when spawn runs on
* Windows with the default it automatically escapes the command and values, including
* surrounding them in double quotes (" ").
*
* So we don't need to add double quotes around the values for the `--standard` and `--stdin-path`
* options, otherwise the values will get double the amount of quotes and errors will occur.
*
* e.g. ["ERROR" - 10:33:56 PM] ERROR: the ""d:\Name\projects\my project\phpcs.xml"" coding
* standard is not installed. The installed coding standards are MySource, PEAR, PSR1, PSR2,
* PSR12, Squiz, Zend and JPSR12.
*
* The sniffer is different, it needs to be surrounded by double quotes.
*/

if (standard !== '') {
args.push(`--standard=${standard}`);
}
args.push(`--stdin-path=${filePath}`);
args = args.concat(additionalArguments);
args.push('-');
return args;
};

/**
* Get the document range
* @param document TextDocument
Expand Down Expand Up @@ -117,32 +77,34 @@ const format = async (document: TextDocument, fullDocument: boolean) => {

if (resourceConf.fixerEnable === false) {
window.showInformationMessage(
'Fixer is disable for this workspace or PHPCBF was not found for this workspace.',
'Fixer is disabled for this workspace or PHPCBF was not found for this workspace.',
);
return '';
}
logger.startTimer('Fixer');

const additionalArguments = resourceConf.fixerArguments.filter((arg) => {
if (
arg.indexOf('--standard') === -1 &&
arg.indexOf('--stdin-path') === -1 &&
arg !== '-q' &&
arg !== '-'
) {
return true;
}
// setup and spawn fixer process

return false;
});
let standard: string;

// setup and spawn fixer process
const standard = await createStandardsPathResolver(
document,
resourceConf,
).resolve();
try {
standard = await createStandardsPathResolver(
document,
resourceConf,
).resolve();
} catch (error: any) {
window.showErrorMessage(error.message, 'OK');
logger.error(error.message);

return '';
}

const lintArgs = getArgs(document, standard, additionalArguments);
const lintArgs = getArgs(
document.fileName,
standard,
resourceConf.fixerArguments,
'fixer',
);

let fileText = document.getText();

Expand All @@ -154,14 +116,49 @@ const format = async (document: TextDocument, fullDocument: boolean) => {
env: process.env,
encoding: 'utf8',
input: fileText,
// Required to prevent EINVAL errors when spawning .bat files on Windows.
// https://github.com/valeryan/vscode-phpsab/issues/128
// https://github.com/nodejs/node/issues/52554
shell: true,
};

logger.info(
`FIXER COMMAND: ${resourceConf.executablePathCBF} ${lintArgs.join(' ')}`,
);
const CBFExecutable = resourceConf.executablePathCBF;
const parsedArgs = parseArgs(lintArgs);

const command = constructCommandString(CBFExecutable, parsedArgs);

logger.info(`FIXER COMMAND: ${command}`);

const fixer = spawn.sync(resourceConf.executablePathCBF, lintArgs, options);
const fixer = spawnSync(command, options);

const exitcode = fixer.status;
const stdout = fixer.stdout.toString();
const stderr = fixer.stderr.toString();

// Set the original command information (not parsed) for Windows ENOENT error handling
const originalCommand = {
commandPath: CBFExecutable,
args: lintArgs,
};

const nodeError =
(fixer.error as ConsoleError) ||
addWindowsEnoentError(fixer, originalCommand, 'spawnSync');

logger.info(`FIXER EXIT CODE: ${exitcode}`);

// We only log STDOUT if it starts with ERROR (3.x versions of phpcbf output "ERROR"
// messages to stdout), as otherwise it could be the whole file contents,
// which clutters the log when debugging.
//
// This will be removed once we require phpcbf 4.x, which outputs errors to STDERR.
if (stdout && stdout.startsWith('ERROR')) {
logger.info(`FIXER STDOUT: ${stdout.trim()}`);
}

if (stderr) {
logger.error(`FIXER STDERR: ${stderr.trim()}`);
}

let fixed = stdout;

Expand All @@ -176,6 +173,8 @@ const format = async (document: TextDocument, fullDocument: boolean) => {
let error: string = '';
let result: string = '';
let message: string = 'No fixable errors were found.';
let errorMsg: string = '';
let extraLoggerMsg: string = '';

/**
* fixer exit codes:
Expand All @@ -184,38 +183,29 @@ const format = async (document: TextDocument, fullDocument: boolean) => {
* Exit code 2 is used to indicate that FIXER failed to fix some of the fixable errors it found
* Exit code 3 is used for general script execution errors
*/
switch (fixer.status) {
switch (exitcode) {
case null: {
// deal with some special case errors
error = 'A General Execution error occurred.';

if (fixer.error === undefined) {
break;
}
const execError: ConsoleError = fixer.error;
if (execError.code === 'ETIMEDOUT') {
error = 'FIXER: Formatting the document timed out.';
if (!nodeError) {
return '';
}

if (execError.code === 'ENOENT') {
error = `FIXER: ${execError.message}. executablePath not found.`;
}
break;
}
case 0: {
if (settings.debug) {
window.showInformationMessage(message);
}
// Deal with Node errors.
// Destructure the returned object and assign to variables.
({ errorMsg, extraLoggerMsg } = determineNodeError(nodeError, 'fixer'));
error += errorMsg;

break;
}
case 1: {
if (fixed.length > 0 && fixed !== fileText) {
result = fixed;
message = 'All fixable errors were fixed correctly.';
}

if (settings.debug) {
window.showInformationMessage(message);
// If Node errors.
else if (nodeError) {
// Destructure the returned object and assign to variables.
({ errorMsg, extraLoggerMsg } = determineNodeError(nodeError, 'fixer'));
error += errorMsg;
}

break;
Expand All @@ -225,24 +215,48 @@ const format = async (document: TextDocument, fullDocument: boolean) => {
result = fixed;
message = 'FIXER failed to fix some of the fixable errors.';
}

if (settings.debug) {
window.showInformationMessage(message);
// If Node errors.
else if (nodeError) {
// Destructure the returned object and assign to variables.
({ errorMsg, extraLoggerMsg } = determineNodeError(nodeError, 'fixer'));
error += errorMsg;
}

break;
}
default:
error = errors[fixer.status];
// A PHPCBF error occurred.
error = errors[exitcode];
if (fixed.length > 0) {
error += '\n' + fixed + '\n';
}
logger.error(fixed);
// Other errors.
else {
// If Node errors.
if (nodeError) {
// Destructure the returned object and assign to variables.
({ errorMsg, extraLoggerMsg } = determineNodeError(
nodeError,
'fixer',
));
error += errorMsg;
}
// If no specific error is found, return a generic fatal error.
else {
error += 'FATAL: Unknown error occurred.';
}
}
}

logger.endTimer('Fixer');

window.showInformationMessage(message);

if (error !== '') {
logger.error(`${error}${extraLoggerMsg}`);
return Promise.reject(error);
} else {
logger.info(`FIXER MESSAGE: ${message}`);
}

return result;
Expand Down Expand Up @@ -276,13 +290,15 @@ export const registerFixerAsDocumentProvider = (
format(document, isFullDocument)
.then((text) => {
if (text.length > 0) {
// Edit the document with the fixes.
return resolve([new TextEdit(fullRange, text)]);
} else {
throw new Error('PHPCBF returned an empty document');
// Nothing to fix.
return resolve([]);
}
})
.catch((err) => {
window.showErrorMessage(err);
window.showErrorMessage(err, 'OK');
return reject(err);
});
});
Expand Down
Loading
Loading