]>
Commit | Line | Data |
---|---|---|
6f036462 TL |
1 | /* |
2 | * STOP!!! DO NOT MODIFY. | |
3 | * | |
4 | * This file is part of the ongoing work to move the eslintrc-style config | |
5 | * system into the @eslint/eslintrc package. This file needs to remain | |
6 | * unchanged in order for this work to proceed. | |
7 | * | |
8 | * If you think you need to change this file, please contact @nzakas first. | |
9 | * | |
10 | * Thanks in advance for your cooperation. | |
11 | */ | |
12 | ||
eb39fafa DC |
13 | /** |
14 | * @fileoverview Tests for config validator. | |
15 | * @author Brandon Mills | |
16 | */ | |
17 | ||
18 | "use strict"; | |
19 | ||
20 | //------------------------------------------------------------------------------ | |
21 | // Requirements | |
22 | //------------------------------------------------------------------------------ | |
23 | ||
24 | const assert = require("chai").assert, | |
25 | { Linter } = require("../../../lib/linter"), | |
26 | validator = require("../../../lib/shared/config-validator"), | |
27 | Rules = require("../../../lib/linter/rules"); | |
eb39fafa DC |
28 | |
29 | //------------------------------------------------------------------------------ | |
609c276f | 30 | // Helpers |
eb39fafa DC |
31 | //------------------------------------------------------------------------------ |
32 | ||
609c276f TL |
33 | const linter = new Linter(); |
34 | ||
f2a92ac6 DC |
35 | const mockRule = { |
36 | meta: { | |
37 | schema: [{ | |
38 | enum: ["first", "second"] | |
39 | }] | |
40 | }, | |
41 | create(context) { | |
42 | return { | |
43 | Program(node) { | |
44 | context.report(node, "Expected a validation error."); | |
45 | } | |
46 | }; | |
eb39fafa | 47 | } |
eb39fafa DC |
48 | }; |
49 | ||
f2a92ac6 DC |
50 | const mockObjectRule = { |
51 | meta: { | |
52 | schema: { | |
53 | enum: ["first", "second"] | |
eb39fafa | 54 | } |
f2a92ac6 DC |
55 | }, |
56 | create(context) { | |
57 | return { | |
58 | Program(node) { | |
59 | context.report(node, "Expected a validation error."); | |
60 | } | |
61 | }; | |
62 | } | |
63 | }; | |
eb39fafa | 64 | |
f2a92ac6 DC |
65 | const mockNoOptionsRule = { |
66 | meta: { | |
67 | schema: [] | |
68 | }, | |
69 | create(context) { | |
70 | return { | |
71 | Program(node) { | |
72 | context.report(node, "Expected a validation error."); | |
73 | } | |
74 | }; | |
75 | } | |
76 | }; | |
eb39fafa DC |
77 | |
78 | const mockRequiredOptionsRule = { | |
79 | meta: { | |
80 | schema: { | |
81 | type: "array", | |
82 | minItems: 1 | |
83 | } | |
84 | }, | |
85 | create(context) { | |
86 | return { | |
87 | Program(node) { | |
88 | context.report(node, "Expected a validation error."); | |
89 | } | |
90 | }; | |
91 | } | |
92 | }; | |
93 | ||
609c276f TL |
94 | //------------------------------------------------------------------------------ |
95 | // Tests | |
96 | //------------------------------------------------------------------------------ | |
97 | ||
eb39fafa DC |
98 | describe("Validator", () => { |
99 | ||
100 | /** | |
101 | * Gets a loaded rule given a rule ID | |
102 | * @param {string} ruleId The ID of the rule | |
103 | * @returns {{create: Function}} The loaded rule | |
104 | */ | |
105 | function ruleMapper(ruleId) { | |
106 | return linter.getRules().get(ruleId) || new Rules().get(ruleId); | |
107 | } | |
108 | ||
109 | beforeEach(() => { | |
110 | linter.defineRule("mock-rule", mockRule); | |
111 | linter.defineRule("mock-required-options-rule", mockRequiredOptionsRule); | |
112 | }); | |
113 | ||
114 | describe("validate", () => { | |
115 | ||
116 | it("should do nothing with an empty config", () => { | |
117 | validator.validate({}, "tests", ruleMapper); | |
118 | }); | |
119 | ||
120 | it("should do nothing with a valid eslint config", () => { | |
121 | validator.validate( | |
122 | { | |
123 | $schema: "http://json.schemastore.org/eslintrc", | |
124 | root: true, | |
125 | globals: { globalFoo: "readonly" }, | |
126 | parser: "parserFoo", | |
127 | env: { browser: true }, | |
128 | plugins: ["pluginFoo", "pluginBar"], | |
129 | settings: { foo: "bar" }, | |
130 | extends: ["configFoo", "configBar"], | |
131 | parserOptions: { foo: "bar" }, | |
132 | rules: {} | |
133 | }, | |
134 | "tests", | |
135 | ruleMapper | |
136 | ); | |
137 | }); | |
138 | ||
139 | it("should throw with an unknown property", () => { | |
140 | const fn = validator.validate.bind( | |
141 | null, | |
142 | { | |
143 | foo: true | |
144 | }, | |
145 | "tests", | |
146 | ruleMapper | |
147 | ); | |
148 | ||
149 | assert.throws(fn, "Unexpected top-level property \"foo\"."); | |
150 | }); | |
151 | ||
152 | describe("root", () => { | |
153 | it("should throw with a string value", () => { | |
154 | const fn = validator.validate.bind(null, { root: "true" }, null, ruleMapper); | |
155 | ||
156 | assert.throws(fn, "Property \"root\" is the wrong type (expected boolean but got `\"true\"`)."); | |
157 | }); | |
158 | ||
159 | it("should throw with a numeric value", () => { | |
160 | const fn = validator.validate.bind(null, { root: 0 }, null, ruleMapper); | |
161 | ||
162 | assert.throws(fn, "Property \"root\" is the wrong type (expected boolean but got `0`)."); | |
163 | }); | |
164 | }); | |
165 | ||
166 | describe("globals", () => { | |
167 | it("should throw with a string value", () => { | |
168 | const fn = validator.validate.bind(null, { globals: "jQuery" }, null, ruleMapper); | |
169 | ||
170 | assert.throws(fn, "Property \"globals\" is the wrong type (expected object but got `\"jQuery\"`)."); | |
171 | }); | |
172 | ||
173 | it("should throw with an array value", () => { | |
174 | const fn = validator.validate.bind(null, { globals: ["jQuery"] }, null, ruleMapper); | |
175 | ||
176 | assert.throws(fn, "Property \"globals\" is the wrong type (expected object but got `[\"jQuery\"]`)."); | |
177 | }); | |
178 | }); | |
179 | ||
180 | describe("parser", () => { | |
181 | it("should not throw with a null value", () => { | |
182 | validator.validate({ parser: null }, null, ruleMapper); | |
183 | }); | |
184 | }); | |
185 | ||
186 | describe("env", () => { | |
187 | ||
188 | it("should throw with an array environment", () => { | |
189 | const fn = validator.validate.bind(null, { env: [] }, null, ruleMapper); | |
190 | ||
191 | assert.throws(fn, "Property \"env\" is the wrong type (expected object but got `[]`)."); | |
192 | }); | |
193 | ||
194 | it("should throw with a primitive environment", () => { | |
195 | const fn = validator.validate.bind(null, { env: 1 }, null, ruleMapper); | |
196 | ||
197 | assert.throws(fn, "Property \"env\" is the wrong type (expected object but got `1`)."); | |
198 | }); | |
199 | ||
200 | it("should catch invalid environments", () => { | |
201 | const fn = validator.validate.bind(null, { env: { browser: true, invalid: true } }, null, ruleMapper); | |
202 | ||
203 | assert.throws(fn, "Environment key \"invalid\" is unknown\n"); | |
204 | }); | |
205 | ||
206 | it("should catch disabled invalid environments", () => { | |
207 | const fn = validator.validate.bind(null, { env: { browser: true, invalid: false } }, null, ruleMapper); | |
208 | ||
209 | assert.throws(fn, "Environment key \"invalid\" is unknown\n"); | |
210 | }); | |
211 | ||
212 | it("should do nothing with an undefined environment", () => { | |
213 | validator.validate({}, null, ruleMapper); | |
214 | }); | |
215 | ||
216 | }); | |
217 | ||
218 | describe("plugins", () => { | |
219 | it("should not throw with an empty array", () => { | |
220 | validator.validate({ plugins: [] }, null, ruleMapper); | |
221 | }); | |
222 | ||
223 | it("should throw with a string", () => { | |
224 | const fn = validator.validate.bind(null, { plugins: "react" }, null, ruleMapper); | |
225 | ||
226 | assert.throws(fn, "Property \"plugins\" is the wrong type (expected array but got `\"react\"`)."); | |
227 | }); | |
228 | }); | |
229 | ||
230 | describe("settings", () => { | |
231 | it("should not throw with an empty object", () => { | |
232 | validator.validate({ settings: {} }, null, ruleMapper); | |
233 | }); | |
234 | ||
235 | it("should throw with an array", () => { | |
236 | const fn = validator.validate.bind(null, { settings: ["foo"] }, null, ruleMapper); | |
237 | ||
238 | assert.throws(fn, "Property \"settings\" is the wrong type (expected object but got `[\"foo\"]`)."); | |
239 | }); | |
240 | }); | |
241 | ||
242 | describe("extends", () => { | |
243 | it("should not throw with an empty array", () => { | |
244 | validator.validate({ extends: [] }, null, ruleMapper); | |
245 | }); | |
246 | ||
247 | it("should not throw with a string", () => { | |
248 | validator.validate({ extends: "react" }, null, ruleMapper); | |
249 | }); | |
250 | ||
251 | it("should throw with an object", () => { | |
252 | const fn = validator.validate.bind(null, { extends: {} }, null, ruleMapper); | |
253 | ||
254 | assert.throws(fn, "ESLint configuration in null is invalid:\n\t- Property \"extends\" is the wrong type (expected string but got `{}`).\n\t- Property \"extends\" is the wrong type (expected array but got `{}`).\n\t- \"extends\" should match exactly one schema in oneOf. Value: {}."); | |
255 | }); | |
256 | }); | |
257 | ||
258 | describe("parserOptions", () => { | |
259 | it("should not throw with an empty object", () => { | |
260 | validator.validate({ parserOptions: {} }, null, ruleMapper); | |
261 | }); | |
262 | ||
263 | it("should throw with an array", () => { | |
264 | const fn = validator.validate.bind(null, { parserOptions: ["foo"] }, null, ruleMapper); | |
265 | ||
266 | assert.throws(fn, "Property \"parserOptions\" is the wrong type (expected object but got `[\"foo\"]`)."); | |
267 | }); | |
268 | }); | |
269 | ||
270 | describe("rules", () => { | |
271 | ||
272 | it("should do nothing with an empty rules object", () => { | |
273 | validator.validate({ rules: {} }, "tests", ruleMapper); | |
274 | }); | |
275 | ||
276 | it("should do nothing with a valid config with rules", () => { | |
277 | validator.validate({ rules: { "mock-rule": [2, "second"] } }, "tests", ruleMapper); | |
278 | }); | |
279 | ||
280 | it("should do nothing with a valid config when severity is off", () => { | |
281 | validator.validate({ rules: { "mock-rule": ["off", "second"] } }, "tests", ruleMapper); | |
282 | }); | |
283 | ||
284 | it("should do nothing with an invalid config when severity is off", () => { | |
285 | validator.validate({ rules: { "mock-required-options-rule": "off" } }, "tests", ruleMapper); | |
286 | }); | |
287 | ||
288 | it("should do nothing with an invalid config when severity is an array with 'off'", () => { | |
289 | validator.validate({ rules: { "mock-required-options-rule": ["off"] } }, "tests", ruleMapper); | |
290 | }); | |
291 | ||
292 | it("should do nothing with a valid config when severity is warn", () => { | |
293 | validator.validate({ rules: { "mock-rule": ["warn", "second"] } }, "tests", ruleMapper); | |
294 | }); | |
295 | ||
296 | it("should do nothing with a valid config when severity is error", () => { | |
297 | validator.validate({ rules: { "mock-rule": ["error", "second"] } }, "tests", ruleMapper); | |
298 | }); | |
299 | ||
300 | it("should do nothing with a valid config when severity is Off", () => { | |
301 | validator.validate({ rules: { "mock-rule": ["Off", "second"] } }, "tests", ruleMapper); | |
302 | }); | |
303 | ||
304 | it("should do nothing with a valid config when severity is Warn", () => { | |
305 | validator.validate({ rules: { "mock-rule": ["Warn", "second"] } }, "tests", ruleMapper); | |
306 | }); | |
307 | ||
308 | it("should do nothing with a valid config when severity is Error", () => { | |
309 | validator.validate({ rules: { "mock-rule": ["Error", "second"] } }, "tests", ruleMapper); | |
310 | }); | |
311 | ||
312 | it("should catch invalid rule options", () => { | |
313 | const fn = validator.validate.bind(null, { rules: { "mock-rule": [3, "third"] } }, "tests", ruleMapper); | |
314 | ||
315 | assert.throws(fn, "tests:\n\tConfiguration for rule \"mock-rule\" is invalid:\n\tSeverity should be one of the following: 0 = off, 1 = warn, 2 = error (you passed '3').\n"); | |
316 | }); | |
317 | ||
318 | it("should allow for rules with no options", () => { | |
319 | linter.defineRule("mock-no-options-rule", mockNoOptionsRule); | |
320 | ||
321 | validator.validate({ rules: { "mock-no-options-rule": 2 } }, "tests", ruleMapper); | |
322 | }); | |
323 | ||
324 | it("should not allow options for rules with no options", () => { | |
325 | linter.defineRule("mock-no-options-rule", mockNoOptionsRule); | |
326 | ||
327 | const fn = validator.validate.bind(null, { rules: { "mock-no-options-rule": [2, "extra"] } }, "tests", ruleMapper); | |
328 | ||
329 | assert.throws(fn, "tests:\n\tConfiguration for rule \"mock-no-options-rule\" is invalid:\n\tValue [\"extra\"] should NOT have more than 0 items.\n"); | |
330 | }); | |
331 | }); | |
332 | ||
333 | describe("globals", () => { | |
334 | it("should disallow globals set to invalid values", () => { | |
335 | assert.throws( | |
336 | () => validator.validate({ globals: { foo: "AAAAA" } }, "tests", ruleMapper), | |
337 | "ESLint configuration of global 'foo' in tests is invalid:\n'AAAAA' is not a valid configuration for a global (use 'readonly', 'writable', or 'off')" | |
338 | ); | |
339 | }); | |
340 | }); | |
341 | ||
342 | describe("overrides", () => { | |
343 | it("should not throw with an empty overrides array", () => { | |
344 | validator.validate({ overrides: [] }, "tests", ruleMapper); | |
345 | }); | |
346 | ||
347 | it("should not throw with a valid overrides array", () => { | |
348 | validator.validate({ overrides: [{ files: "*", rules: {} }] }, "tests", ruleMapper); | |
349 | }); | |
350 | ||
351 | it("should throw if override does not specify files", () => { | |
352 | const fn = validator.validate.bind(null, { overrides: [{ rules: {} }] }, "tests", ruleMapper); | |
353 | ||
354 | assert.throws(fn, "ESLint configuration in tests is invalid:\n\t- \"overrides[0]\" should have required property 'files'. Value: {\"rules\":{}}.\n"); | |
355 | }); | |
356 | ||
357 | it("should throw if override has an empty files array", () => { | |
358 | const fn = validator.validate.bind(null, { overrides: [{ files: [] }] }, "tests", ruleMapper); | |
359 | ||
360 | assert.throws(fn, "ESLint configuration in tests is invalid:\n\t- Property \"overrides[0].files\" is the wrong type (expected string but got `[]`).\n\t- \"overrides[0].files\" should NOT have fewer than 1 items. Value: [].\n\t- \"overrides[0].files\" should match exactly one schema in oneOf. Value: [].\n"); | |
361 | }); | |
362 | ||
363 | it("should not throw if override has nested overrides", () => { | |
364 | validator.validate({ overrides: [{ files: "*", overrides: [{ files: "*", rules: {} }] }] }, "tests", ruleMapper); | |
365 | }); | |
366 | ||
367 | it("should not throw if override extends", () => { | |
368 | validator.validate({ overrides: [{ files: "*", extends: "eslint-recommended" }] }, "tests", ruleMapper); | |
369 | }); | |
370 | ||
371 | it("should throw if override tries to set root", () => { | |
372 | const fn = validator.validate.bind(null, { overrides: [{ files: "*", root: "true" }] }, "tests", ruleMapper); | |
373 | ||
374 | assert.throws(fn, "ESLint configuration in tests is invalid:\n\t- Unexpected top-level property \"overrides[0].root\".\n"); | |
375 | }); | |
376 | ||
377 | describe("env", () => { | |
378 | ||
379 | it("should catch invalid environments", () => { | |
380 | const fn = validator.validate.bind(null, { overrides: [{ files: "*", env: { browser: true, invalid: true } }] }, null, ruleMapper); | |
381 | ||
382 | assert.throws(fn, "Environment key \"invalid\" is unknown\n"); | |
383 | }); | |
384 | ||
385 | it("should catch disabled invalid environments", () => { | |
386 | const fn = validator.validate.bind(null, { overrides: [{ files: "*", env: { browser: true, invalid: false } }] }, null, ruleMapper); | |
387 | ||
388 | assert.throws(fn, "Environment key \"invalid\" is unknown\n"); | |
389 | }); | |
390 | ||
391 | }); | |
392 | ||
393 | describe("rules", () => { | |
394 | ||
395 | it("should catch invalid rule options", () => { | |
396 | const fn = validator.validate.bind(null, { overrides: [{ files: "*", rules: { "mock-rule": [3, "third"] } }] }, "tests", ruleMapper); | |
397 | ||
398 | assert.throws(fn, "tests:\n\tConfiguration for rule \"mock-rule\" is invalid:\n\tSeverity should be one of the following: 0 = off, 1 = warn, 2 = error (you passed '3').\n"); | |
399 | }); | |
400 | ||
401 | it("should not allow options for rules with no options", () => { | |
402 | linter.defineRule("mock-no-options-rule", mockNoOptionsRule); | |
403 | ||
404 | const fn = validator.validate.bind(null, { overrides: [{ files: "*", rules: { "mock-no-options-rule": [2, "extra"] } }] }, "tests", ruleMapper); | |
405 | ||
406 | assert.throws(fn, "tests:\n\tConfiguration for rule \"mock-no-options-rule\" is invalid:\n\tValue [\"extra\"] should NOT have more than 0 items.\n"); | |
407 | }); | |
408 | }); | |
409 | ||
410 | }); | |
411 | ||
412 | }); | |
413 | ||
414 | describe("getRuleOptionsSchema", () => { | |
415 | ||
416 | it("should return null for a missing rule", () => { | |
417 | assert.strictEqual(validator.getRuleOptionsSchema(ruleMapper("non-existent-rule")), null); | |
418 | }); | |
419 | ||
420 | it("should not modify object schema", () => { | |
421 | linter.defineRule("mock-object-rule", mockObjectRule); | |
422 | assert.deepStrictEqual(validator.getRuleOptionsSchema(ruleMapper("mock-object-rule")), { | |
423 | enum: ["first", "second"] | |
424 | }); | |
425 | }); | |
426 | ||
427 | }); | |
428 | ||
429 | describe("validateRuleOptions", () => { | |
430 | ||
431 | it("should throw for incorrect warning level number", () => { | |
432 | const fn = validator.validateRuleOptions.bind(null, ruleMapper("mock-rule"), "mock-rule", 3, "tests"); | |
433 | ||
434 | assert.throws(fn, "tests:\n\tConfiguration for rule \"mock-rule\" is invalid:\n\tSeverity should be one of the following: 0 = off, 1 = warn, 2 = error (you passed '3').\n"); | |
435 | }); | |
436 | ||
437 | it("should throw for incorrect warning level string", () => { | |
438 | const fn = validator.validateRuleOptions.bind(null, ruleMapper("mock-rule"), "mock-rule", "booya", "tests"); | |
439 | ||
440 | assert.throws(fn, "tests:\n\tConfiguration for rule \"mock-rule\" is invalid:\n\tSeverity should be one of the following: 0 = off, 1 = warn, 2 = error (you passed '\"booya\"').\n"); | |
441 | }); | |
442 | ||
443 | it("should throw for invalid-type warning level", () => { | |
444 | const fn = validator.validateRuleOptions.bind(null, ruleMapper("mock-rule"), "mock-rule", [["error"]], "tests"); | |
445 | ||
446 | assert.throws(fn, "tests:\n\tConfiguration for rule \"mock-rule\" is invalid:\n\tSeverity should be one of the following: 0 = off, 1 = warn, 2 = error (you passed '[ \"error\" ]').\n"); | |
447 | }); | |
448 | ||
449 | it("should only check warning level for nonexistent rules", () => { | |
450 | const fn = validator.validateRuleOptions.bind(null, ruleMapper("non-existent-rule"), "non-existent-rule", [3, "foobar"], "tests"); | |
451 | ||
452 | assert.throws(fn, "tests:\n\tConfiguration for rule \"non-existent-rule\" is invalid:\n\tSeverity should be one of the following: 0 = off, 1 = warn, 2 = error (you passed '3').\n"); | |
453 | }); | |
454 | ||
455 | it("should only check warning level for plugin rules", () => { | |
456 | const fn = validator.validateRuleOptions.bind(null, ruleMapper("plugin/rule"), "plugin/rule", 3, "tests"); | |
457 | ||
458 | assert.throws(fn, "tests:\n\tConfiguration for rule \"plugin/rule\" is invalid:\n\tSeverity should be one of the following: 0 = off, 1 = warn, 2 = error (you passed '3').\n"); | |
459 | }); | |
460 | ||
461 | it("should throw for incorrect configuration values", () => { | |
462 | const fn = validator.validateRuleOptions.bind(null, ruleMapper("mock-rule"), "mock-rule", [2, "frist"], "tests"); | |
463 | ||
464 | assert.throws(fn, "tests:\n\tConfiguration for rule \"mock-rule\" is invalid:\n\tValue \"frist\" should be equal to one of the allowed values.\n"); | |
465 | }); | |
466 | ||
467 | it("should throw for too many configuration values", () => { | |
468 | const fn = validator.validateRuleOptions.bind(null, ruleMapper("mock-rule"), "mock-rule", [2, "first", "second"], "tests"); | |
469 | ||
470 | assert.throws(fn, "tests:\n\tConfiguration for rule \"mock-rule\" is invalid:\n\tValue [\"first\",\"second\"] should NOT have more than 1 items.\n"); | |
471 | }); | |
472 | ||
473 | }); | |
474 | ||
475 | }); |