Skip to content

Commit

Permalink
Merge branch 'feature/other-languages' [skip ci]
Browse files Browse the repository at this point in the history
Resolves #16.
  • Loading branch information
wongjn committed Nov 8, 2020
2 parents 8cf6811 + dcdd42b commit 89ddc08
Show file tree
Hide file tree
Showing 15 changed files with 343 additions and 176 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
23 changes: 14 additions & 9 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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]": {
Expand Down Expand Up @@ -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
Expand All @@ -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
Expand Down
10 changes: 4 additions & 6 deletions extension.js
Original file line number Diff line number Diff line change
Expand Up @@ -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 = {
Expand All @@ -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(),
);
},
Expand Down
17 changes: 17 additions & 0 deletions lib/files.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
/**
* @file
* Utilities relating to files.
*/

const { workspace } = require('vscode');

/**
* Returns the `extraFiles` configuration as an array of document filters.
*
* @return {import('vscode').DocumentFilter[]}
* Document filters.
*/
module.exports.getExtraFileSelectors = () => workspace
.getConfiguration('phpSniffer')
.get('extraFiles', [])
.map((pattern) => ({ pattern, scheme: 'file' }));
96 changes: 71 additions & 25 deletions lib/formatter.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,24 @@
* 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.
*
* @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.
Expand All @@ -18,32 +33,10 @@ 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);
}

/**
* A formatter function to format text via PHPCBF.
*
* @callback Format
*
* @param {string} text
* The string to format.
*
* @return {Promise<string>}
* A promise that resolves to the formatted text.
*
* @throws {Error}
* When there is an error with executing the formatting command.
*/
const isFullDocumentRange = (range, document) => range.isEqual(documentFullRange(document));

/**
* Formatter provider.
* Formatter provider for PHP files.
*
* @type {import('vscode').DocumentRangeFormattingEditProvider}
*/
Expand All @@ -65,3 +58,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();
},
};
};
7 changes: 6 additions & 1 deletion lib/runner.js
Original file line number Diff line number Diff line change
Expand Up @@ -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[] */
Expand Down
22 changes: 16 additions & 6 deletions lib/validator.js
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -108,13 +109,22 @@ const onDocumentClose = (diagnostics, tokenManager) => ({ uri }) => {
};

/**
* Whether validation is disabled currently with contextual circumstances.
* Whether validation should run for the given document.
*
* @return {boolean} True if no validation should run, false otherwise.
* @param {import('vscode').TextDocument} document
* The document to validate.
*
* @return {boolean}
* True if validation should run, false otherwise.
*/
const validationDisabled = () => {
const config = workspace.getConfiguration('phpSniffer');
return config.get('disableWhenDebugging', false) && !!debug.activeDebugSession;
const shouldValidate = (document) => {
const config = workspace.getConfiguration('phpSniffer', document.uri);

return (
!document.isClosed
&& (document.languageId === 'php' || languages.match(getExtraFileSelectors(), document) > 0)
&& (!config.get('disableWhenDebugging', false) || !debug.activeDebugSession)
);
};

/**
Expand All @@ -129,7 +139,7 @@ const validationDisabled = () => {
* The validator function.
*/
const validateDocument = (diagnostics, tokenManager) => (document) => {
if (document.languageId !== 'php' || document.isClosed || validationDisabled()) {
if (!shouldValidate(document)) {
return;
}

Expand Down
9 changes: 8 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@
"phpcbf"
],
"activationEvents": [
"onLanguage:php"
"onStartupFinished"
],
"main": "./extension",
"contributes": {
Expand Down Expand Up @@ -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",
Expand Down
5 changes: 2 additions & 3 deletions test/fixtures/.vscode/settings.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
{
"php.validate.enable": false,
"[php]": {
"editor.defaultFormatter": "wongjn.php-sniffer"
}
"css.validate": false,
"editor.defaultFormatter": "wongjn.php-sniffer"
}
4 changes: 4 additions & 0 deletions test/fixtures/css.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
<?xml version="1.0"?>
<ruleset name="CSS">
<rule ref="Squiz.CSS.ColonSpacing"/>
</ruleset>
50 changes: 50 additions & 0 deletions test/functional/extra-files.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
const path = require('path');
const assert = require('assert');
const { commands, window, workspace, Uri } = require('vscode');
const { execPromise, FIXTURES_PATH } = require('../utils');
const { getNextDiagnostics } = require('./utils');

suite('`extraFiles` handling', 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);
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']),
]);
});

suiteTeardown(function () {
const config = workspace.getConfiguration('phpSniffer', fixtureUri);
return Promise.all([
workspace.fs.delete(fixtureUri),
config.update('executablesFolder', undefined),
config.update('standard', undefined),
config.update('extraFiles', 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);
});

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}');
});
});
Loading

0 comments on commit 89ddc08

Please sign in to comment.