diff --git a/.codeclimate.yml b/.codeclimate.yml index 497f42db8..f87184cf2 100644 --- a/.codeclimate.yml +++ b/.codeclimate.yml @@ -15,3 +15,4 @@ ratings: exclude_paths: - "node_modules/**" - "test/**" +- "integration/**" diff --git a/Makefile b/Makefile index ef2b0db49..d628ef211 100644 --- a/Makefile +++ b/Makefile @@ -1,12 +1,25 @@ -.PHONY: image test citest +.PHONY: image test citest integration IMAGE_NAME ?= codeclimate/codeclimate-eslint NPM_TEST_TARGET ?= test +NPM_INTEGRATION_TARGET ?= integration + +DEBUG ?= false +ifeq ($(DEBUG),true) + NPM_TEST_TARGET = test.debug + NPM_INTEGRATION_TARGET = integration.debug +endif image: docker build --rm -t $(IMAGE_NAME) . +integration: image + docker run -ti --rm \ + --volume $(PWD):/code \ + --workdir /code \ + $(IMAGE_NAME) npm run $(NPM_INTEGRATION_TARGET) + test: image docker run -ti --rm \ --volume $(PWD):/code \ @@ -16,4 +29,4 @@ test: image citest: docker run --rm \ --workdir /usr/src/app \ - $(IMAGE_NAME) npm run test + $(IMAGE_NAME) sh -c "npm run test && npm run integration" diff --git a/bin/eslint.js b/bin/eslint.js index 83a371bc5..efe465092 100755 --- a/bin/eslint.js +++ b/bin/eslint.js @@ -1,243 +1,7 @@ #!/usr/src/app/bin/node_gc -var CODE_DIR = "/code"; +const CODE_DIR = "/code"; process.chdir(CODE_DIR); -// Redirect `console.log` so that we are the only ones -// writing to STDOUT -var stdout = console.log; -console.log = console.error; - -var eslint = require('../lib/eslint-patch')(require('eslint')); - -var CLIEngine = eslint.CLIEngine; -var docs = eslint.docs; -var fs = require("fs"); -var glob = require("glob"); -var options = { extensions: [".js"], ignore: true, reset: false, useEslintrc: true }; -var cli; // instantiation delayed until after options are (potentially) modified -var debug = false; -var BatchSanitizer = require("../lib/batch_sanitizer"); -var ignoreWarnings = false; -var ESLINT_WARNING_SEVERITY = 1; -var checks = require("../lib/checks"); -var validateConfig = require("../lib/validate_config"); -var computeFingerprint = require("../lib/compute_fingerprint"); -const ConfigUpgrader = require("../lib/config_upgrader"); - -// a wrapper for emitting perf timing -function runWithTiming(name, fn) { - var start = new Date() - , rv = fn() - , duration = (new Date() - start) / 1000; - if (debug) { - console.error("eslint.timing." + name + ": " + duration + "s"); - } - return rv; -} - -function contentBody(check) { - var content = docs.get(check) || "For more information visit "; - return content + "Source: http://eslint.org/docs/rules/\n"; -} - -function buildIssueJson(message, path) { - // ESLint doesn't emit a ruleId in the - // case of a fatal error (such as an invalid - // token) - var checkName = message.ruleId; - if(message.fatal) { - checkName = "fatal"; - } - var line = message.line || 1; - var column = message.column || 1; - - var issue = { - type: "issue", - categories: checks.categories(checkName), - check_name: checkName, - description: message.message, - content: { - body: contentBody(checkName) - }, - location: { - path: path, - positions: { - begin: { - line: line, - column: column - }, - end: { - line: line, - column: column - } - } - }, - remediation_points: checks.remediationPoints(checkName, message, cli.getConfigForFile(path)) - }; - - var fingerprint = computeFingerprint(path, checkName, message.message); - - if (fingerprint) { - issue["fingerprint"] = fingerprint; - } - - return JSON.stringify(issue); -} - -function isFileWithMatchingExtension(file, extensions) { - var stats = fs.lstatSync(file); - var extension = "." + file.split(".").pop(); - return ( - stats.isFile() && - !stats.isSymbolicLink() - && extensions.indexOf(extension) >= 0 - ); -} - -function isFileIgnoredByLibrary(file) { - return cli.isPathIgnored(file); -} - -function prunePathsWithinSymlinks(paths) { - // Extracts symlinked paths and filters them out, including any child paths - var symlinks = paths.filter(function(path) { - return fs.lstatSync(path).isSymbolicLink(); - }); - - return paths.filter(function(path) { - var withinSymlink = false; - symlinks.forEach(function(symlink) { - if (path.indexOf(symlink) === 0) { - withinSymlink = true; - } - }); - return !withinSymlink; - }); -} - -function inclusionBasedFileListBuilder(includePaths) { - // Uses glob to expand the files and directories in includePaths, filtering - // down to match the list of desired extensions. - return function(extensions) { - var analysisFiles = []; - - includePaths.forEach(function(fileOrDirectory, i) { - if ((/\/$/).test(fileOrDirectory)) { - // if it ends in a slash, expand and push - var filesInThisDirectory = glob.sync( - fileOrDirectory + "/**/**" - ); - prunePathsWithinSymlinks(filesInThisDirectory).forEach(function(file, j){ - if (!isFileIgnoredByLibrary(file) && isFileWithMatchingExtension(file, extensions)) { - analysisFiles.push(file); - } - }); - } else { - if (!isFileIgnoredByLibrary(fileOrDirectory) && isFileWithMatchingExtension(fileOrDirectory, extensions)) { - analysisFiles.push(fileOrDirectory); - } - } - }); - - return analysisFiles; - }; -} - -var buildFileList; -runWithTiming("engineConfig", function () { - if (fs.existsSync("/config.json")) { - var engineConfig = JSON.parse(fs.readFileSync("/config.json")); - - if (engineConfig.include_paths) { - buildFileList = inclusionBasedFileListBuilder( - engineConfig.include_paths - ); - } else { - // No explicit includes, let's try with everything - buildFileList = inclusionBasedFileListBuilder(["./"]); - } - - var userConfig = engineConfig.config || {}; - if (userConfig.config) { - options.configFile = CODE_DIR + "/" + userConfig.config; - options.useEslintrc = false; - } - - if (userConfig.extensions) { - options.extensions = userConfig.extensions; - } - - if (userConfig.ignore_path) { - options.ignorePath = userConfig.ignore_path; - } - - if (userConfig.ignore_warnings) { - ignoreWarnings = true; - } - - if (userConfig.debug) { - debug = true; - } - } - - cli = new CLIEngine(options); -}); - -var analysisFiles = runWithTiming("buildFileList", function() { - return buildFileList(options.extensions); -}); - -function analyzeFiles() { - var batchNum = 0 - , batchSize = 10 - , batchFiles - , batchReport - , sanitizedBatchFiles; - - while(analysisFiles.length > 0) { - batchFiles = analysisFiles.splice(0, batchSize); - sanitizedBatchFiles = (new BatchSanitizer(batchFiles)).sanitizedFiles(); - - if (debug) { - process.stderr.write("Analyzing: " + batchFiles + "\n"); - } - - runWithTiming("analyze-batch-" + batchNum, function() { - batchReport = cli.executeOnFiles(sanitizedBatchFiles); - }); - runWithTiming("report-batch" + batchNum, function() { - batchReport.results.forEach(function(result) { - var path = result.filePath.replace(/^\/code\//, ""); - - result.messages.forEach(function(message) { - if (ignoreWarnings && message.severity === ESLINT_WARNING_SEVERITY) { return; } - - var issueJson = buildIssueJson(message, path); - process.stdout.write(issueJson + "\u0000\n"); - }); - }); - }); - runWithTiming("gc-batch-" + batchNum, function() { - batchFiles = null; - batchReport = null; - global.gc(); - }); - - batchNum++; - } -} - -if (validateConfig(options.configFile)) { - console.error("ESLint is running with the " + cli.getConfigForFile(null).parser + " parser."); - - for (const line of ConfigUpgrader.upgradeInstructions(options.configFile, analysisFiles, process.cwd())) { - console.error(line); - } - - analyzeFiles(); -} else { - console.error("No rules are configured. Make sure you have added a config file with rules enabled."); - console.error("See our documentation at https://docs.codeclimate.com/docs/eslint for more information."); - process.exit(1); -} +const ESLint = require("../lib/eslint"); +ESLint.run(console, { dir: CODE_DIR }); diff --git a/integration/empty_config/config.json b/integration/empty_config/config.json new file mode 100644 index 000000000..a8e6e7ae6 --- /dev/null +++ b/integration/empty_config/config.json @@ -0,0 +1,10 @@ +{ + "enabled": true, + "config": { + "config": "empty_config/eslintrc.yml", + "debug": "true" + }, + "include_paths": [ + "/usr/src/app/integration/empty_config/index.js" + ] +} diff --git a/integration/empty_config/index.js b/integration/empty_config/index.js new file mode 100644 index 000000000..68559a432 --- /dev/null +++ b/integration/empty_config/index.js @@ -0,0 +1,2 @@ +function dummy() { +} diff --git a/integration/eslint_test.js b/integration/eslint_test.js new file mode 100644 index 000000000..72de5f24e --- /dev/null +++ b/integration/eslint_test.js @@ -0,0 +1,64 @@ +const sinon = require("sinon"); +const expect = require("chai").expect; + +const ESLint = require('../lib/eslint'); + +describe("eslint integration", function() { + let consoleMock = {}; + + function executeConfig(configPath) { + return ESLint.run(consoleMock, { dir: __dirname, configPath: `${__dirname}/${configPath}`}); + } + + beforeEach(function() { + consoleMock.output = []; + consoleMock.log = function(msg) { consoleMock.output.push(msg) }; + consoleMock.error = sinon.spy(); + }); + + describe("eslintrc has not supported plugins", function() { + it("does not raise any error", function() { + this.timeout(3000); + + function executeUnsupportedPlugins() { + executeConfig("with_unsupported_plugins/config.json"); + } + + expect(executeUnsupportedPlugins).to.not.throw(); + expect(consoleMock.output).to.not.be.empty; + }); + }); + + describe("validating config", function() { + it("warns about empty config but not raise error", function() { + function executeEmptyConfig() { + executeConfig("empty_config/config.json"); + } + + expect(executeEmptyConfig).to.not.throw(); + sinon.assert.calledWith(consoleMock.error, 'No rules are configured. Make sure you have added a config file with rules enabled.'); + }); + }); + + describe("extends plugin", function() { + it("loads the plugin and does not include repeated issues of not found rules", function() { + this.timeout(5000); + executeConfig("extends_airbnb/config.json"); + + const ruleDefinitionIssues = consoleMock.output.filter(function(o) { return o.includes("Definition for rule"); }); + expect(ruleDefinitionIssues).to.be.empty; + }); + }); + + describe("output", function() { + it("is not messed up", function() { + this.timeout(5000); + + executeConfig("output_mess/config.json"); + + expect(consoleMock.output).to.have.lengthOf(1); + expect(consoleMock.output[0]).to.match(/^\{.*/); + }); + }); + +}); diff --git a/integration/extends_airbnb/config.json b/integration/extends_airbnb/config.json new file mode 100644 index 000000000..f1279921f --- /dev/null +++ b/integration/extends_airbnb/config.json @@ -0,0 +1,10 @@ +{ + "enabled": true, + "config": { + "config": "extends_airbnb/eslintrc.json", + "debug": "true" + }, + "include_paths": [ + "/usr/src/app/integration/extends_airbnb/index.js" + ] +} diff --git a/integration/extends_airbnb/eslintrc.json b/integration/extends_airbnb/eslintrc.json new file mode 100644 index 000000000..1125e23cb --- /dev/null +++ b/integration/extends_airbnb/eslintrc.json @@ -0,0 +1,5 @@ +{ + "extends": "airbnb", + "parser": "babel-eslint", + "rules": {} +} diff --git a/integration/extends_airbnb/index.js b/integration/extends_airbnb/index.js new file mode 100644 index 000000000..e69de29bb diff --git a/integration/output_mess/_eslintrc content.txt b/integration/output_mess/_eslintrc content.txt new file mode 100644 index 000000000..c9733c0aa --- /dev/null +++ b/integration/output_mess/_eslintrc content.txt @@ -0,0 +1,299 @@ +{ + "parser": "babel-eslint", + "parserOptions": { + "ecmaVersion": 6, + "sourceType": "module", + "ecmaFeatures": { + "arrowFunctions": true, + "blockBindings": true, + "classes": true, + "defaultParams": true, + "destructuring": true, + "experimentalObjectRestSpread": true, + "forOf": true, + "generators": false, + "jsx": true, + "modules": true, + "objectLiteralComputedProperties": true, + "objectLiteralDuplicateProperties": false, + "objectLiteralShorthandMethods": true, + "objectLiteralShorthandProperties": true, + "spread": true, + "superInFunctions": true, + "templateStrings": true + } + }, + "env": { + "browser": true, + "es6": true + }, + "plugins": [ + "react" + ], + "rules": { + "strict": [ + 2, + "never" + ], + "no-var": 2, + "no-shadow": 2, + "no-shadow-restricted-names": 2, + "no-unused-vars": [ + 2, + { + "vars": "local", + "args": "after-used" + } + ], + "no-use-before-define": [ + 2, + "nofunc" + ], + "jsx-quotes": [ + 1, + "prefer-single" + ], + "react/no-deprecated": 1, + "react/display-name": 1, + "react/forbid-prop-types": 0, + "react/jsx-boolean-value": 0, + "react/jsx-closing-bracket-location": [ + 1, + "after-props" + ], + "react/jsx-curly-spacing": 1, + "react/jsx-indent-props": [ + 1, + 2 + ], + "react/jsx-max-props-per-line": [ + 1, + { + "maximum": 4 + } + ], + "react/jsx-no-bind": 1, + "react/jsx-no-duplicate-props": 1, + "react/jsx-no-literals": 0, + "react/jsx-no-undef": 1, + "react/jsx-sort-props": 0, + "react/jsx-uses-react": 1, + "react/jsx-uses-vars": 1, + "react/no-danger": 0, + "react/no-did-mount-set-state": 1, + "react/no-did-update-set-state": 1, + "react/no-direct-mutation-state": 1, + "react/no-multi-comp": 0, + "react/no-set-state": 0, + "react/no-unknown-property": 1, + "react/prefer-es6-class": 1, + "react/prop-types": 1, + "react/sort-prop-types": 1, + "react/react-in-jsx-scope": 0, + "react/require-extension": 1, + "react/self-closing-comp": 1, + "react/sort-comp": 1, + "react/wrap-multilines": 1, + "comma-dangle": 0, + "no-cond-assign": [ + 2, + "always" + ], + "no-console": [ + 2, + { + "allow": [ + "warn", + "error" + ] + } + ], + "no-debugger": 1, + "no-alert": 1, + "no-constant-condition": 1, + "no-dupe-keys": 2, + "no-duplicate-case": 2, + "no-empty": 2, + "no-ex-assign": 2, + "no-extra-boolean-cast": 0, + "no-extra-semi": 2, + "no-func-assign": 2, + "no-inner-declarations": 2, + "no-invalid-regexp": 2, + "no-irregular-whitespace": 2, + "no-obj-calls": 2, + "quote-props": [ + "off", + "as-needed", + { + "keywords": true + } + ], + "no-sparse-arrays": 2, + "no-unreachable": 2, + "use-isnan": 2, + "block-scoped-var": 0, + "consistent-return": 2, + "curly": [ + 2, + "multi-line" + ], + "default-case": 2, + "dot-notation": [ + 2, + { + "allowKeywords": true + } + ], + "eqeqeq": [ + 2, + "smart" + ], + "guard-for-in": 2, + "no-caller": 2, + "no-else-return": 2, + "no-eval": 2, + "no-extend-native": 2, + "no-extra-bind": 2, + "no-fallthrough": 2, + "no-floating-decimal": 2, + "no-implied-eval": 2, + "no-lone-blocks": 2, + "no-loop-func": 2, + "no-multi-str": 2, + "no-native-reassign": [ + 2, + { + "exceptions": [ + "App" + ] + } + ], + "no-new": 2, + "no-new-func": 2, + "no-new-wrappers": 2, + "no-octal": 2, + "no-octal-escape": 2, + "no-param-reassign": 2, + "no-proto": 2, + "no-redeclare": 2, + "no-return-assign": 2, + "no-script-url": 2, + "no-self-compare": 2, + "no-sequences": 2, + "no-throw-literal": 2, + "no-with": 2, + "radix": 2, + "vars-on-top": 0, + "wrap-iife": [ + 2, + "any" + ], + "yoda": 2, + "max-len": [ + 1, + 100 + ], + "indent": [ + "error", + 2, + { + "SwitchCase": 1 + } + ], + "brace-style": [ + 2, + "1tbs", + { + "allowSingleLine": true + } + ], + "camelcase": [ + 2, + { + "properties": "never" + } + ], + "comma-spacing": [ + 2, + { + "before": false, + "after": true + } + ], + "comma-style": [ + 2, + "last" + ], + "eol-last": 2, + "func-names": 0, + "func-style": [ + 2, + "expression" + ], + "key-spacing": [ + 2, + { + "beforeColon": false, + "afterColon": true + } + ], + "new-cap": [ + 0, + { + "newIsCap": false + } + ], + "no-multiple-empty-lines": [ + 2, + { + "max": 2 + } + ], + "no-nested-ternary": 2, + "no-new-object": 2, + "no-array-constructor": 2, + "no-spaced-func": 2, + "no-trailing-spaces": 2, + "no-extra-parens": 0, + "no-underscore-dangle": 0, + "one-var": [ + 2, + "never" + ], + "padded-blocks": [ + 2, + "never" + ], + "quotes": [ + 2, + "single" + ], + "semi": [ + 2, + "always" + ], + "semi-spacing": [ + 2, + { + "before": false, + "after": true + } + ], + "keyword-spacing": 1, + "space-before-blocks": 2, + "space-before-function-paren": [ + 2, + "never" + ], + "space-infix-ops": 2, + "spaced-comment": 2, + "no-multi-spaces": 2 + }, + "settings": { + "react": { + "pragma": "React", + "version": "0.15.0" + } + } +} diff --git a/integration/output_mess/config.json b/integration/output_mess/config.json new file mode 100644 index 000000000..6d7380454 --- /dev/null +++ b/integration/output_mess/config.json @@ -0,0 +1,10 @@ +{ + "enabled": true, + "config": { + "config": "output_mess/eslintrc.json", + "debug": "true" + }, + "include_paths": [ + "/usr/src/app/integration/output_mess/index.js" + ] +} diff --git a/integration/output_mess/eslintrc.json b/integration/output_mess/eslintrc.json new file mode 100644 index 000000000..c9733c0aa --- /dev/null +++ b/integration/output_mess/eslintrc.json @@ -0,0 +1,299 @@ +{ + "parser": "babel-eslint", + "parserOptions": { + "ecmaVersion": 6, + "sourceType": "module", + "ecmaFeatures": { + "arrowFunctions": true, + "blockBindings": true, + "classes": true, + "defaultParams": true, + "destructuring": true, + "experimentalObjectRestSpread": true, + "forOf": true, + "generators": false, + "jsx": true, + "modules": true, + "objectLiteralComputedProperties": true, + "objectLiteralDuplicateProperties": false, + "objectLiteralShorthandMethods": true, + "objectLiteralShorthandProperties": true, + "spread": true, + "superInFunctions": true, + "templateStrings": true + } + }, + "env": { + "browser": true, + "es6": true + }, + "plugins": [ + "react" + ], + "rules": { + "strict": [ + 2, + "never" + ], + "no-var": 2, + "no-shadow": 2, + "no-shadow-restricted-names": 2, + "no-unused-vars": [ + 2, + { + "vars": "local", + "args": "after-used" + } + ], + "no-use-before-define": [ + 2, + "nofunc" + ], + "jsx-quotes": [ + 1, + "prefer-single" + ], + "react/no-deprecated": 1, + "react/display-name": 1, + "react/forbid-prop-types": 0, + "react/jsx-boolean-value": 0, + "react/jsx-closing-bracket-location": [ + 1, + "after-props" + ], + "react/jsx-curly-spacing": 1, + "react/jsx-indent-props": [ + 1, + 2 + ], + "react/jsx-max-props-per-line": [ + 1, + { + "maximum": 4 + } + ], + "react/jsx-no-bind": 1, + "react/jsx-no-duplicate-props": 1, + "react/jsx-no-literals": 0, + "react/jsx-no-undef": 1, + "react/jsx-sort-props": 0, + "react/jsx-uses-react": 1, + "react/jsx-uses-vars": 1, + "react/no-danger": 0, + "react/no-did-mount-set-state": 1, + "react/no-did-update-set-state": 1, + "react/no-direct-mutation-state": 1, + "react/no-multi-comp": 0, + "react/no-set-state": 0, + "react/no-unknown-property": 1, + "react/prefer-es6-class": 1, + "react/prop-types": 1, + "react/sort-prop-types": 1, + "react/react-in-jsx-scope": 0, + "react/require-extension": 1, + "react/self-closing-comp": 1, + "react/sort-comp": 1, + "react/wrap-multilines": 1, + "comma-dangle": 0, + "no-cond-assign": [ + 2, + "always" + ], + "no-console": [ + 2, + { + "allow": [ + "warn", + "error" + ] + } + ], + "no-debugger": 1, + "no-alert": 1, + "no-constant-condition": 1, + "no-dupe-keys": 2, + "no-duplicate-case": 2, + "no-empty": 2, + "no-ex-assign": 2, + "no-extra-boolean-cast": 0, + "no-extra-semi": 2, + "no-func-assign": 2, + "no-inner-declarations": 2, + "no-invalid-regexp": 2, + "no-irregular-whitespace": 2, + "no-obj-calls": 2, + "quote-props": [ + "off", + "as-needed", + { + "keywords": true + } + ], + "no-sparse-arrays": 2, + "no-unreachable": 2, + "use-isnan": 2, + "block-scoped-var": 0, + "consistent-return": 2, + "curly": [ + 2, + "multi-line" + ], + "default-case": 2, + "dot-notation": [ + 2, + { + "allowKeywords": true + } + ], + "eqeqeq": [ + 2, + "smart" + ], + "guard-for-in": 2, + "no-caller": 2, + "no-else-return": 2, + "no-eval": 2, + "no-extend-native": 2, + "no-extra-bind": 2, + "no-fallthrough": 2, + "no-floating-decimal": 2, + "no-implied-eval": 2, + "no-lone-blocks": 2, + "no-loop-func": 2, + "no-multi-str": 2, + "no-native-reassign": [ + 2, + { + "exceptions": [ + "App" + ] + } + ], + "no-new": 2, + "no-new-func": 2, + "no-new-wrappers": 2, + "no-octal": 2, + "no-octal-escape": 2, + "no-param-reassign": 2, + "no-proto": 2, + "no-redeclare": 2, + "no-return-assign": 2, + "no-script-url": 2, + "no-self-compare": 2, + "no-sequences": 2, + "no-throw-literal": 2, + "no-with": 2, + "radix": 2, + "vars-on-top": 0, + "wrap-iife": [ + 2, + "any" + ], + "yoda": 2, + "max-len": [ + 1, + 100 + ], + "indent": [ + "error", + 2, + { + "SwitchCase": 1 + } + ], + "brace-style": [ + 2, + "1tbs", + { + "allowSingleLine": true + } + ], + "camelcase": [ + 2, + { + "properties": "never" + } + ], + "comma-spacing": [ + 2, + { + "before": false, + "after": true + } + ], + "comma-style": [ + 2, + "last" + ], + "eol-last": 2, + "func-names": 0, + "func-style": [ + 2, + "expression" + ], + "key-spacing": [ + 2, + { + "beforeColon": false, + "afterColon": true + } + ], + "new-cap": [ + 0, + { + "newIsCap": false + } + ], + "no-multiple-empty-lines": [ + 2, + { + "max": 2 + } + ], + "no-nested-ternary": 2, + "no-new-object": 2, + "no-array-constructor": 2, + "no-spaced-func": 2, + "no-trailing-spaces": 2, + "no-extra-parens": 0, + "no-underscore-dangle": 0, + "one-var": [ + 2, + "never" + ], + "padded-blocks": [ + 2, + "never" + ], + "quotes": [ + 2, + "single" + ], + "semi": [ + 2, + "always" + ], + "semi-spacing": [ + 2, + { + "before": false, + "after": true + } + ], + "keyword-spacing": 1, + "space-before-blocks": 2, + "space-before-function-paren": [ + 2, + "never" + ], + "space-infix-ops": 2, + "spaced-comment": 2, + "no-multi-spaces": 2 + }, + "settings": { + "react": { + "pragma": "React", + "version": "0.15.0" + } + } +} diff --git a/integration/output_mess/index.js b/integration/output_mess/index.js new file mode 100644 index 000000000..aaf404641 --- /dev/null +++ b/integration/output_mess/index.js @@ -0,0 +1 @@ +import React from 'react'; diff --git a/integration/with_unsupported_plugins/config.json b/integration/with_unsupported_plugins/config.json new file mode 100644 index 000000000..9f3b35f87 --- /dev/null +++ b/integration/with_unsupported_plugins/config.json @@ -0,0 +1,10 @@ +{ + "enabled": true, + "config": { + "config": "with_unsupported_plugins/eslintrc.yml", + "debug": "false" + }, + "include_paths": [ + "/usr/src/app/integration/with_unsupported_plugins/index.js" + ] +} diff --git a/integration/with_unsupported_plugins/eslintrc.yml b/integration/with_unsupported_plugins/eslintrc.yml new file mode 100644 index 000000000..188ce14c5 --- /dev/null +++ b/integration/with_unsupported_plugins/eslintrc.yml @@ -0,0 +1,21 @@ +env: + es6: true + node: true +parserOptions: + sourceType: module +plugins: + - node + - not_supported +extends: + - not_valid + - 'plugin:invalidplugin/recommended' + - 'eslint:recommended' + - 'plugin:node/recommended' +rules: + invalidplugin/rule: 1 + node/exports-style: [error, module.exports] + indent: [error, 4] + linebreak-style: [error, unix] + quotes: [error, double] + semi: [error, always] + no-console: off diff --git a/integration/with_unsupported_plugins/index.js b/integration/with_unsupported_plugins/index.js new file mode 100644 index 000000000..68559a432 --- /dev/null +++ b/integration/with_unsupported_plugins/index.js @@ -0,0 +1,2 @@ +function dummy() { +} diff --git a/lib/docs.js b/lib/docs.js index 0f76ad4ee..aae974b4a 100644 --- a/lib/docs.js +++ b/lib/docs.js @@ -7,34 +7,26 @@ var fs = require('fs') , path = require('path'); -//------------------------------------------------------------------------------ -// Privates -//------------------------------------------------------------------------------ +function Docs() { -var docs = Object.create(null); + var docs = {}; + + function get(ruleId) { + return docs[ruleId]; + } -function load() { var docsDir = path.join(__dirname, '/docs/rules'); - fs.readdirSync(docsDir).forEach(function(file) { + fs.existsSync(docsDir) && fs.readdirSync(docsDir).forEach(function(file) { var content = fs.readFileSync(docsDir + '/' + file, 'utf8'); // Remove the .md extension from the filename docs[file.slice(0, -3)] = content; }); -} - -//------------------------------------------------------------------------------ -// Public Interface -//------------------------------------------------------------------------------ -exports.get = function(ruleId) { - return docs[ruleId]; -}; - -//------------------------------------------------------------------------------ -// Initialization -//------------------------------------------------------------------------------ + return { + get: get + }; +} -// loads existing docs -load(); +module.exports = Docs; diff --git a/lib/empty-plugin.js b/lib/empty-plugin.js new file mode 100644 index 000000000..b8f05e40a --- /dev/null +++ b/lib/empty-plugin.js @@ -0,0 +1,6 @@ +module.exports = { + rules: {}, + configs: { + recommended: {} + } +}; diff --git a/lib/eslint-patch.js b/lib/eslint-patch.js index f2677ca98..f288d29d7 100644 --- a/lib/eslint-patch.js +++ b/lib/eslint-patch.js @@ -1,33 +1,43 @@ -'use strict'; -var meld = require('meld'); -var docs = require('./docs'); -var Config = require("eslint/lib/config"); -var ConfigUpgrader = require('./config_upgrader'); - -var supportedPlugins = ['react', 'babel']; - -module.exports = function patcher(eslint) { - - meld.around(eslint.CLIEngine, 'loadPlugins', function(joinPoint) { - var pluginNames = joinPoint.args[0]; - var filteredPluginNames = pluginNames.filter(function(pluginName) { - return supportedPlugins.indexOf(pluginName) >= 0; - }); - return joinPoint.proceed(filteredPluginNames); - }); - - meld.around(eslint.CLIEngine, 'addPlugin', function() { - return; - }); - - // meld.around(eslint.CLIEngine.Config, 'loadPackage', function(joinPoint) { - // var filePath = joinPoint.args[0]; - // if (filePath.match(/^eslint-config-airbnb.*/)) { - // return joinPoint.proceed(); - // } else { - // return {}; - // } - // }); +"use strict"; + +const Plugins = require("eslint/lib/config/plugins"); +const ModuleResolver = require("eslint/lib/util/module-resolver"); + +const ConfigFile = require("eslint/lib/config/config-file"); + +const Config = require("eslint/lib/config"); +const ConfigUpgrader = require("./config_upgrader"); + +module.exports = function patch() { + const skippedModules = []; + function warnModuleNotSupported(name) { + if(!skippedModules.includes(name)) { + skippedModules.push(name); + console.error(`Module not supported: ${name}`); + } + } + + const resolve = ModuleResolver.prototype.resolve; + ModuleResolver.prototype.resolve = function(name, path) { + try { + return resolve.apply(this, [name, path]); + } catch(e) { + warnModuleNotSupported(name); + return `${__dirname}/empty-plugin.js`; + } + }; + + Plugins.loadAll = function(pluginNames) { + for(const name of pluginNames) { + try { + + Plugins.load(name); + + } catch(e) { + warnModuleNotSupported(`eslint-plugin-${name}`); + } + } + }; const originalGetConfig = Config.prototype.getConfig; Config.prototype.getConfig = function(filePath) { @@ -37,7 +47,5 @@ module.exports = function patcher(eslint) { return configUpgrader.upgrade(originalConfig); }; - eslint.docs = docs; - - return eslint; + return require('eslint'); }; diff --git a/lib/eslint.js b/lib/eslint.js new file mode 100755 index 000000000..77efb13f6 --- /dev/null +++ b/lib/eslint.js @@ -0,0 +1,243 @@ +#!/usr/src/app/bin/node_gc + +const fs = require("fs"); +const glob = require("glob"); + +const eslint = require('./eslint-patch')(); +const docs = require('./docs')(); +const BatchSanitizer = require("./batch_sanitizer"); +const checks = require("./checks"); +const validateConfig = require("./validate_config"); +const computeFingerprint = require("./compute_fingerprint"); +const ConfigUpgrader = require("./config_upgrader"); + +const CLIEngine = eslint.CLIEngine; +const options = { extensions: [".js"], ignore: true, reset: false, useEslintrc: true }; + +function run(console, runOptions) { + const STDOUT = console.log; + console.log = console.error; + + var configPath = runOptions.configPath || "/config.json"; + var codeDir = runOptions.dir || "/code"; + + var cli; // instantiation delayed until after options are (potentially) modified + var debug = false; + var ignoreWarnings = false; + var ESLINT_WARNING_SEVERITY = 1; + + // a wrapper for emitting perf timing + function runWithTiming(name, fn) { + const start = new Date(); + const result = fn(); + + if (debug) { + const duration = (new Date() - start) / 1000; + console.error("eslint.timing." + name + ": " + duration + "s"); + } + + return result; + } + + function contentBody(check) { + var content = docs.get(check) || "For more information visit "; + return content + "Source: http://eslint.org/docs/rules/\n"; + } + + function buildIssueJson(message, path) { + // ESLint doesn't emit a ruleId in the + // case of a fatal error (such as an invalid + // token) + var checkName = message.ruleId; + if(message.fatal) { + checkName = "fatal"; + } + var line = message.line || 1; + var column = message.column || 1; + + var issue = { + type: "issue", + categories: checks.categories(checkName), + check_name: checkName, + description: message.message, + content: { + body: contentBody(checkName) + }, + location: { + path: path, + positions: { + begin: { + line: line, + column: column + }, + end: { + line: line, + column: column + } + } + }, + remediation_points: checks.remediationPoints(checkName, message, cli.getConfigForFile(path)) + }; + + var fingerprint = computeFingerprint(path, checkName, message.message); + + if (fingerprint) { + issue["fingerprint"] = fingerprint; + } + + return JSON.stringify(issue); + } + + function isFileWithMatchingExtension(file, extensions) { + var stats = fs.lstatSync(file); + var extension = "." + file.split(".").pop(); + return ( + stats.isFile() && + !stats.isSymbolicLink() + && extensions.indexOf(extension) >= 0 + ); + } + + function isFileIgnoredByLibrary(file) { + return cli.isPathIgnored(file); + } + + function prunePathsWithinSymlinks(paths) { + // Extracts symlinked paths and filters them out, including any child paths + var symlinks = paths.filter(function(path) { + return fs.lstatSync(path).isSymbolicLink(); + }); + + return paths.filter(function(path) { + var withinSymlink = false; + symlinks.forEach(function(symlink) { + if (path.indexOf(symlink) === 0) { + withinSymlink = true; + } + }); + return !withinSymlink; + }); + } + + function inclusionBasedFileListBuilder(includePaths) { + // Uses glob to expand the files and directories in includePaths, filtering + // down to match the list of desired extensions. + return function(extensions) { + var analysisFiles = []; + + includePaths.forEach(function(fileOrDirectory, i) { + if ((/\/$/).test(fileOrDirectory)) { + // if it ends in a slash, expand and push + var filesInThisDirectory = glob.sync( + fileOrDirectory + "/**/**" + ); + prunePathsWithinSymlinks(filesInThisDirectory).forEach(function(file, j){ + if (!isFileIgnoredByLibrary(file) && isFileWithMatchingExtension(file, extensions)) { + analysisFiles.push(file); + } + }); + } else { + if (!isFileIgnoredByLibrary(fileOrDirectory) && isFileWithMatchingExtension(fileOrDirectory, extensions)) { + analysisFiles.push(fileOrDirectory); + } + } + }); + + return analysisFiles; + }; + } + + function overrideOptions(userConfig) { + if (userConfig.config) { + options.configFile = codeDir + "/" + userConfig.config; + options.useEslintrc = false; + } + + if (userConfig.extensions) { + options.extensions = userConfig.extensions; + } + + if (userConfig.ignore_path) { + options.ignorePath = userConfig.ignore_path; + } + + ignoreWarnings = !!userConfig.ignore_warnings; + debug = !!userConfig.debug; + } + + // No explicit includes, let's try with everything + var buildFileList = inclusionBasedFileListBuilder(["./"]); + + runWithTiming("engineConfig", function () { + if (fs.existsSync(configPath)) { + var engineConfig = JSON.parse(fs.readFileSync(configPath)); + + if (engineConfig.include_paths) { + buildFileList = inclusionBasedFileListBuilder(engineConfig.include_paths); + } + + overrideOptions(engineConfig.config || {}); + } + + cli = new CLIEngine(options); + }); + + var analysisFiles = runWithTiming("buildFileList", function() { + return buildFileList(options.extensions); + }); + + function analyzeFiles() { + var batchNum = 0 + , batchSize = 10 + , batchFiles + , batchReport + , sanitizedBatchFiles; + + while(analysisFiles.length > 0) { + batchFiles = analysisFiles.splice(0, batchSize); + sanitizedBatchFiles = (new BatchSanitizer(batchFiles)).sanitizedFiles(); + + if (debug) { + console.error("Analyzing: " + batchFiles); + } + + runWithTiming("analyze-batch-" + batchNum, function() { + batchReport = cli.executeOnFiles(sanitizedBatchFiles); + }); + runWithTiming("report-batch" + batchNum, function() { + batchReport.results.forEach(function(result) { + var path = result.filePath.replace(/^\/code\//, ""); + + result.messages.forEach(function(message) { + if (ignoreWarnings && message.severity === ESLINT_WARNING_SEVERITY) { return; } + + var issueJson = buildIssueJson(message, path); + STDOUT(issueJson + "\u0000\n"); + }); + }); + }); + runWithTiming("gc-batch-" + batchNum, function() { + batchFiles = null; + batchReport = null; + global.gc(); + }); + + batchNum++; + } + } + + if (validateConfig(options.configFile)) { + console.error("ESLint is running with the " + cli.getConfigForFile(null).parser + " parser."); + + for (const line of ConfigUpgrader.upgradeInstructions(options.configFile, analysisFiles, process.cwd())) { + console.error(line); + } + + analyzeFiles(); + } else { + console.error("No rules are configured. Make sure you have added a config file with rules enabled."); + console.error("See our documentation at https://docs.codeclimate.com/docs/eslint for more information."); + } +} + +module.exports = { run }; diff --git a/package.json b/package.json index c5d257b2e..f10b49d8e 100644 --- a/package.json +++ b/package.json @@ -59,8 +59,7 @@ "eslint-plugin-vue": "^2.0.1", "eslint-plugin-xogroup": "^1.2.0", "glob": "^7.0.6", - "prettier": "^0.22.0", - "meld": "^1.3.2" + "prettier": "^0.22.0" }, "devDependencies": { "chai": "^3.5.0", @@ -70,8 +69,10 @@ "temp": "^0.8.3" }, "scripts": { - "test": "mocha test", - "test.debug": "mocha debug test" + "integration": "mocha -gc integration", + "integration.debug": "mocha -gc debug integration", + "test": "mocha -gc test", + "test.debug": "mocha -gc debug test" }, "engine": "node >= 0.12.4" } diff --git a/test/eslint-patch_test.js b/test/eslint-patch_test.js new file mode 100644 index 000000000..d126becb8 --- /dev/null +++ b/test/eslint-patch_test.js @@ -0,0 +1,89 @@ +const expect = require("chai").expect; +const sinon = require("sinon"); + +const Plugins = require("eslint/lib/config/plugins"); +const ModuleResolver = require("eslint/lib/util/module-resolver"); +const eslintPatch = require("../lib/eslint-patch"); + +describe("eslint-patch", function() { + describe("patch", function() { + let loadAll; + + before(function() { + loadAll = Plugins.loadAll; + }); + + after(function() { + Plugins.loadAll = loadAll; + }); + + it("intercepts plugins", function() { + eslintPatch(); + expect(loadAll).to.not.equal(Plugins.loadAll, "Plugins.loadAll is not patched"); + }); + }); + + describe("Plugins.loadAll", function() { + before(function() { + eslintPatch(); + }); + + it("delegates each plugin to be loaded", function () { + Plugins.load = sinon.spy(); + + Plugins.loadAll([ "jasmine", "mocha" ]); + + expect(Plugins.load.calledWith("jasmine")).to.be.true; + expect(Plugins.load.calledWith("mocha")).to.be.true; + }); + + it("only warns not supported once", function () { + console.error = sinon.spy(); + Plugins.load = sinon.stub().throws(); + + Plugins.loadAll([ "node" ]); + Plugins.loadAll([ "node" ]); + + sinon.assert.calledOnce(console.error); + sinon.assert.calledWith(console.error, "Module not supported: eslint-plugin-node"); + }); + + it("does not raise exception for unsupported plugins", function() { + Plugins.getAll = sinon.stub().returns([]); + Plugins.load = sinon.stub().throws(); + + function loadPlugin() { + Plugins.loadAll([ "unsupported-plugin" ]); + } + + expect(loadPlugin).to.not.throw(); + }); + }); + + describe("loading extends configuration", function() { + it("patches module resolver", function() { + const resolve = ModuleResolver.prototype.resolve; + + eslintPatch(); + expect(ModuleResolver.prototype.resolve).to.not.eql(resolve); + }); + + it("returns fake config for skipped modules", function() { + eslintPatch(); + Plugins.loadAll(['invalidplugin']); + expect(new ModuleResolver().resolve('eslint-plugin-invalidplugin')).to.match(/.+empty-plugin.js/); + }); + + it("does not warn user repeatedly about not supported modules", function() { + console.error = sinon.spy(); + eslintPatch(); + + for(var i=0; i<3; i++) { + new ModuleResolver().resolve('eslint-plugin-bogus'); + } + + expect(console.error.callCount).to.eql(1); + }); + }); + +}); diff --git a/yarn.lock b/yarn.lock index 0ecc99747..d1a0ed102 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1467,10 +1467,6 @@ lru-cache@2: version "2.7.3" resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-2.7.3.tgz#6d4524e8b955f95d4f5b58851ce21dd72fb4e952" -meld@^1.3.2: - version "1.3.2" - resolved "https://registry.yarnpkg.com/meld/-/meld-1.3.2.tgz#8c3235fb5001b8796f8768818e9e4563b0de8066" - minimatch@0.3: version "0.3.0" resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-0.3.0.tgz#275d8edaac4f1bb3326472089e7949c8394699dd"