-
-
Notifications
You must be signed in to change notification settings - Fork 4.6k
/
Copy patheslint.js
434 lines (349 loc) · 18.2 KB
/
eslint.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
/**
* @fileoverview Integration tests for the eslint.js executable.
* @author Teddy Katz
*/
"use strict";
//-----------------------------------------------------------------------------
// Requirements
//-----------------------------------------------------------------------------
const childProcess = require("child_process");
const fs = require("fs");
const assert = require("chai").assert;
const path = require("path");
//------------------------------------------------------------------------------
// Data
//------------------------------------------------------------------------------
const EXECUTABLE_PATH = path.resolve(path.join(__dirname, "../../bin/eslint.js"));
//-----------------------------------------------------------------------------
// Helpers
//-----------------------------------------------------------------------------
/**
* Returns a Promise for when a child process exits
* @param {ChildProcess} exitingProcess The child process
* @returns {Promise<number>} A Promise that fulfills with the exit code when the child process exits
*/
function awaitExit(exitingProcess) {
return new Promise(resolve => exitingProcess.once("exit", resolve));
}
/**
* Asserts that the exit code of a given child process will equal the given value.
* @param {ChildProcess} exitingProcess The child process
* @param {number} expectedExitCode The expected exit code of the child process
* @returns {Promise<void>} A Promise that fulfills if the exit code ends up matching, and rejects otherwise.
*/
function assertExitCode(exitingProcess, expectedExitCode) {
return awaitExit(exitingProcess).then(exitCode => {
assert.strictEqual(exitCode, expectedExitCode, `Expected an exit code of ${expectedExitCode} but got ${exitCode}.`);
});
}
/**
* Returns a Promise for the stdout of a process.
* @param {ChildProcess} runningProcess The child process
* @returns {Promise<{stdout: string, stderr: string}>} A Promise that fulfills with all of the
* stdout and stderr output produced by the process when it exits.
*/
function getOutput(runningProcess) {
let stdout = "";
let stderr = "";
runningProcess.stdout.on("data", data => (stdout += data));
runningProcess.stderr.on("data", data => (stderr += data));
return awaitExit(runningProcess).then(() => ({ stdout, stderr }));
}
//------------------------------------------------------------------------------
// Tests
//------------------------------------------------------------------------------
describe("bin/eslint.js", () => {
const forkedProcesses = new Set();
/**
* Forks the process to run an instance of ESLint.
* @param {string[]} [args] An array of arguments
* @param {Object} [options] An object containing options for the resulting child process
* @returns {ChildProcess} The resulting child process
*/
function runESLint(args, options) {
const newProcess = childProcess.fork(EXECUTABLE_PATH, args, Object.assign({ silent: true }, options));
forkedProcesses.add(newProcess);
return newProcess;
}
describe("reading from stdin", () => {
it("has exit code 0 if no linting errors are reported", () => {
const child = runESLint(["--stdin", "--no-config-lookup"]);
child.stdin.write("var foo = bar;\n");
child.stdin.end();
return assertExitCode(child, 0);
});
it("has exit code 0 if no linting errors are reported", () => {
const child = runESLint([
"--stdin",
"--no-config-lookup",
"--rule",
"{'no-extra-semi': 2}",
"--fix-dry-run",
"--format",
"json"
]);
const expectedOutput = JSON.stringify([
{
filePath: "<text>",
messages: [],
suppressedMessages: [],
errorCount: 0,
fatalErrorCount: 0,
warningCount: 0,
fixableErrorCount: 0,
fixableWarningCount: 0,
output: "var foo = bar;\n",
usedDeprecatedRules: []
}
]);
const exitCodePromise = assertExitCode(child, 0);
const stdoutPromise = getOutput(child).then(output => {
assert.strictEqual(output.stdout.trim(), expectedOutput);
assert.strictEqual(output.stderr, "");
});
child.stdin.write("var foo = bar;;\n");
child.stdin.end();
return Promise.all([exitCodePromise, stdoutPromise]);
});
it("has exit code 1 if a syntax error is thrown", () => {
const child = runESLint(["--stdin", "--no-config-lookup"]);
child.stdin.write("This is not valid JS syntax.\n");
child.stdin.end();
return assertExitCode(child, 1);
});
it("has exit code 2 if a syntax error is thrown when exit-on-fatal-error is true", () => {
const child = runESLint(["--stdin", "--no-config-lookup", "--exit-on-fatal-error"]);
child.stdin.write("This is not valid JS syntax.\n");
child.stdin.end();
return assertExitCode(child, 2);
});
it("has exit code 1 if a linting error occurs", () => {
const child = runESLint(["--stdin", "--no-config-lookup", "--rule", "semi:2"]);
child.stdin.write("var foo = bar // <-- no semicolon\n");
child.stdin.end();
return assertExitCode(child, 1);
});
it(
"gives a detailed error message if no config file is found in /",
() => {
if (
fs.readdirSync("/").some(
fileName =>
/^\.eslintrc(?:\.(?:js|yaml|yml|json))?$/u
.test(fileName)
)
) {
return Promise.resolve(true);
}
const child = runESLint(
["--stdin"], { cwd: "/", env: { HOME: "/" } }
);
const exitCodePromise = assertExitCode(child, 2);
const stderrPromise = getOutput(child).then(output => {
assert.match(
output.stderr,
/ESLint couldn't find a configuration file/u
);
});
child.stdin.write("1 < 3;\n");
child.stdin.end();
return Promise.all([exitCodePromise, stderrPromise]);
}
);
it("successfully reads from an asynchronous pipe", () => {
const child = runESLint(["--stdin", "--no-config-lookup"]);
child.stdin.write("var foo = bar;\n");
return new Promise(resolve => setTimeout(resolve, 300)).then(() => {
child.stdin.write("var baz = qux;\n");
child.stdin.end();
return assertExitCode(child, 0);
});
});
it("successfully handles more than 4k data via stdin", () => {
const child = runESLint(["--stdin", "--no-config-lookup"]);
const large = fs.createReadStream(path.join(__dirname, "../bench/large.js"), "utf8");
large.pipe(child.stdin);
return assertExitCode(child, 0);
});
});
describe("running on files", () => {
it("has exit code 0 if no linting errors occur", () => assertExitCode(runESLint(["bin/eslint.js", "--no-config-lookup"]), 0));
it("has exit code 0 if a linting warning is reported", () => assertExitCode(runESLint(["bin/eslint.js", "--no-config-lookup", "--rule", "semi: [1, never]"]), 0));
it("has exit code 1 if a linting error is reported", () => assertExitCode(runESLint(["bin/eslint.js", "--no-config-lookup", "--rule", "semi: [2, never]"]), 1));
it("has exit code 1 if a syntax error is thrown", () => assertExitCode(runESLint(["tests/fixtures/exit-on-fatal-error/fatal-error.js", "--no-config-lookup"]), 1));
});
describe("automatically fixing files", () => {
const fixturesPath = path.join(__dirname, "../fixtures/autofix-integration");
const tempFilePath = `${fixturesPath}/temp.js`;
const startingText = fs.readFileSync(`${fixturesPath}/left-pad.js`).toString();
const expectedFixedText = fs.readFileSync(`${fixturesPath}/left-pad-expected.js`).toString();
const expectedFixedTextQuiet = fs.readFileSync(`${fixturesPath}/left-pad-expected-quiet.js`).toString();
beforeEach(() => {
fs.writeFileSync(tempFilePath, startingText);
});
it("has exit code 0 and fixes a file if all rules can be fixed", () => {
const child = runESLint(["--fix", "--no-config-lookup", "--no-ignore", tempFilePath]);
const exitCodeAssertion = assertExitCode(child, 0);
const outputFileAssertion = awaitExit(child).then(() => {
assert.strictEqual(fs.readFileSync(tempFilePath).toString(), expectedFixedText);
});
return Promise.all([exitCodeAssertion, outputFileAssertion]);
});
it("has exit code 0, fixes errors in a file, and does not report or fix warnings if --quiet and --fix are used", () => {
const child = runESLint(["--fix", "--quiet", "--no-config-lookup", "--no-ignore", tempFilePath]);
const exitCodeAssertion = assertExitCode(child, 0);
const stdoutAssertion = getOutput(child).then(output => assert.strictEqual(output.stdout, ""));
const outputFileAssertion = awaitExit(child).then(() => {
assert.strictEqual(fs.readFileSync(tempFilePath).toString(), expectedFixedTextQuiet);
});
return Promise.all([exitCodeAssertion, stdoutAssertion, outputFileAssertion]);
});
it("has exit code 1 and fixes a file if not all rules can be fixed", () => {
const child = runESLint(["--fix", "--no-config-lookup", "--no-ignore", "--rule", "max-len: [2, 10]", tempFilePath]);
const exitCodeAssertion = assertExitCode(child, 1);
const outputFileAssertion = awaitExit(child).then(() => {
assert.strictEqual(fs.readFileSync(tempFilePath).toString(), expectedFixedText);
});
return Promise.all([exitCodeAssertion, outputFileAssertion]);
});
afterEach(() => {
fs.unlinkSync(tempFilePath);
});
});
describe("cache files", () => {
const CACHE_PATH = ".temp-eslintcache";
const SOURCE_PATH = "tests/fixtures/cache/src/test-file.js";
const ARGS_WITHOUT_CACHE = ["--no-config-lookup", "--no-ignore", SOURCE_PATH, "--cache-location", CACHE_PATH];
const ARGS_WITH_CACHE = ARGS_WITHOUT_CACHE.concat("--cache");
describe("when no cache file exists", () => {
it("creates a cache file when the --cache flag is used", () => {
const child = runESLint(ARGS_WITH_CACHE);
return assertExitCode(child, 0).then(() => {
assert.isTrue(fs.existsSync(CACHE_PATH), "Cache file should exist at the given location");
// Cache file should contain valid JSON
JSON.parse(fs.readFileSync(CACHE_PATH, "utf8"));
});
});
});
describe("when a valid cache file already exists", () => {
beforeEach(() => {
const child = runESLint(ARGS_WITH_CACHE);
return assertExitCode(child, 0).then(() => {
assert.isTrue(fs.existsSync(CACHE_PATH), "Cache file should exist at the given location");
});
});
it("can lint with an existing cache file and the --cache flag", () => {
const child = runESLint(ARGS_WITH_CACHE);
return assertExitCode(child, 0).then(() => {
// Note: This doesn't actually verify that the cache file is used for anything.
assert.isTrue(fs.existsSync(CACHE_PATH), "Cache file should still exist after linting with --cache");
});
});
it("updates the cache file when the source file is modified", () => {
const initialCacheContent = fs.readFileSync(CACHE_PATH, "utf8");
// Update the file to change its mtime
fs.writeFileSync(SOURCE_PATH, fs.readFileSync(SOURCE_PATH, "utf8"));
const child = runESLint(ARGS_WITH_CACHE);
return assertExitCode(child, 0).then(() => {
const newCacheContent = fs.readFileSync(CACHE_PATH, "utf8");
assert.notStrictEqual(initialCacheContent, newCacheContent, "Cache file should change after source is modified");
});
});
it("deletes the cache file when run without the --cache argument", () => {
const child = runESLint(ARGS_WITHOUT_CACHE);
return assertExitCode(child, 0).then(() => {
assert.isFalse(fs.existsSync(CACHE_PATH), "Cache file should be deleted after running ESLint without the --cache argument");
});
});
});
// https://github.com/eslint/eslint/issues/7748
describe("when an invalid cache file already exists", () => {
beforeEach(() => {
fs.writeFileSync(CACHE_PATH, "This is not valid JSON.");
// Sanity check
assert.throws(
() => JSON.parse(fs.readFileSync(CACHE_PATH, "utf8")),
SyntaxError,
/Unexpected token/u,
"Cache file should not contain valid JSON at the start"
);
});
it("overwrites the invalid cache file with a valid one when the --cache argument is used", () => {
const child = runESLint(ARGS_WITH_CACHE);
return assertExitCode(child, 0).then(() => {
assert.isTrue(fs.existsSync(CACHE_PATH), "Cache file should exist at the given location");
// Cache file should contain valid JSON
JSON.parse(fs.readFileSync(CACHE_PATH, "utf8"));
});
});
it("deletes the invalid cache file when the --cache argument is not used", () => {
const child = runESLint(ARGS_WITHOUT_CACHE);
return assertExitCode(child, 0).then(() => {
assert.isFalse(fs.existsSync(CACHE_PATH), "Cache file should be deleted after running ESLint without the --cache argument");
});
});
});
afterEach(() => {
if (fs.existsSync(CACHE_PATH)) {
fs.unlinkSync(CACHE_PATH);
}
});
});
describe("handling crashes", () => {
it("prints the error message to stderr in the event of a crash", () => {
const child = runESLint(["--rule=no-restricted-syntax:[error, 'Invalid Selector [[[']", "--no-config-lookup", "Makefile.js"]);
const exitCodeAssertion = assertExitCode(child, 2);
const outputAssertion = getOutput(child).then(output => {
const expectedSubstring = "Syntax error in selector";
assert.strictEqual(output.stdout, "");
assert.include(output.stderr, expectedSubstring);
});
return Promise.all([exitCodeAssertion, outputAssertion]);
});
it("prints the error message exactly once to stderr in the event of a crash", () => {
const child = runESLint(["--rule=no-restricted-syntax:[error, 'Invalid Selector [[[']", "--no-config-lookup", "Makefile.js"]);
const exitCodeAssertion = assertExitCode(child, 2);
const outputAssertion = getOutput(child).then(output => {
const expectedSubstring = "Syntax error in selector";
assert.strictEqual(output.stdout, "");
assert.include(output.stderr, expectedSubstring);
// The message should appear exactly once in stderr
assert.strictEqual(output.stderr.indexOf(expectedSubstring), output.stderr.lastIndexOf(expectedSubstring));
});
return Promise.all([exitCodeAssertion, outputAssertion]);
});
// https://github.com/eslint/eslint/issues/17560
describe("does not print duplicate errors in the event of a crash", () => {
it("when there is an invalid config read from a config file", () => {
const config = path.join(__dirname, "../fixtures/bin/eslint.config-invalid.js");
const child = runESLint(["--config", config, "conf", "tools"]);
const exitCodeAssertion = assertExitCode(child, 2);
const outputAssertion = getOutput(child).then(output => {
// The error text should appear exactly once in stderr
assert.strictEqual(output.stderr.match(/A config object is using the "globals" key/gu).length, 1);
});
return Promise.all([exitCodeAssertion, outputAssertion]);
});
it("when there is an error in the next tick", () => {
const config = path.join(__dirname, "../fixtures/bin/eslint.config-tick-throws.js");
const child = runESLint(["--config", config, "Makefile.js"]);
const exitCodeAssertion = assertExitCode(child, 2);
const outputAssertion = getOutput(child).then(output => {
// The error text should appear exactly once in stderr
assert.strictEqual(output.stderr.match(/test_error_stack/gu).length, 1);
});
return Promise.all([exitCodeAssertion, outputAssertion]);
});
});
it("prints the error message pointing to line of code", () => {
const invalidConfig = path.join(__dirname, "../fixtures/bin/eslint.config.js");
const child = runESLint(["--no-ignore", "-c", invalidConfig]);
return assertExitCode(child, 2);
});
});
afterEach(() => {
// Clean up all the processes after every test.
forkedProcesses.forEach(child => child.kill());
forkedProcesses.clear();
});
});