From e13738f090407b99983bf9d573c472dc78c87770 Mon Sep 17 00:00:00 2001 From: wongjn <11310624+wongjn@users.noreply.github.com> Date: Sat, 7 Nov 2020 15:27:29 +0000 Subject: [PATCH 1/9] Pass file extension explicitly to executable --- lib/runner.js | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/lib/runner.js b/lib/runner.js index 6dfcd7b..779dcc8 100644 --- a/lib/runner.js +++ b/lib/runner.js @@ -114,7 +114,12 @@ const createRunner = (token, uri, fullDocument = true) => { ['report', 'json'], ['bootstrap', resolve(__dirname, 'files.php')], ]); - if (uri.scheme === 'file') args.set('stdin-path', uri.fsPath); + if (uri.scheme === 'file') { + args.set('stdin-path', uri.fsPath); + // Use the same logic that is in PHP_CodeSniffer to parse the file's + // extension (see https://github.com/squizlabs/PHP_CodeSniffer/blob/3.5.8/src/Files/File.php#L242). + args.set('extensions', uri.path.split('.').pop() || ''); + } if (standard) args.set('standard', standard); /** @type string[] */ From d28c40480cc5da332a82010109fa447b75cac7de Mon Sep 17 00:00:00 2001 From: wongjn <11310624+wongjn@users.noreply.github.com> Date: Sat, 7 Nov 2020 19:17:48 +0000 Subject: [PATCH 2/9] Refactor test utilities --- test/functional/locations.test.js | 105 ++++++++++++++++++++++++++- test/functional/run.test.js | 26 +------ test/functional/utils.js | 117 ++++++------------------------ 3 files changed, 123 insertions(+), 125 deletions(-) diff --git a/test/functional/locations.test.js b/test/functional/locations.test.js index 583219b..14535a3 100644 --- a/test/functional/locations.test.js +++ b/test/functional/locations.test.js @@ -1,8 +1,105 @@ -const { workspace, Uri } = require('vscode'); -const { sep } = require('path'); -const { testCase, hasGlobalPHPCS } = require('./utils'); +const assert = require('assert'); +const { commands, languages, window, workspace, Uri } = require('vscode'); +const path = require('path'); +const { createFile, writeFile, unlink } = require('fs-extra'); +const { hasGlobalPHPCS } = require('./utils'); const { execPromise, FIXTURES_PATH } = require('../utils'); +/** + * Test case function call. + * + * @param {object} options + * Options for the test case. + * @param {string} options.description + * Description of the suite. + * @param {string} options.content + * The content of the file for validation and before formatting. + * @param {{ row: number, column: number }[]} options.expectedValidationErrors + * Expected errors that should be should be in diagnostics. + * @param {string} options.expectedFormattedResult + * Expected file content after running formatting. + * @param {string} [options.standard] + * The standard to test with. + * @param {Function} [options.testSetup] + * Optional function to run on suiteSetup, with an optional returned function + * to run on teardown. + */ +function testCase({ + description, + content, + expectedValidationErrors, + expectedFormattedResult, + standard, + testSetup, +}) { + const filePath = path.join(FIXTURES_PATH, `index${Math.floor(Math.random() * 3000)}.php`); + const fileUri = Uri.file(filePath); + + suite(description, function () { + // Possible teardown callback. + let tearDown; + + suiteSetup(async function () { + await Promise.all([ + createFile(filePath), + workspace.getConfiguration('phpSniffer', fileUri).update('standard', standard), + ]); + + await writeFile(filePath, content); + if (testSetup) tearDown = await testSetup.call(this, fileUri); + }); + + suiteTeardown(async function () { + await Promise.all([ + workspace.getConfiguration('phpSniffer', fileUri).update('standard', undefined), + unlink(filePath), + ]); + if (tearDown) await tearDown.call(this); + }); + + test('Validation errors are reported', async function () { + const diagnosticsPromise = new Promise((resolve) => { + const subscription = languages.onDidChangeDiagnostics(({ uris }) => { + const list = uris.map((uri) => uri.toString()); + if (list.indexOf(fileUri.toString()) === -1) return; + + const diagnostics = languages.getDiagnostics(fileUri); + if (diagnostics.length === 0) return; + + subscription.dispose(); + resolve(diagnostics); + }); + }); + + workspace.openTextDocument(fileUri); + const diagnostics = await diagnosticsPromise; + + assert.strictEqual( + diagnostics.length, + expectedValidationErrors.length, + 'Correct number of diagnostics are created.', + ); + diagnostics.forEach((diagnostic, i) => { + const { row, column } = expectedValidationErrors[i]; + const { start } = diagnostic.range; + + assert.strictEqual(start.line, row, `Diagnostic ${i + 1} line number is correct`); + assert.strictEqual(start.character, column, `Diagnostic ${i + 1} character position is correct`); + }); + }); + + test('Fixable validation errors are fixed via formatting', async function () { + // Visually open the document so commands can be run on it. + const document = await workspace.openTextDocument(fileUri); + await window.showTextDocument(document); + + await commands.executeCommand('editor.action.formatDocument'); + assert.strictEqual(document.getText(), expectedFormattedResult); + await commands.executeCommand('workbench.action.closeAllEditors'); + }); + }); +} + /** * Runs test cases for two files for preset and a local ruleset. */ @@ -74,7 +171,7 @@ suite('Executable & ruleset locations', function () { await execPromise('composer install --no-dev', { cwd: FIXTURES_PATH }); await workspace .getConfiguration('phpSniffer', Uri.file(FIXTURES_PATH)) - .update('executablesFolder', `vendor${sep}bin${sep}`); + .update('executablesFolder', `vendor${path.sep}bin${path.sep}`); }); suiteTeardown(async function () { diff --git a/test/functional/run.test.js b/test/functional/run.test.js index ad5ab64..46ec405 100644 --- a/test/functional/run.test.js +++ b/test/functional/run.test.js @@ -3,31 +3,7 @@ const path = require('path'); const { commands, languages, Range, window, workspace, Uri } = require('vscode'); const { createFile, writeFile, unlink } = require('fs-extra'); const { execPromise, FIXTURES_PATH } = require('../utils'); - -/** - * Get diagnostics for a file. - * - * @param {Uri} fileUri - * The URI of the file to get diagnostics of. - * @return {Promise} - * Diagnostics for the file. - */ -const getNextDiagnostics = (fileUri) => { - const existingCount = languages.getDiagnostics(fileUri).length; - - return new Promise((resolve) => { - const subscription = languages.onDidChangeDiagnostics(({ uris }) => { - const list = uris.map((uri) => uri.toString()); - if (list.indexOf(fileUri.toString()) === -1) return; - - const diagnostics = languages.getDiagnostics(fileUri); - if (diagnostics.length !== existingCount) { - resolve(diagnostics); - subscription.dispose(); - } - }); - }); -}; +const { getNextDiagnostics } = require('./utils'); /** * Constructs a promise that waits for the given length of time. diff --git a/test/functional/utils.js b/test/functional/utils.js index 2995465..5f9c6d2 100644 --- a/test/functional/utils.js +++ b/test/functional/utils.js @@ -3,11 +3,8 @@ * Utilities for tests. */ -const assert = require('assert'); -const path = require('path'); -const { createFile, writeFile, unlink } = require('fs-extra'); -const { commands, languages, window, workspace, Uri } = require('vscode'); -const { execPromise, FIXTURES_PATH } = require('../utils'); +const { languages } = require('vscode'); +const { execPromise } = require('../utils'); /** * Tests whether there is a global PHPCS on the current machine. @@ -27,98 +24,26 @@ async function hasGlobalPHPCS() { module.exports.hasGlobalPHPCS = hasGlobalPHPCS; /** - * Test case function call. + * Get diagnostics for a file. * - * @param {object} options - * Options for the test case. - * @param {string} options.description - * Description of the suite. - * @param {string} options.content - * The content of the file for validation and before formatting. - * @param {{ row: number, column: number }[]} options.expectedValidationErrors - * Expected errors that should be should be in diagnostics. - * @param {string} options.expectedFormattedResult - * Expected file content after running formatting. - * @param {string} [options.standard] - * The standard to test with. - * @param {Function} [options.testSetup] - * Optional function to run on suiteSetup, with an optional returned function - * to run on teardown. + * @param {import('vscode').Uri} fileUri + * The URI of the file to get diagnostics of. + * @return {Promise} + * Diagnostics for the file. */ -function testCase({ - description, - content, - expectedValidationErrors, - expectedFormattedResult, - standard, - testSetup, -}) { - const filePath = path.join(FIXTURES_PATH, `index${Math.floor(Math.random() * 3000)}.php`); - const fileUri = Uri.file(filePath); - - suite(description, function () { - // Possible teardown callback. - let tearDown; - - suiteSetup(async function () { - await Promise.all([ - createFile(filePath), - workspace.getConfiguration('phpSniffer', fileUri).update('standard', standard), - ]); - - await writeFile(filePath, content); - if (testSetup) tearDown = await testSetup.call(this, fileUri); - }); - - suiteTeardown(async function () { - await Promise.all([ - workspace.getConfiguration('phpSniffer', fileUri).update('standard', undefined), - unlink(filePath), - ]); - if (tearDown) await tearDown.call(this); - }); - - test('Validation errors are reported', async function () { - const diagnosticsPromise = new Promise((resolve) => { - const subscription = languages.onDidChangeDiagnostics(({ uris }) => { - const list = uris.map((uri) => uri.toString()); - if (list.indexOf(fileUri.toString()) === -1) return; - - const diagnostics = languages.getDiagnostics(fileUri); - if (diagnostics.length === 0) return; - - subscription.dispose(); - resolve(diagnostics); - }); - }); - - workspace.openTextDocument(fileUri); - const diagnostics = await diagnosticsPromise; - - assert.strictEqual( - diagnostics.length, - expectedValidationErrors.length, - 'Correct number of diagnostics are created.', - ); - diagnostics.forEach((diagnostic, i) => { - const { row, column } = expectedValidationErrors[i]; - const { start } = diagnostic.range; - - assert.strictEqual(start.line, row, `Diagnostic ${i + 1} line number is correct`); - assert.strictEqual(start.character, column, `Diagnostic ${i + 1} character position is correct`); - }); - }); - - test('Fixable validation errors are fixed via formatting', async function () { - // Visually open the document so commands can be run on it. - const document = await workspace.openTextDocument(fileUri); - await window.showTextDocument(document); - - await commands.executeCommand('editor.action.formatDocument'); - assert.strictEqual(document.getText(), expectedFormattedResult); - await commands.executeCommand('workbench.action.closeAllEditors'); +module.exports.getNextDiagnostics = (fileUri) => { + const existingCount = languages.getDiagnostics(fileUri).length; + + return new Promise((resolve) => { + const subscription = languages.onDidChangeDiagnostics(({ uris }) => { + const list = uris.map((uri) => uri.toString()); + if (list.indexOf(fileUri.toString()) === -1) return; + + const diagnostics = languages.getDiagnostics(fileUri); + if (diagnostics.length !== existingCount) { + resolve(diagnostics); + subscription.dispose(); + } }); }); -} - -module.exports.testCase = testCase; +}; From 686a92091aec99504b86af5a254774f1c26d08f6 Mon Sep 17 00:00:00 2001 From: wongjn <11310624+wongjn@users.noreply.github.com> Date: Sat, 7 Nov 2020 21:59:28 +0000 Subject: [PATCH 3/9] Validate more file types --- lib/validator.js | 39 ++++++++++++++++++++---- package.json | 9 +++++- test/fixtures/css.xml | 4 +++ test/functional/extra-files.test.js | 46 +++++++++++++++++++++++++++++ 4 files changed, 91 insertions(+), 7 deletions(-) create mode 100644 test/fixtures/css.xml create mode 100644 test/functional/extra-files.test.js diff --git a/lib/validator.js b/lib/validator.js index b873210..ea563f8 100644 --- a/lib/validator.js +++ b/lib/validator.js @@ -108,13 +108,40 @@ const onDocumentClose = (diagnostics, tokenManager) => ({ uri }) => { }; /** - * Whether validation is disabled currently with contextual circumstances. + * Returns whether a given document matches the `extraFiles` setting. * - * @return {boolean} True if no validation should run, false otherwise. + * @param {import('vscode').WorkspaceConfiguration} config + * Settings for `phpSniffer`. + * @param {import('vscode').TextDocument} document + * The document to test. + * + * @return {boolean} + * True if document matches, false otherwise. */ -const validationDisabled = () => { - const config = workspace.getConfiguration('phpSniffer'); - return config.get('disableWhenDebugging', false) && !!debug.activeDebugSession; +const matchExtraFiles = (config, document) => { + const selectors = config + .get('extraFiles', []) + .map((pattern) => ({ pattern, scheme: 'file' })); + return languages.match(selectors, document) > 0; +}; + +/** + * Whether validation should run for the given document. + * + * @param {import('vscode').TextDocument} document + * The document to validate. + * + * @return {boolean} + * True if validation should run, false otherwise. + */ +const shouldValidate = (document) => { + const config = workspace.getConfiguration('phpSniffer', document.uri); + + return ( + !document.isClosed + && (document.languageId === 'php' || matchExtraFiles(config, document)) + && (!config.get('disableWhenDebugging', false) || !debug.activeDebugSession) + ); }; /** @@ -129,7 +156,7 @@ const validationDisabled = () => { * The validator function. */ const validateDocument = (diagnostics, tokenManager) => (document) => { - if (document.languageId !== 'php' || document.isClosed || validationDisabled()) { + if (!shouldValidate(document)) { return; } diff --git a/package.json b/package.json index e89258f..fa69b26 100644 --- a/package.json +++ b/package.json @@ -18,7 +18,7 @@ "phpcbf" ], "activationEvents": [ - "onLanguage:php" + "onStartupFinished" ], "main": "./extension", "contributes": { @@ -47,6 +47,13 @@ "minimum": 0, "markdownDescription": "When `phpSniffer.run` is `onType`, this sets the amount of milliseconds the validator will wait after typing has stopped before it will run." }, + "phpSniffer.extraFiles": { + "type": "array", + "uniqueItems": true, + "default": [], + "markdownDescription": "[Glob patterns](https://code.visualstudio.com/api/references/vscode-api#GlobPattern) of extra files to match that this extension should run on. Useful for standards that don't just validate PHP files.", + "items": { "type": "string" } + }, "phpSniffer.executablesFolder": { "scope": "resource", "type": "string", diff --git a/test/fixtures/css.xml b/test/fixtures/css.xml new file mode 100644 index 0000000..4b6f95b --- /dev/null +++ b/test/fixtures/css.xml @@ -0,0 +1,4 @@ + + + + diff --git a/test/functional/extra-files.test.js b/test/functional/extra-files.test.js new file mode 100644 index 0000000..6606d9e --- /dev/null +++ b/test/functional/extra-files.test.js @@ -0,0 +1,46 @@ +const path = require('path'); +const assert = require('assert'); +const { commands, workspace, Uri } = require('vscode'); +const { execPromise, FIXTURES_PATH } = require('../utils'); +const { getNextDiagnostics } = require('./utils'); + +suite('Test `extraFiles` setting', function () { + const fixtureUri = Uri.file(path.join(FIXTURES_PATH, 'style.css')); + const textEncoder = new TextEncoder(); + + suiteSetup(function () { + this.timeout(60000); + + const config = workspace.getConfiguration('phpSniffer', fixtureUri); + const cssConfig = workspace.getConfiguration('css', fixtureUri); + return Promise.all([ + execPromise('composer install --no-dev', { cwd: FIXTURES_PATH }), + workspace.fs.writeFile(fixtureUri, textEncoder.encode('a{margin : 0}')), + config.update('executablesFolder', `vendor${path.sep}bin${path.sep}`), + config.update('standard', './css.xml'), + config.update('extraFiles', ['**/*.css']), + cssConfig.update('validate', false), + ]); + }); + + suiteTeardown(function () { + const config = workspace.getConfiguration('phpSniffer', fixtureUri); + const cssConfig = workspace.getConfiguration('css', fixtureUri); + return Promise.all([ + workspace.fs.delete(fixtureUri), + config.update('executablesFolder', undefined), + config.update('standard', undefined), + config.update('extraFiles', undefined), + cssConfig.update('validate', undefined), + ]); + }); + + teardown(() => commands.executeCommand('workbench.action.closeAllEditors')); + + test('Validation errors reported', async function () { + const diagnosticsWatch = getNextDiagnostics(fixtureUri); + workspace.openTextDocument(fixtureUri); + + assert.strictEqual(1, (await diagnosticsWatch).length); + }); +}); From bbb031d2a6127a2207efda43be148c59bdedec02 Mon Sep 17 00:00:00 2001 From: wongjn <11310624+wongjn@users.noreply.github.com> Date: Sun, 8 Nov 2020 10:26:14 +0000 Subject: [PATCH 4/9] Refactor extraFiles selector --- lib/files.js | 17 +++++++++++++++++ lib/validator.js | 21 ++------------------- test/integration/files.test.js | 27 +++++++++++++++++++++++++++ 3 files changed, 46 insertions(+), 19 deletions(-) create mode 100644 lib/files.js create mode 100644 test/integration/files.test.js diff --git a/lib/files.js b/lib/files.js new file mode 100644 index 0000000..405449d --- /dev/null +++ b/lib/files.js @@ -0,0 +1,17 @@ +/** + * @file + * Utilities relating to files. + */ + +const { workspace } = require('vscode'); + +/** + * Returns the `extraFiles` configuration as an array of document selectors. + * + * @return {import('vscode').DocumentFilter[]} + * Document selectors. + */ +module.exports.getExtraFileSelectors = () => workspace + .getConfiguration('phpSniffer') + .get('extraFiles', []) + .map((pattern) => ({ pattern, scheme: 'file' })); diff --git a/lib/validator.js b/lib/validator.js index ea563f8..73957c9 100644 --- a/lib/validator.js +++ b/lib/validator.js @@ -8,6 +8,7 @@ const { languages, window, workspace, CancellationTokenSource, ProgressLocation, const { reportFlatten } = require('./phpcs-report'); const { createRunner } = require('./runner'); const { createTokenManager } = require('./tokens'); +const { getExtraFileSelectors } = require('./files'); /** * @typedef {import('vscode').Diagnostic} Diagnostic @@ -107,24 +108,6 @@ const onDocumentClose = (diagnostics, tokenManager) => ({ uri }) => { tokenManager.discardToken(uri.fsPath); }; -/** - * Returns whether a given document matches the `extraFiles` setting. - * - * @param {import('vscode').WorkspaceConfiguration} config - * Settings for `phpSniffer`. - * @param {import('vscode').TextDocument} document - * The document to test. - * - * @return {boolean} - * True if document matches, false otherwise. - */ -const matchExtraFiles = (config, document) => { - const selectors = config - .get('extraFiles', []) - .map((pattern) => ({ pattern, scheme: 'file' })); - return languages.match(selectors, document) > 0; -}; - /** * Whether validation should run for the given document. * @@ -139,7 +122,7 @@ const shouldValidate = (document) => { return ( !document.isClosed - && (document.languageId === 'php' || matchExtraFiles(config, document)) + && (document.languageId === 'php' || languages.match(getExtraFileSelectors(), document) > 0) && (!config.get('disableWhenDebugging', false) || !debug.activeDebugSession) ); }; diff --git a/test/integration/files.test.js b/test/integration/files.test.js new file mode 100644 index 0000000..1862987 --- /dev/null +++ b/test/integration/files.test.js @@ -0,0 +1,27 @@ +const assert = require('assert'); +const { workspace } = require('vscode'); +const { getExtraFileSelectors } = require('../../lib/files'); + +suite('getExtraFileSelectors()', function () { + suiteSetup(function () { + return workspace + .getConfiguration('phpSniffer') + .update('extraFiles', ['**/*.css', '**/*.md']); + }); + + suiteTeardown(function () { + return workspace + .getConfiguration('phpSniffer') + .update('extraFiles', undefined); + }); + + test('Returns document filters', function () { + assert.deepStrictEqual( + getExtraFileSelectors(), + [ + { scheme: 'file', pattern: '**/*.css' }, + { scheme: 'file', pattern: '**/*.md' }, + ], + ); + }); +}); From 7d43416db9f3396a5973ca3c8a37f392fa1dad4f Mon Sep 17 00:00:00 2001 From: wongjn <11310624+wongjn@users.noreply.github.com> Date: Sun, 8 Nov 2020 10:37:58 +0000 Subject: [PATCH 5/9] Correct nouns in JSDoc [skip ci] --- lib/files.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/files.js b/lib/files.js index 405449d..44c008f 100644 --- a/lib/files.js +++ b/lib/files.js @@ -6,10 +6,10 @@ const { workspace } = require('vscode'); /** - * Returns the `extraFiles` configuration as an array of document selectors. + * Returns the `extraFiles` configuration as an array of document filters. * * @return {import('vscode').DocumentFilter[]} - * Document selectors. + * Document filters. */ module.exports.getExtraFileSelectors = () => workspace .getConfiguration('phpSniffer') From d10391c6e5d6fc7570d2fdce5bcf4738a6e36e28 Mon Sep 17 00:00:00 2001 From: wongjn <11310624+wongjn@users.noreply.github.com> Date: Sun, 8 Nov 2020 10:49:14 +0000 Subject: [PATCH 6/9] Create document full range function --- lib/formatter.js | 23 +++++++++++++++-------- 1 file changed, 15 insertions(+), 8 deletions(-) diff --git a/lib/formatter.js b/lib/formatter.js index f4c7e81..bf1cd92 100644 --- a/lib/formatter.js +++ b/lib/formatter.js @@ -7,6 +7,20 @@ const { Position, ProgressLocation, Range, TextEdit, window } = require('vscode' const { processSnippet } = require('./strings'); const { createRunner } = require('./runner'); +/** + * Gets a full range of a document. + * + * @param {import('vscode').TextDocument} document + * The document to get the full range of. + * + * @return {import('vscode').Range} + * The range that covers the whole document. + */ +const documentFullRange = (document) => new Range( + new Position(0, 0), + document.lineAt(document.lineCount - 1).range.end, +); + /** * Tests whether a range is for the full document. * @@ -18,14 +32,7 @@ const { createRunner } = require('./runner'); * @return {boolean} * `true` if the given `range` is the full `document`. */ -function isFullDocumentRange(range, document) { - const documentRange = new Range( - new Position(0, 0), - document.lineAt(document.lineCount - 1).range.end, - ); - - return range.isEqual(documentRange); -} +const isFullDocumentRange = (range, document) => range.isEqual(documentFullRange(document)); /** * A formatter function to format text via PHPCBF. From 7d33dfbe2a06260902bc24da60a3ba306e89e227 Mon Sep 17 00:00:00 2001 From: wongjn <11310624+wongjn@users.noreply.github.com> Date: Sun, 8 Nov 2020 10:53:03 +0000 Subject: [PATCH 7/9] Register generic formatter for non-PHP files --- extension.js | 10 ++--- lib/formatter.js | 58 ++++++++++++++++++++++++++++- test/fixtures/.vscode/settings.json | 5 +-- test/functional/extra-files.test.js | 16 +++++--- 4 files changed, 72 insertions(+), 17 deletions(-) diff --git a/extension.js b/extension.js index 55f80d4..6a805d2 100644 --- a/extension.js +++ b/extension.js @@ -3,8 +3,8 @@ * Extension entry. */ -const vscode = require('vscode'); -const { Formatter } = require('./lib/formatter'); +const { languages } = require('vscode'); +const { activateGenericFormatter, Formatter } = require('./lib/formatter'); const { createValidator } = require('./lib/validator'); module.exports = { @@ -16,10 +16,8 @@ module.exports = { */ activate(context) { context.subscriptions.push( - vscode.languages.registerDocumentRangeFormattingEditProvider( - { language: 'php', scheme: 'file' }, - Formatter, - ), + languages.registerDocumentRangeFormattingEditProvider({ language: 'php', scheme: 'file' }, Formatter), + activateGenericFormatter(), createValidator(), ); }, diff --git a/lib/formatter.js b/lib/formatter.js index bf1cd92..a3b4370 100644 --- a/lib/formatter.js +++ b/lib/formatter.js @@ -3,9 +3,10 @@ * Contains the Formatter class. */ -const { Position, ProgressLocation, Range, TextEdit, window } = require('vscode'); +const { languages, Position, ProgressLocation, Range, TextEdit, window, workspace } = require('vscode'); const { processSnippet } = require('./strings'); const { createRunner } = require('./runner'); +const { getExtraFileSelectors } = require('./files'); /** * Gets a full range of a document. @@ -50,7 +51,7 @@ const isFullDocumentRange = (range, document) => range.isEqual(documentFullRange */ /** - * Formatter provider. + * Formatter provider for PHP files. * * @type {import('vscode').DocumentRangeFormattingEditProvider} */ @@ -72,3 +73,56 @@ module.exports.Formatter = { return replacement ? [new TextEdit(range, replacement)] : []; }, }; + +/** + * Formatter provider for non-PHP files. + * + * @type {import('vscode').DocumentFormattingEditProvider} + */ +const GenericFormatter = { + /** + * {@inheritDoc} + */ + provideDocumentFormattingEdits(document, formatOptions, token) { + return module.exports.Formatter.provideDocumentRangeFormattingEdits( + document, + documentFullRange(document), + formatOptions, + token, + ); + }, +}; + +/** + * Registers the generic formatter. + * + * @return {import('vscode').Disposable} + * Disposable for the formatter. + */ +const registerGenericFormatter = () => languages.registerDocumentFormattingEditProvider( + getExtraFileSelectors(), + GenericFormatter, +); + +/** + * Formatter provider for any file type. + * + * @return {import('vscode').Disposable} + */ +module.exports.activateGenericFormatter = () => { + let formatter = registerGenericFormatter(); + + const onConfigChange = workspace.onDidChangeConfiguration((event) => { + if (event.affectsConfiguration('phpSniffer.extraFiles')) { + formatter.dispose(); + formatter = registerGenericFormatter(); + } + }); + + return { + dispose() { + onConfigChange.dispose(); + formatter.dispose(); + }, + }; +}; diff --git a/test/fixtures/.vscode/settings.json b/test/fixtures/.vscode/settings.json index 90bf05f..5291b71 100644 --- a/test/fixtures/.vscode/settings.json +++ b/test/fixtures/.vscode/settings.json @@ -1,6 +1,5 @@ { "php.validate.enable": false, - "[php]": { - "editor.defaultFormatter": "wongjn.php-sniffer" - } + "css.validate": false, + "editor.defaultFormatter": "wongjn.php-sniffer" } diff --git a/test/functional/extra-files.test.js b/test/functional/extra-files.test.js index 6606d9e..8768138 100644 --- a/test/functional/extra-files.test.js +++ b/test/functional/extra-files.test.js @@ -1,10 +1,10 @@ const path = require('path'); const assert = require('assert'); -const { commands, workspace, Uri } = require('vscode'); +const { commands, window, workspace, Uri } = require('vscode'); const { execPromise, FIXTURES_PATH } = require('../utils'); const { getNextDiagnostics } = require('./utils'); -suite('Test `extraFiles` setting', function () { +suite('`extraFiles` handling', function () { const fixtureUri = Uri.file(path.join(FIXTURES_PATH, 'style.css')); const textEncoder = new TextEncoder(); @@ -12,26 +12,22 @@ suite('Test `extraFiles` setting', function () { this.timeout(60000); const config = workspace.getConfiguration('phpSniffer', fixtureUri); - const cssConfig = workspace.getConfiguration('css', fixtureUri); return Promise.all([ execPromise('composer install --no-dev', { cwd: FIXTURES_PATH }), workspace.fs.writeFile(fixtureUri, textEncoder.encode('a{margin : 0}')), config.update('executablesFolder', `vendor${path.sep}bin${path.sep}`), config.update('standard', './css.xml'), config.update('extraFiles', ['**/*.css']), - cssConfig.update('validate', false), ]); }); suiteTeardown(function () { const config = workspace.getConfiguration('phpSniffer', fixtureUri); - const cssConfig = workspace.getConfiguration('css', fixtureUri); return Promise.all([ workspace.fs.delete(fixtureUri), config.update('executablesFolder', undefined), config.update('standard', undefined), config.update('extraFiles', undefined), - cssConfig.update('validate', undefined), ]); }); @@ -43,4 +39,12 @@ suite('Test `extraFiles` setting', function () { assert.strictEqual(1, (await diagnosticsWatch).length); }); + + test('Formatting is applied', async function () { + const document = await workspace.openTextDocument(fixtureUri); + await window.showTextDocument(document); + await commands.executeCommand('editor.action.formatDocument'); + + assert.strictEqual(document.getText(), 'a{margin: 0}'); + }); }); From aa357201248285d3be0cb0287f5aad67e5313910 Mon Sep 17 00:00:00 2001 From: wongjn <11310624+wongjn@users.noreply.github.com> Date: Sun, 8 Nov 2020 10:58:57 +0000 Subject: [PATCH 8/9] Remove unused Format callback typedef --- lib/formatter.js | 15 --------------- 1 file changed, 15 deletions(-) diff --git a/lib/formatter.js b/lib/formatter.js index a3b4370..673aa7a 100644 --- a/lib/formatter.js +++ b/lib/formatter.js @@ -35,21 +35,6 @@ const documentFullRange = (document) => new Range( */ const isFullDocumentRange = (range, document) => range.isEqual(documentFullRange(document)); -/** - * A formatter function to format text via PHPCBF. - * - * @callback Format - * - * @param {string} text - * The string to format. - * - * @return {Promise} - * A promise that resolves to the formatted text. - * - * @throws {Error} - * When there is an error with executing the formatting command. - */ - /** * Formatter provider for PHP files. * From dcdd42bfc561109bd5aebb215c4c4a2a03df2840 Mon Sep 17 00:00:00 2001 From: wongjn <11310624+wongjn@users.noreply.github.com> Date: Sun, 8 Nov 2020 11:01:31 +0000 Subject: [PATCH 9/9] Update documentation --- CHANGELOG.md | 1 + README.md | 23 ++++++++++++++--------- 2 files changed, 15 insertions(+), 9 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index ce442ed..03250e7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,7 @@ All notable changes to the "PHP Sniffer" extension will be documented in this fi - Reword ENOENT errors - Add new setting `disableWhenDebugging` to disable `phpcs` when any debug session is active (#42) - Add option to disable validation (#38) +- Add setting for running on non-PHP files (#16) ### Fixed - Avoid "write EPIPE" error (#35) diff --git a/README.md b/README.md index c468930..a4a20a7 100644 --- a/README.md +++ b/README.md @@ -4,16 +4,16 @@ [![PHP Sniffer on the Visual Studio Marketplace](https://vsmarketplacebadge.apphb.com/version-short/wongjn.php-sniffer.svg)](https://marketplace.visualstudio.com/items?itemName=wongjn.php-sniffer) Uses [PHP_CodeSniffer](https://github.com/squizlabs/PHP_CodeSniffer) to format -and lint PHP code. +and lint (mainly) PHP code. ## Features -- Runs `phpcs` to lint PHP code. -- Runs `phpcbf` to format fixable PHP code validation errors, using the built-in - commands "Format Document" or "Format Selection". - - One may need to set this extension as the default PHP language formatter if - you have more than one PHP language extension enabled. Use the following - snippet in a `settings.json`: +- Runs `phpcs` to lint code. +- Runs `phpcbf` to format fixable code validation errors, using the built-in + commands "Format Document" or "Format Selection" (PHP only). + - One may need to set this extension as the default language formatter for + some languages. The following snippet is an example for PHP that can be + added in a `settings.json`: ```json { "[php]": { @@ -63,6 +63,11 @@ or `never`. amount of milliseconds the validator will wait after typing has stopped before it will run. The validator will also cancel an older run if the run is on the same file. +* `phpSniffer.extraFiles`: [Glob patterns](https://code.visualstudio.com/api/references/vscode-api#GlobPattern) +of extra files to match that this extension should run on. Useful for standards +that don't just validate PHP files. This extension will **always** run on PHP +files — be sure to have your `files.associations` setting correctly setup for +PHP files. * `phpSniffer.executablesFolder`: The **folder** where both `phpcs` and `phpcbf` executables are. Use this to specify a different executable if it is not in your global `PATH`, such as when using `PHP_Codesniffer` as a project-scoped @@ -79,8 +84,8 @@ at the root of the currently open file's workspace folder in the following order 2. `phpcs.xml` 3. `.phpcs.xml.dist` 4. `phpcs.xml.dist` -* `phpSniffer.snippetExcludeSniffs`: Sniffs to exclude when formatting a code -snippet (such as when _formatting on paste_ or on the command +* `phpSniffer.snippetExcludeSniffs`: Sniffs to exclude when formatting a PHP +code snippet (such as when _formatting on paste_ or on the command `format on selection`). This is passed to the `phpcbf` command as the value for `--exclude` when **not** formatting a whole file. * `phpSniffer.disableWhenDebugging`: Disable sniffing when any debug session is