diff --git a/Makefile b/Makefile index ef2b0db49..8820fb870 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) npm run test integration diff --git a/bin/eslint.js b/bin/eslint.js index 1571fc352..5dc2583a6 100755 --- a/bin/eslint.js +++ b/bin/eslint.js @@ -1,244 +1,9 @@ #!/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; +const ESLint = require("../lib/eslint"); +const exitCode = ESLint.run({ dir: CODE_DIR }); - -var eslint = require('../lib/eslint-patch')(); - -var CLIEngine = eslint.CLIEngine; -var docs = require('../lib/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); -} +process.exit(exitCode); diff --git a/integration/eslint_test.js b/integration/eslint_test.js new file mode 100644 index 000000000..e8c711ff3 --- /dev/null +++ b/integration/eslint_test.js @@ -0,0 +1,39 @@ +const sinon = require("sinon"); +const expect = require("chai").expect; + +global.gc = function(){}; + +const STDOUT = console.log; +const STDERR = console.error; + +describe("eslint integration", function() { + describe("eslintrc has not supported plugins", function() { + before(function() { + console.log = sinon.spy(); + console.error = sinon.spy(); + }); + + after(function() { + console.log = STDOUT; + console.error = STDERR; + }); + + it("does not raise any error", function() { + this.timeout(3000); + + var consoleStub = { + log: sinon.spy(), + error: sinon.spy() + }; + + function execute() { + const ESLint = require('../lib/eslint'); + ESLint.run({ dir: __dirname, configPath: `${__dirname}/with_unsupported_plugins/config.json`}); + } + + expect(execute).to.not.throw(); + expect(console.log.called).to.be.ok; + }); + }); + +}); 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 14eda0194..aae974b4a 100644 --- a/lib/docs.js +++ b/lib/docs.js @@ -9,7 +9,7 @@ var fs = require('fs') function Docs() { - var docs = Object.create(null); + var docs = {}; function get(ruleId) { return docs[ruleId]; @@ -17,7 +17,7 @@ function Docs() { 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 diff --git a/lib/eslint-patch.js b/lib/eslint-patch.js index 62114876a..fd6d82633 100644 --- a/lib/eslint-patch.js +++ b/lib/eslint-patch.js @@ -12,10 +12,10 @@ module.exports = function patch() { const skippedModules = []; function warnModuleNotSupported(name) { - if(skippedModules.indexOf(name) < 0) { - skippedModules.push(name); - console.error(`Module not supported: ${name}`); - } + if(skippedModules.indexOf(name) < 0) { + skippedModules.push(name); + console.error(`Module not supported: ${name}`); + } } const resolve = ModuleResolver.prototype.resolve; diff --git a/lib/eslint.js b/lib/eslint.js new file mode 100755 index 000000000..658320983 --- /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(runOptions) { + 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); + console.log(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."); + return 1; + } + + return 0; +} + +module.exports = { run }; diff --git a/package.json b/package.json index 1d28841fb..1fc0b340d 100644 --- a/package.json +++ b/package.json @@ -68,6 +68,8 @@ "temp": "^0.8.3" }, "scripts": { + "integration": "mocha integration", + "integration.debug": "mocha debug integration", "test": "mocha test", "test.debug": "mocha debug test" },