Skip to content

Commit 473c99a

Browse files
filipesperandiodblandin
authored andcommitted
Prevent libs from messing with stdout (codeclimate#229)
* Revert "Revert "Prevent errors from unsupported plugins/rules/modules (codeclimate#215)"" This reverts commit 659472f. * Prevent libs from messing with stdout * Keep it testable with no performance penalty
1 parent d88d6a0 commit 473c99a

23 files changed

+1148
-302
lines changed

.codeclimate.yml

+1
Original file line numberDiff line numberDiff line change
@@ -15,3 +15,4 @@ ratings:
1515
exclude_paths:
1616
- "node_modules/**"
1717
- "test/**"
18+
- "integration/**"

Makefile

+15-2
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,25 @@
1-
.PHONY: image test citest
1+
.PHONY: image test citest integration
22

33
IMAGE_NAME ?= codeclimate/codeclimate-eslint
44

55
NPM_TEST_TARGET ?= test
6+
NPM_INTEGRATION_TARGET ?= integration
7+
8+
DEBUG ?= false
9+
ifeq ($(DEBUG),true)
10+
NPM_TEST_TARGET = test.debug
11+
NPM_INTEGRATION_TARGET = integration.debug
12+
endif
613

714
image:
815
docker build --rm -t $(IMAGE_NAME) .
916

17+
integration: image
18+
docker run -ti --rm \
19+
--volume $(PWD):/code \
20+
--workdir /code \
21+
$(IMAGE_NAME) npm run $(NPM_INTEGRATION_TARGET)
22+
1023
test: image
1124
docker run -ti --rm \
1225
--volume $(PWD):/code \
@@ -16,4 +29,4 @@ test: image
1629
citest:
1730
docker run --rm \
1831
--workdir /usr/src/app \
19-
$(IMAGE_NAME) npm run test
32+
$(IMAGE_NAME) sh -c "npm run test && npm run integration"

bin/eslint.js

+3-239
Original file line numberDiff line numberDiff line change
@@ -1,243 +1,7 @@
11
#!/usr/src/app/bin/node_gc
22

3-
var CODE_DIR = "/code";
3+
const CODE_DIR = "/code";
44
process.chdir(CODE_DIR);
55

6-
// Redirect `console.log` so that we are the only ones
7-
// writing to STDOUT
8-
var stdout = console.log;
9-
console.log = console.error;
10-
11-
var eslint = require('../lib/eslint-patch')(require('eslint'));
12-
13-
var CLIEngine = eslint.CLIEngine;
14-
var docs = eslint.docs;
15-
var fs = require("fs");
16-
var glob = require("glob");
17-
var options = { extensions: [".js"], ignore: true, reset: false, useEslintrc: true };
18-
var cli; // instantiation delayed until after options are (potentially) modified
19-
var debug = false;
20-
var BatchSanitizer = require("../lib/batch_sanitizer");
21-
var ignoreWarnings = false;
22-
var ESLINT_WARNING_SEVERITY = 1;
23-
var checks = require("../lib/checks");
24-
var validateConfig = require("../lib/validate_config");
25-
var computeFingerprint = require("../lib/compute_fingerprint");
26-
const ConfigUpgrader = require("../lib/config_upgrader");
27-
28-
// a wrapper for emitting perf timing
29-
function runWithTiming(name, fn) {
30-
var start = new Date()
31-
, rv = fn()
32-
, duration = (new Date() - start) / 1000;
33-
if (debug) {
34-
console.error("eslint.timing." + name + ": " + duration + "s");
35-
}
36-
return rv;
37-
}
38-
39-
function contentBody(check) {
40-
var content = docs.get(check) || "For more information visit ";
41-
return content + "Source: http://eslint.org/docs/rules/\n";
42-
}
43-
44-
function buildIssueJson(message, path) {
45-
// ESLint doesn't emit a ruleId in the
46-
// case of a fatal error (such as an invalid
47-
// token)
48-
var checkName = message.ruleId;
49-
if(message.fatal) {
50-
checkName = "fatal";
51-
}
52-
var line = message.line || 1;
53-
var column = message.column || 1;
54-
55-
var issue = {
56-
type: "issue",
57-
categories: checks.categories(checkName),
58-
check_name: checkName,
59-
description: message.message,
60-
content: {
61-
body: contentBody(checkName)
62-
},
63-
location: {
64-
path: path,
65-
positions: {
66-
begin: {
67-
line: line,
68-
column: column
69-
},
70-
end: {
71-
line: line,
72-
column: column
73-
}
74-
}
75-
},
76-
remediation_points: checks.remediationPoints(checkName, message, cli.getConfigForFile(path))
77-
};
78-
79-
var fingerprint = computeFingerprint(path, checkName, message.message);
80-
81-
if (fingerprint) {
82-
issue["fingerprint"] = fingerprint;
83-
}
84-
85-
return JSON.stringify(issue);
86-
}
87-
88-
function isFileWithMatchingExtension(file, extensions) {
89-
var stats = fs.lstatSync(file);
90-
var extension = "." + file.split(".").pop();
91-
return (
92-
stats.isFile() &&
93-
!stats.isSymbolicLink()
94-
&& extensions.indexOf(extension) >= 0
95-
);
96-
}
97-
98-
function isFileIgnoredByLibrary(file) {
99-
return cli.isPathIgnored(file);
100-
}
101-
102-
function prunePathsWithinSymlinks(paths) {
103-
// Extracts symlinked paths and filters them out, including any child paths
104-
var symlinks = paths.filter(function(path) {
105-
return fs.lstatSync(path).isSymbolicLink();
106-
});
107-
108-
return paths.filter(function(path) {
109-
var withinSymlink = false;
110-
symlinks.forEach(function(symlink) {
111-
if (path.indexOf(symlink) === 0) {
112-
withinSymlink = true;
113-
}
114-
});
115-
return !withinSymlink;
116-
});
117-
}
118-
119-
function inclusionBasedFileListBuilder(includePaths) {
120-
// Uses glob to expand the files and directories in includePaths, filtering
121-
// down to match the list of desired extensions.
122-
return function(extensions) {
123-
var analysisFiles = [];
124-
125-
includePaths.forEach(function(fileOrDirectory, i) {
126-
if ((/\/$/).test(fileOrDirectory)) {
127-
// if it ends in a slash, expand and push
128-
var filesInThisDirectory = glob.sync(
129-
fileOrDirectory + "/**/**"
130-
);
131-
prunePathsWithinSymlinks(filesInThisDirectory).forEach(function(file, j){
132-
if (!isFileIgnoredByLibrary(file) && isFileWithMatchingExtension(file, extensions)) {
133-
analysisFiles.push(file);
134-
}
135-
});
136-
} else {
137-
if (!isFileIgnoredByLibrary(fileOrDirectory) && isFileWithMatchingExtension(fileOrDirectory, extensions)) {
138-
analysisFiles.push(fileOrDirectory);
139-
}
140-
}
141-
});
142-
143-
return analysisFiles;
144-
};
145-
}
146-
147-
var buildFileList;
148-
runWithTiming("engineConfig", function () {
149-
if (fs.existsSync("/config.json")) {
150-
var engineConfig = JSON.parse(fs.readFileSync("/config.json"));
151-
152-
if (engineConfig.include_paths) {
153-
buildFileList = inclusionBasedFileListBuilder(
154-
engineConfig.include_paths
155-
);
156-
} else {
157-
// No explicit includes, let's try with everything
158-
buildFileList = inclusionBasedFileListBuilder(["./"]);
159-
}
160-
161-
var userConfig = engineConfig.config || {};
162-
if (userConfig.config) {
163-
options.configFile = CODE_DIR + "/" + userConfig.config;
164-
options.useEslintrc = false;
165-
}
166-
167-
if (userConfig.extensions) {
168-
options.extensions = userConfig.extensions;
169-
}
170-
171-
if (userConfig.ignore_path) {
172-
options.ignorePath = userConfig.ignore_path;
173-
}
174-
175-
if (userConfig.ignore_warnings) {
176-
ignoreWarnings = true;
177-
}
178-
179-
if (userConfig.debug) {
180-
debug = true;
181-
}
182-
}
183-
184-
cli = new CLIEngine(options);
185-
});
186-
187-
var analysisFiles = runWithTiming("buildFileList", function() {
188-
return buildFileList(options.extensions);
189-
});
190-
191-
function analyzeFiles() {
192-
var batchNum = 0
193-
, batchSize = 10
194-
, batchFiles
195-
, batchReport
196-
, sanitizedBatchFiles;
197-
198-
while(analysisFiles.length > 0) {
199-
batchFiles = analysisFiles.splice(0, batchSize);
200-
sanitizedBatchFiles = (new BatchSanitizer(batchFiles)).sanitizedFiles();
201-
202-
if (debug) {
203-
process.stderr.write("Analyzing: " + batchFiles + "\n");
204-
}
205-
206-
runWithTiming("analyze-batch-" + batchNum, function() {
207-
batchReport = cli.executeOnFiles(sanitizedBatchFiles);
208-
});
209-
runWithTiming("report-batch" + batchNum, function() {
210-
batchReport.results.forEach(function(result) {
211-
var path = result.filePath.replace(/^\/code\//, "");
212-
213-
result.messages.forEach(function(message) {
214-
if (ignoreWarnings && message.severity === ESLINT_WARNING_SEVERITY) { return; }
215-
216-
var issueJson = buildIssueJson(message, path);
217-
process.stdout.write(issueJson + "\u0000\n");
218-
});
219-
});
220-
});
221-
runWithTiming("gc-batch-" + batchNum, function() {
222-
batchFiles = null;
223-
batchReport = null;
224-
global.gc();
225-
});
226-
227-
batchNum++;
228-
}
229-
}
230-
231-
if (validateConfig(options.configFile)) {
232-
console.error("ESLint is running with the " + cli.getConfigForFile(null).parser + " parser.");
233-
234-
for (const line of ConfigUpgrader.upgradeInstructions(options.configFile, analysisFiles, process.cwd())) {
235-
console.error(line);
236-
}
237-
238-
analyzeFiles();
239-
} else {
240-
console.error("No rules are configured. Make sure you have added a config file with rules enabled.");
241-
console.error("See our documentation at https://docs.codeclimate.com/docs/eslint for more information.");
242-
process.exit(1);
243-
}
6+
const ESLint = require("../lib/eslint");
7+
ESLint.run(console, { dir: CODE_DIR });

integration/empty_config/config.json

+10
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
{
2+
"enabled": true,
3+
"config": {
4+
"config": "empty_config/eslintrc.yml",
5+
"debug": "true"
6+
},
7+
"include_paths": [
8+
"/usr/src/app/integration/empty_config/index.js"
9+
]
10+
}

integration/empty_config/index.js

+2
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
function dummy() {
2+
}

integration/eslint_test.js

+64
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
const sinon = require("sinon");
2+
const expect = require("chai").expect;
3+
4+
const ESLint = require('../lib/eslint');
5+
6+
describe("eslint integration", function() {
7+
let consoleMock = {};
8+
9+
function executeConfig(configPath) {
10+
return ESLint.run(consoleMock, { dir: __dirname, configPath: `${__dirname}/${configPath}`});
11+
}
12+
13+
beforeEach(function() {
14+
consoleMock.output = [];
15+
consoleMock.log = function(msg) { consoleMock.output.push(msg) };
16+
consoleMock.error = sinon.spy();
17+
});
18+
19+
describe("eslintrc has not supported plugins", function() {
20+
it("does not raise any error", function() {
21+
this.timeout(3000);
22+
23+
function executeUnsupportedPlugins() {
24+
executeConfig("with_unsupported_plugins/config.json");
25+
}
26+
27+
expect(executeUnsupportedPlugins).to.not.throw();
28+
expect(consoleMock.output).to.not.be.empty;
29+
});
30+
});
31+
32+
describe("validating config", function() {
33+
it("warns about empty config but not raise error", function() {
34+
function executeEmptyConfig() {
35+
executeConfig("empty_config/config.json");
36+
}
37+
38+
expect(executeEmptyConfig).to.not.throw();
39+
sinon.assert.calledWith(consoleMock.error, 'No rules are configured. Make sure you have added a config file with rules enabled.');
40+
});
41+
});
42+
43+
describe("extends plugin", function() {
44+
it("loads the plugin and does not include repeated issues of not found rules", function() {
45+
this.timeout(5000);
46+
executeConfig("extends_airbnb/config.json");
47+
48+
const ruleDefinitionIssues = consoleMock.output.filter(function(o) { return o.includes("Definition for rule"); });
49+
expect(ruleDefinitionIssues).to.be.empty;
50+
});
51+
});
52+
53+
describe("output", function() {
54+
it("is not messed up", function() {
55+
this.timeout(5000);
56+
57+
executeConfig("output_mess/config.json");
58+
59+
expect(consoleMock.output).to.have.lengthOf(1);
60+
expect(consoleMock.output[0]).to.match(/^\{.*/);
61+
});
62+
});
63+
64+
});
+10
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
{
2+
"enabled": true,
3+
"config": {
4+
"config": "extends_airbnb/eslintrc.json",
5+
"debug": "true"
6+
},
7+
"include_paths": [
8+
"/usr/src/app/integration/extends_airbnb/index.js"
9+
]
10+
}
+5
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
{
2+
"extends": "airbnb",
3+
"parser": "babel-eslint",
4+
"rules": {}
5+
}

integration/extends_airbnb/index.js

Whitespace-only changes.

0 commit comments

Comments
 (0)