diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..04ba039 --- /dev/null +++ b/.editorconfig @@ -0,0 +1,20 @@ +# EditorConfig helps developers define and maintain consistent +# coding styles between different editors and IDEs +# editorconfig.org + +root = true + +[*] + +# Change these settings to your own preference +indent_style = space +indent_size = 2 + +# We recommend you to keep these unchanged +end_of_line = lf +charset = utf-8 +trim_trailing_whitespace = true +insert_final_newline = true + +[*.md] +trim_trailing_whitespace = false diff --git a/.gitignore b/.gitignore index e936855..67015aa 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,4 @@ .idea node_modules .github_changelog_generator +package-lock.json diff --git a/.travis.yml b/.travis.yml index d05cb5b..ec8278b 100644 --- a/.travis.yml +++ b/.travis.yml @@ -2,7 +2,7 @@ env: global: - COMPOSER_DISABLE_XDEBUG_WARN="1" - - secure: "fiJ/CO0SWd3PPnNbYEtgKxw2xa9fKTOx3qNK48joGEkYFngl5o18l8GlaCZo0wzjhdZdhopts0ZYejdyqRQgxA1QKfCG/Te0ukygRMPXBC+oSTY8fyCWnueZ0fF3O0wrDQUYUTLf8nPrFKOw7bOi0sn+Lcu3mUaWlGxrsHsg9GU=" + - COMPOSER_OAUTH_TOKEN="cc4a091c096e7d3cfe053c3f669fb840be60ab98" matrix: include: diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index f528c18..e63a143 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -10,7 +10,7 @@ If you would like to contribute enhancements or fixes, please do the following: Please note that modifications should follow these coding guidelines: - Indent is 2 spaces. -- Code should pass [CoffeeLint](http://www.coffeelint.org/) with the provided `coffeelint.json`. +- Code should pass [ESLint](http://eslint.org/). - Vertical whitespace helps readability, don’t be afraid to use it. Thank you for helping out! diff --git a/lib/main.js b/lib/main.js index 3309f64..99711af 100644 --- a/lib/main.js +++ b/lib/main.js @@ -1,33 +1,70 @@ 'use babel'; -// eslint-disable-next-line import/no-extraneous-dependencies, import/extensions +// eslint-disable-next-line import/extensions, import/no-extraneous-dependencies import { CompositeDisposable } from 'atom'; -import Path from 'path'; -import * as helpers from 'atom-linter'; -// Local vars -const regex = /(.+):(\d+)\t*(.+)/g; +let helpers; +let path; -// Settings -let executablePath; -let rulesets; +function loadDeps() { + if (!helpers) { + helpers = require('atom-linter'); + } + if (!path) { + path = require('path'); + } +} export default { activate() { - require('atom-package-deps').install('linter-phpmd'); + this.idleCallbacks = new Set(); + let depsCallbackID; + const installLinterPhpmdDeps = () => { + this.idleCallbacks.delete(depsCallbackID); + if (!atom.inSpecMode()) { + require('atom-package-deps').install('linter-phpmd'); + } + loadDeps(); + }; + depsCallbackID = window.requestIdleCallback(installLinterPhpmdDeps); + this.idleCallbacks.add(depsCallbackID); this.subscriptions = new CompositeDisposable(); + this.subscriptions.add( atom.config.observe('linter-phpmd.executablePath', (value) => { - executablePath = value; + this.executablePath = value; + }), + atom.config.observe('linter-phpmd.autoExecutableSearch', (value) => { + this.autoExecutableSearch = value; + }), + atom.config.observe('linter-phpmd.disableWhenNoConfigFile', (value) => { + this.disableWhenNoConfigFile = value; + }), + atom.config.observe('linter-phpmd.rulesOrConfigFile', (value) => { + this.rulesOrConfigFile = value; }), - atom.config.observe('linter-phpmd.rulesets', (value) => { - rulesets = value; + atom.config.observe('linter-phpmd.autoConfigSearch', (value) => { + this.autoConfigSearch = value; + }), + atom.config.observe('linter-phpmd.minimumPriority', (value) => { + this.minimumPriority = value; + }), + atom.config.observe('linter-phpmd.strictMode', (value) => { + this.strictMode = value; }), ); + + // Backwards compatibility + if (atom.config.get('linter-phpmd.rulesets') !== undefined) { + atom.config.set('linter-phpmd.rulesOrConfigFile', atom.config.get('linter-phpmd.rulesets')); + atom.config.unset('linter-phpmd.rulesets'); + } }, deactivate() { + this.idleCallbacks.forEach(callbackID => window.cancelIdleCallback(callbackID)); + this.idleCallbacks.clear(); this.subscriptions.dispose(); }, @@ -41,39 +78,102 @@ export default { const filePath = textEditor.getPath(); const fileText = textEditor.getText(); - let ruleset = rulesets; - if (/^[a-z0-9]+\.xml$/gi.test(rulesets)) { - const rulesetPath = await helpers.findAsync(filePath, rulesets); - if (rulesetPath !== null) { - ruleset = rulesetPath; + if (fileText === '' || !filePath) { + // Empty file, empty results + return []; + } + + loadDeps(); + const fileDir = path.dirname(filePath); + + let executable = this.executablePath; + + // Check if a local PHPMD executable is available + if (this.autoExecutableSearch === true) { + const phpmdNames = ['vendor/bin/phpmd']; + const projExecutable = await helpers.findCachedAsync(fileDir, phpmdNames); + + if (projExecutable !== null) { + executable = projExecutable; } } + // Rulesets + let rulesets = this.rulesOrConfigFile.join(','); + let confFile = null; + + // Check if a rulesets file exists and handle it + if (this.autoConfigSearch === true) { + confFile = await helpers.findAsync(fileDir, ['phpmd.xml', 'phpmd.xml.dist', 'phpmd.ruleset.xml']); + + // Check if we should stop linting when no config file could be found + if (this.disableWhenNoConfigFile && !confFile) { + return []; + } + + // Override rulessets with found config file + if (confFile) { + rulesets = confFile; + } + } + + // PHPMD cli parameters const parameters = [ filePath, 'text', - ruleset, + rulesets, ]; - const projectDir = atom.project.relativizePath(filePath)[0]; - const options = { + // Rule priority threshold; rules with lower priority than this will not be used + if (this.minimumPriority >= 0) { + parameters.push('--minimumpriority', this.minimumPriority); + } + + // Strict mode + if (this.strictMode === true) { + parameters.push('--strict'); + } + + // Current working dir + let workDir = fileDir; + + // Determine project path + const projectPath = atom.project.relativizePath(filePath)[0]; + + // Set current working dir based on config path or project path + if (confFile) { + workDir = path.dirname(confFile); + } else if (projectPath) { + workDir = projectPath; + } + + // PHPMD exec options + const execOptions = { + cwd: workDir, ignoreExitCode: true, - cwd: projectDir || Path.dirname(filePath), + timeout: 1000 * 60 * 5, // ms * s * m: 5 minutes }; - const output = await helpers.exec(executablePath, parameters, options); + // Execute PHPMD + const result = await helpers.exec(executable, parameters, execOptions); + + if (result === null) { + // Our specific spawn was terminated by a newer call, tell Linter not + // to update messages + return null; + } + // Check if the file contents have changed since the lint was triggered if (textEditor.getText() !== fileText) { - // eslint-disable-next-line no-console - console.warn('linter-phpmd:: The file was modified since the ' + - 'request was sent to check it. Since any results would no longer ' + - 'be valid, they are not being updated. Please save the file ' + - 'again to update the results.'); + // Contents have changed, tell Linter not to update results return null; } + // Message regex + const regex = /(.+):(\d+)\t*(.+)/g; + const messages = []; - let match = regex.exec(output); + let match = regex.exec(result); while (match !== null) { const line = Number.parseInt(match[2], 10) - 1; messages.push({ @@ -83,8 +183,9 @@ export default { text: match[3], }); - match = regex.exec(output); + match = regex.exec(result); } + return messages; }, }; diff --git a/package.json b/package.json index 2c15f97..9b055e0 100644 --- a/package.json +++ b/package.json @@ -18,16 +18,58 @@ "configSchema": { "executablePath": { "type": "string", - "title": "PHPMD Executable Path", "default": "phpmd", + "description": "Enter the path to your phpmd executable.", "order": 1 }, - "rulesets": { - "type": "string", - "title": "PHPMD Rulesets", - "default": "cleancode,codesize,controversial,design,naming,unusedcode", - "description": "Comma separated list of rulesets to use in phpmd. You can also enter the name of your ruleset file (example: `ruleset.xml`) to load that from the current file's directory (or any of the parent directories)", + "autoExecutableSearch": { + "title": "Search for executables", + "type": "boolean", + "default": true, + "description": "Automatically search for `vendor/bin/phpmd` executable. Overrides the exectuable defined above.", "order": 2 + }, + "rulesOrConfigFile": { + "type": "array", + "default": [ + "cleancode", + "codesize", + "controversial", + "design", + "naming", + "unusedcode" + ], + "items": { + "type": "string" + }, + "description": "Enter path to ruleset file or a predefined rulesets.", + "order": 3 + }, + "disableWhenNoConfigFile": { + "type": "boolean", + "default": false, + "description": "Disable the linter when the default ruleset file is not found.", + "order": 4 + }, + "autoConfigSearch": { + "title": "Search for ruleset files", + "type": "boolean", + "default": true, + "description": "Automatically search for any `phpmd.xml`, `phpmd.xml.dist` or `phpmd.ruleset.xml` file to use as ruleset. Overrides custom ruleset(s) defined above.", + "order": 5 + }, + "minimumPriority": { + "type": "integer", + "default": -1, + "minimum": -1, + "description": "Rule priority threshold; rules with lower priority than this will not be used.", + "order": 6 + }, + "strictMode": { + "type": "boolean", + "default": false, + "description": "Also report those nodes with a @SuppressWarnings annotation.", + "order": 7 } }, "homepage": "https://github.com/AtomLinter/linter-phpmd#readme", @@ -71,11 +113,12 @@ } ] }, - "env": { - "node": true - }, "globals": { "atom": true + }, + "env": { + "node": true, + "browser": true } } } diff --git a/spec/files/bad-suppressed.php b/spec/files/bad-suppressed.php new file mode 100644 index 0000000..58822b9 --- /dev/null +++ b/spec/files/bad-suppressed.php @@ -0,0 +1,8 @@ + { @@ -49,4 +50,33 @@ describe('The phpmd provider for Linter', () => { expect(messages.length).toBe(0); }); + + it('finds nothing wrong with suppressed warnings', async () => { + const editor = await atom.workspace.open(badSuppressedPath); + const messages = await lint(editor); + + expect(messages.length).toBe(0); + }); + + it('finds nothing wrong with suppressed warnings by minimum priority', async () => { + atom.config.set('linter-phpmd.minimumPriority', 0); + const editor = await atom.workspace.open(badPath); + const messages = await lint(editor); + + expect(messages.length).toBe(0); + }); + + it('verifies the messages for bad-suppressed.php with strict mode set', async () => { + atom.config.set('linter-phpmd.strictMode', true); + const editor = await atom.workspace.open(badSuppressedPath); + const messages = await lint(editor); + + expect(messages.length).toBe(1); + expect(messages[0].type).toBe('Error'); + expect(messages[0].html).not.toBeDefined(); + expect(messages[0].text).toBe('Avoid using short method names like ::a(). ' + + 'The configured minimum method name length is 3.'); + expect(messages[0].filePath).toBe(badSuppressedPath); + expect(messages[0].range).toEqual([[6, 0], [6, 14]]); + }); });