]> git.proxmox.com Git - pve-eslint.git/blame - eslint/tests/lib/linter/report-translator.js
exit with error also on warnings
[pve-eslint.git] / eslint / tests / lib / linter / report-translator.js
CommitLineData
eb39fafa
DC
1/**
2 * @fileoverview Tests for createReportTranslator
3 * @author Teddy Katz
4 */
5"use strict";
6
7//------------------------------------------------------------------------------
8// Requirements
9//------------------------------------------------------------------------------
10
11const assert = require("chai").assert;
12const { SourceCode } = require("../../../lib/source-code");
13const espree = require("espree");
14const createReportTranslator = require("../../../lib/linter/report-translator");
15
16//------------------------------------------------------------------------------
17// Tests
18//------------------------------------------------------------------------------
19
20describe("createReportTranslator", () => {
21
22 /**
23 * Creates a SourceCode instance out of JavaScript text
24 * @param {string} text Source text
25 * @returns {SourceCode} A SourceCode instance for that text
26 */
27 function createSourceCode(text) {
28 return new SourceCode(
29 text,
30 espree.parse(
31 text.replace(/^\uFEFF/u, ""),
32 {
33 loc: true,
34 range: true,
35 raw: true,
36 tokens: true,
37 comment: true
38 }
39 )
40 );
41 }
42
43 let node, location, message, translateReport, suggestion1, suggestion2;
44
45 beforeEach(() => {
46 const sourceCode = createSourceCode("foo\nbar");
47
48 node = sourceCode.ast.body[0];
49 location = sourceCode.ast.body[1].loc.start;
50 message = "foo";
51 suggestion1 = "First suggestion";
52 suggestion2 = "Second suggestion {{interpolated}}";
53 translateReport = createReportTranslator({
54 ruleId: "foo-rule",
55 severity: 2,
56 sourceCode,
57 messageIds: {
58 testMessage: message,
59 suggestion1,
60 suggestion2
61 }
62 });
63 });
64
65 describe("old-style call with location", () => {
66 it("should extract the location correctly", () => {
67 assert.deepStrictEqual(
68 translateReport(node, location, message, {}),
69 {
70 ruleId: "foo-rule",
71 severity: 2,
72 message: "foo",
73 line: 2,
74 column: 1,
75 nodeType: "ExpressionStatement"
76 }
77 );
78 });
79 });
80
81 describe("old-style call without location", () => {
82 it("should use the start location and end location of the node", () => {
83 assert.deepStrictEqual(
84 translateReport(node, message, {}),
85 {
86 ruleId: "foo-rule",
87 severity: 2,
88 message: "foo",
89 line: 1,
90 column: 1,
91 endLine: 1,
92 endColumn: 4,
93 nodeType: "ExpressionStatement"
94 }
95 );
96 });
97 });
98
99 describe("new-style call with all options", () => {
100 it("should include the new-style options in the report", () => {
101 const reportDescriptor = {
102 node,
103 loc: location,
104 message,
105 fix: () => ({ range: [1, 2], text: "foo" }),
106 suggest: [{
107 desc: "suggestion 1",
108 fix: () => ({ range: [2, 3], text: "s1" })
109 }, {
110 desc: "suggestion 2",
111 fix: () => ({ range: [3, 4], text: "s2" })
112 }]
113 };
114
115 assert.deepStrictEqual(
116 translateReport(reportDescriptor),
117 {
118 ruleId: "foo-rule",
119 severity: 2,
120 message: "foo",
121 line: 2,
122 column: 1,
123 nodeType: "ExpressionStatement",
124 fix: {
125 range: [1, 2],
126 text: "foo"
127 },
128 suggestions: [{
129 desc: "suggestion 1",
130 fix: { range: [2, 3], text: "s1" }
131 }, {
132 desc: "suggestion 2",
133 fix: { range: [3, 4], text: "s2" }
134 }]
135 }
136 );
137 });
138
139 it("should translate the messageId into a message", () => {
140 const reportDescriptor = {
141 node,
142 loc: location,
143 messageId: "testMessage",
144 fix: () => ({ range: [1, 2], text: "foo" })
145 };
146
147 assert.deepStrictEqual(
148 translateReport(reportDescriptor),
149 {
150 ruleId: "foo-rule",
151 severity: 2,
152 message: "foo",
153 messageId: "testMessage",
154 line: 2,
155 column: 1,
156 nodeType: "ExpressionStatement",
157 fix: {
158 range: [1, 2],
159 text: "foo"
160 }
161 }
162 );
163 });
164
165 it("should throw when both messageId and message are provided", () => {
166 const reportDescriptor = {
167 node,
168 loc: location,
169 messageId: "testMessage",
170 message: "bar",
171 fix: () => ({ range: [1, 2], text: "foo" })
172 };
173
174 assert.throws(
175 () => translateReport(reportDescriptor),
176 TypeError,
177 "context.report() called with a message and a messageId. Please only pass one."
178 );
179 });
180
181 it("should throw when an invalid messageId is provided", () => {
182 const reportDescriptor = {
183 node,
184 loc: location,
185 messageId: "thisIsNotASpecifiedMessageId",
186 fix: () => ({ range: [1, 2], text: "foo" })
187 };
188
189 assert.throws(
190 () => translateReport(reportDescriptor),
191 TypeError,
192 /^context\.report\(\) called with a messageId of '[^']+' which is not present in the 'messages' config:/u
193 );
194 });
195
196 it("should throw when no message is provided", () => {
197 const reportDescriptor = { node };
198
199 assert.throws(
200 () => translateReport(reportDescriptor),
201 TypeError,
202 "Missing `message` property in report() call; add a message that describes the linting problem."
203 );
204 });
205
206 it("should support messageIds for suggestions and output resulting descriptions", () => {
207 const reportDescriptor = {
208 node,
209 loc: location,
210 message,
211 suggest: [{
212 messageId: "suggestion1",
213 fix: () => ({ range: [2, 3], text: "s1" })
214 }, {
215 messageId: "suggestion2",
216 data: { interpolated: "'interpolated value'" },
217 fix: () => ({ range: [3, 4], text: "s2" })
218 }]
219 };
220
221 assert.deepStrictEqual(
222 translateReport(reportDescriptor),
223 {
224 ruleId: "foo-rule",
225 severity: 2,
226 message: "foo",
227 line: 2,
228 column: 1,
229 nodeType: "ExpressionStatement",
230 suggestions: [{
231 messageId: "suggestion1",
232 desc: "First suggestion",
233 fix: { range: [2, 3], text: "s1" }
234 }, {
235 messageId: "suggestion2",
236 data: { interpolated: "'interpolated value'" },
237 desc: "Second suggestion 'interpolated value'",
238 fix: { range: [3, 4], text: "s2" }
239 }]
240 }
241 );
242 });
243
244 it("should throw when a suggestion defines both a desc and messageId", () => {
245 const reportDescriptor = {
246 node,
247 loc: location,
248 message,
249 suggest: [{
250 desc: "The description",
251 messageId: "suggestion1",
252 fix: () => ({ range: [2, 3], text: "s1" })
253 }]
254 };
255
256 assert.throws(
257 () => translateReport(reportDescriptor),
258 TypeError,
259 "context.report() called with a suggest option that defines both a 'messageId' and an 'desc'. Please only pass one."
260 );
261 });
262
263 it("should throw when a suggestion uses an invalid messageId", () => {
264 const reportDescriptor = {
265 node,
266 loc: location,
267 message,
268 suggest: [{
269 messageId: "noMatchingMessage",
270 fix: () => ({ range: [2, 3], text: "s1" })
271 }]
272 };
273
274 assert.throws(
275 () => translateReport(reportDescriptor),
276 TypeError,
277 /^context\.report\(\) called with a suggest option with a messageId '[^']+' which is not present in the 'messages' config:/u
278 );
279 });
280
281 it("should throw when a suggestion does not provide either a desc or messageId", () => {
282 const reportDescriptor = {
283 node,
284 loc: location,
285 message,
286 suggest: [{
287 fix: () => ({ range: [2, 3], text: "s1" })
288 }]
289 };
290
291 assert.throws(
292 () => translateReport(reportDescriptor),
293 TypeError,
294 "context.report() called with a suggest option that doesn't have either a `desc` or `messageId`"
295 );
296 });
297
298 it("should throw when a suggestion does not provide a fix function", () => {
299 const reportDescriptor = {
300 node,
301 loc: location,
302 message,
303 suggest: [{
304 desc: "The description",
305 fix: false
306 }]
307 };
308
309 assert.throws(
310 () => translateReport(reportDescriptor),
311 TypeError,
312 /^context\.report\(\) called with a suggest option without a fix function. See:/u
313 );
314 });
315 });
316
317 describe("combining autofixes", () => {
318 it("should merge fixes to one if 'fix' function returns an array of fixes.", () => {
319 const reportDescriptor = {
320 node,
321 loc: location,
322 message,
323 fix: () => [{ range: [1, 2], text: "foo" }, { range: [4, 5], text: "bar" }]
324 };
325
326 assert.deepStrictEqual(
327 translateReport(reportDescriptor),
328 {
329 ruleId: "foo-rule",
330 severity: 2,
331 message: "foo",
332 line: 2,
333 column: 1,
334 nodeType: "ExpressionStatement",
335 fix: {
336 range: [1, 5],
337 text: "fooo\nbar"
338 }
339 }
340 );
341 });
342
343 it("should merge fixes to one if 'fix' function returns an iterator of fixes.", () => {
344 const reportDescriptor = {
345 node,
346 loc: location,
347 message,
348 *fix() {
349 yield { range: [1, 2], text: "foo" };
350 yield { range: [4, 5], text: "bar" };
351 }
352 };
353
354 assert.deepStrictEqual(
355 translateReport(reportDescriptor),
356 {
357 ruleId: "foo-rule",
358 severity: 2,
359 message: "foo",
360 line: 2,
361 column: 1,
362 nodeType: "ExpressionStatement",
363 fix: {
364 range: [1, 5],
365 text: "fooo\nbar"
366 }
367 }
368 );
369 });
370
371 it("should pass through fixes if only one is present", () => {
372 const reportDescriptor = {
373 node,
374 loc: location,
375 message,
376 fix: () => [{ range: [1, 2], text: "foo" }]
377 };
378
379 assert.deepStrictEqual(
380 translateReport(reportDescriptor),
381 {
382 ruleId: "foo-rule",
383 severity: 2,
384 message: "foo",
385 line: 2,
386 column: 1,
387 nodeType: "ExpressionStatement",
388 fix: {
389 range: [1, 2],
390 text: "foo"
391 }
392 }
393 );
394 });
395
396 it("should handle inserting BOM correctly.", () => {
397 const reportDescriptor = {
398 node,
399 loc: location,
400 message,
401 fix: () => [{ range: [0, 3], text: "\uFEFFfoo" }, { range: [4, 5], text: "x" }]
402 };
403
404 assert.deepStrictEqual(
405 translateReport(reportDescriptor),
406 {
407 ruleId: "foo-rule",
408 severity: 2,
409 message: "foo",
410 line: 2,
411 column: 1,
412 nodeType: "ExpressionStatement",
413 fix: {
414 range: [0, 5],
415 text: "\uFEFFfoo\nx"
416 }
417 }
418 );
419 });
420
421
422 it("should handle removing BOM correctly.", () => {
423 const sourceCode = createSourceCode("\uFEFFfoo\nbar");
424
425 node = sourceCode.ast.body[0];
426
427 const reportDescriptor = {
428 node,
429 message,
430 fix: () => [{ range: [-1, 3], text: "foo" }, { range: [4, 5], text: "x" }]
431 };
432
433 assert.deepStrictEqual(
434 createReportTranslator({ ruleId: "foo-rule", severity: 1, sourceCode })(reportDescriptor),
435 {
436 ruleId: "foo-rule",
437 severity: 1,
438 message: "foo",
439 line: 1,
440 column: 1,
441 endLine: 1,
442 endColumn: 4,
443 nodeType: "ExpressionStatement",
444 fix: {
445 range: [-1, 5],
446 text: "foo\nx"
447 }
448 }
449 );
450 });
451
452 it("should throw an assertion error if ranges are overlapped.", () => {
453 const reportDescriptor = {
454 node,
455 loc: location,
456 message,
457 fix: () => [{ range: [0, 3], text: "\uFEFFfoo" }, { range: [2, 5], text: "x" }]
458 };
459
460 assert.throws(
461 translateReport.bind(null, reportDescriptor),
462 "Fix objects must not be overlapped in a report."
463 );
464 });
465
466 it("should include a fix passed as the last argument when location is passed", () => {
467 assert.deepStrictEqual(
468 translateReport(
469 node,
470 { line: 42, column: 23 },
471 "my message {{1}}{{0}}",
472 ["!", "testing"],
473 () => ({ range: [1, 1], text: "" })
474 ),
475 {
476 ruleId: "foo-rule",
477 severity: 2,
478 message: "my message testing!",
479 line: 42,
480 column: 24,
481 nodeType: "ExpressionStatement",
482 fix: {
483 range: [1, 1],
484 text: ""
485 }
486 }
487 );
488 });
489 });
490
491 describe("suggestions", () => {
492 it("should support multiple suggestions.", () => {
493 const reportDescriptor = {
494 node,
495 loc: location,
496 message,
497 suggest: [{
498 desc: "A first suggestion for the issue",
499 fix: () => [{ range: [1, 2], text: "foo" }]
500 }, {
501 desc: "A different suggestion for the issue",
502 fix: () => [{ range: [1, 3], text: "foobar" }]
503 }]
504 };
505
506 assert.deepStrictEqual(
507 translateReport(reportDescriptor),
508 {
509 ruleId: "foo-rule",
510 severity: 2,
511 message: "foo",
512 line: 2,
513 column: 1,
514 nodeType: "ExpressionStatement",
515 suggestions: [{
516 desc: "A first suggestion for the issue",
517 fix: { range: [1, 2], text: "foo" }
518 }, {
519 desc: "A different suggestion for the issue",
520 fix: { range: [1, 3], text: "foobar" }
521 }]
522 }
523 );
524 });
525
526 it("should merge suggestion fixes to one if 'fix' function returns an array of fixes.", () => {
527 const reportDescriptor = {
528 node,
529 loc: location,
530 message,
531 suggest: [{
532 desc: "A suggestion for the issue",
533 fix: () => [{ range: [1, 2], text: "foo" }, { range: [4, 5], text: "bar" }]
534 }]
535 };
536
537 assert.deepStrictEqual(
538 translateReport(reportDescriptor),
539 {
540 ruleId: "foo-rule",
541 severity: 2,
542 message: "foo",
543 line: 2,
544 column: 1,
545 nodeType: "ExpressionStatement",
546 suggestions: [{
547 desc: "A suggestion for the issue",
548 fix: {
549 range: [1, 5],
550 text: "fooo\nbar"
551 }
552 }]
553 }
554 );
555 });
556 });
557
558 describe("message interpolation", () => {
559 it("should correctly parse a message when being passed all options in an old-style report", () => {
560 assert.deepStrictEqual(
561 translateReport(node, node.loc.end, "hello {{dynamic}}", { dynamic: node.type }),
562 {
563 severity: 2,
564 ruleId: "foo-rule",
565 message: "hello ExpressionStatement",
566 nodeType: "ExpressionStatement",
567 line: 1,
568 column: 4
569 }
570 );
571 });
572
573 it("should correctly parse a message when being passed all options in a new-style report", () => {
574 assert.deepStrictEqual(
575 translateReport({ node, loc: node.loc.end, message: "hello {{dynamic}}", data: { dynamic: node.type } }),
576 {
577 severity: 2,
578 ruleId: "foo-rule",
579 message: "hello ExpressionStatement",
580 nodeType: "ExpressionStatement",
581 line: 1,
582 column: 4
583 }
584 );
585 });
586
587 it("should correctly parse a message with object keys as numbers", () => {
588 assert.strictEqual(
589 translateReport(node, "my message {{name}}{{0}}", { 0: "!", name: "testing" }).message,
590 "my message testing!"
591 );
592 });
593
594 it("should correctly parse a message with array", () => {
595 assert.strictEqual(
596 translateReport(node, "my message {{1}}{{0}}", ["!", "testing"]).message,
597 "my message testing!"
598 );
599 });
600
601 it("should allow template parameter with inner whitespace", () => {
602 assert.strictEqual(
603 translateReport(node, "message {{parameter name}}", { "parameter name": "yay!" }).message,
604 "message yay!"
605 );
606 });
607
608 it("should allow template parameter with non-identifier characters", () => {
609 assert.strictEqual(
610 translateReport(node, "message {{parameter-name}}", { "parameter-name": "yay!" }).message,
611 "message yay!"
612 );
613 });
614
615 it("should allow template parameter wrapped in braces", () => {
616 assert.strictEqual(
617 translateReport(node, "message {{{param}}}", { param: "yay!" }).message,
618 "message {yay!}"
619 );
620 });
621
622 it("should ignore template parameter with no specified value", () => {
623 assert.strictEqual(
624 translateReport(node, "message {{parameter}}", {}).message,
625 "message {{parameter}}"
626 );
627 });
628
629 it("should handle leading whitespace in template parameter", () => {
630 assert.strictEqual(
631 translateReport({ node, message: "message {{ parameter}}", data: { parameter: "yay!" } }).message,
632 "message yay!"
633 );
634 });
635
636 it("should handle trailing whitespace in template parameter", () => {
637 assert.strictEqual(
638 translateReport({ node, message: "message {{parameter }}", data: { parameter: "yay!" } }).message,
639 "message yay!"
640 );
641 });
642
643 it("should still allow inner whitespace as well as leading/trailing", () => {
644 assert.strictEqual(
645 translateReport(node, "message {{ parameter name }}", { "parameter name": "yay!" }).message,
646 "message yay!"
647 );
648 });
649
650 it("should still allow non-identifier characters as well as leading/trailing whitespace", () => {
651 assert.strictEqual(
652 translateReport(node, "message {{ parameter-name }}", { "parameter-name": "yay!" }).message,
653 "message yay!"
654 );
655 });
656 });
657
658 describe("location inference", () => {
659 it("should use the provided location when given in an old-style call", () => {
660 assert.deepStrictEqual(
661 translateReport(node, { line: 42, column: 13 }, "hello world"),
662 {
663 severity: 2,
664 ruleId: "foo-rule",
665 message: "hello world",
666 nodeType: "ExpressionStatement",
667 line: 42,
668 column: 14
669 }
670 );
671 });
672
673 it("should use the provided location when given in an new-style call", () => {
674 assert.deepStrictEqual(
675 translateReport({ node, loc: { line: 42, column: 13 }, message: "hello world" }),
676 {
677 severity: 2,
678 ruleId: "foo-rule",
679 message: "hello world",
680 nodeType: "ExpressionStatement",
681 line: 42,
682 column: 14
683 }
684 );
685 });
686
687 it("should extract the start and end locations from a node if no location is provided", () => {
688 assert.deepStrictEqual(
689 translateReport(node, "hello world"),
690 {
691 severity: 2,
692 ruleId: "foo-rule",
693 message: "hello world",
694 nodeType: "ExpressionStatement",
695 line: 1,
696 column: 1,
697 endLine: 1,
698 endColumn: 4
699 }
700 );
701 });
702
703 it("should have 'endLine' and 'endColumn' when 'loc' property has 'end' property.", () => {
704 assert.deepStrictEqual(
705 translateReport({ loc: node.loc, message: "hello world" }),
706 {
707 severity: 2,
708 ruleId: "foo-rule",
709 message: "hello world",
710 nodeType: null,
711 line: 1,
712 column: 1,
713 endLine: 1,
714 endColumn: 4
715 }
716 );
717 });
718
719 it("should not have 'endLine' and 'endColumn' when 'loc' property does not have 'end' property.", () => {
720 assert.deepStrictEqual(
721 translateReport({ loc: node.loc.start, message: "hello world" }),
722 {
723 severity: 2,
724 ruleId: "foo-rule",
725 message: "hello world",
726 nodeType: null,
727 line: 1,
728 column: 1
729 }
730 );
731 });
732
733 it("should infer an 'endLine' and 'endColumn' property when using the object-based context.report API", () => {
734 assert.deepStrictEqual(
735 translateReport({ node, message: "hello world" }),
736 {
737 severity: 2,
738 ruleId: "foo-rule",
739 message: "hello world",
740 nodeType: "ExpressionStatement",
741 line: 1,
742 column: 1,
743 endLine: 1,
744 endColumn: 4
745 }
746 );
747 });
748 });
749
750 describe("converting old-style calls", () => {
751 it("should include a fix passed as the last argument when location is not passed", () => {
752 assert.deepStrictEqual(
753 translateReport(node, "my message {{1}}{{0}}", ["!", "testing"], () => ({ range: [1, 1], text: "" })),
754 {
755 severity: 2,
756 ruleId: "foo-rule",
757 message: "my message testing!",
758 nodeType: "ExpressionStatement",
759 line: 1,
760 column: 1,
761 endLine: 1,
762 endColumn: 4,
763 fix: { range: [1, 1], text: "" }
764 }
765 );
766 });
767 });
768
769 describe("validation", () => {
770
771 it("should throw an error if node is not an object", () => {
772 assert.throws(
773 () => translateReport("not a node", "hello world"),
774 "Node must be an object"
775 );
776 });
777
778
779 it("should not throw an error if location is provided and node is not in an old-style call", () => {
780 assert.deepStrictEqual(
781 translateReport(null, { line: 1, column: 1 }, "hello world"),
782 {
783 severity: 2,
784 ruleId: "foo-rule",
785 message: "hello world",
786 nodeType: null,
787 line: 1,
788 column: 2
789 }
790 );
791 });
792
793 it("should not throw an error if location is provided and node is not in a new-style call", () => {
794 assert.deepStrictEqual(
795 translateReport({ loc: { line: 1, column: 1 }, message: "hello world" }),
796 {
797 severity: 2,
798 ruleId: "foo-rule",
799 message: "hello world",
800 nodeType: null,
801 line: 1,
802 column: 2
803 }
804 );
805 });
806
807 it("should throw an error if neither node nor location is provided", () => {
808 assert.throws(
809 () => translateReport(null, "hello world"),
810 "Node must be provided when reporting error if location is not provided"
811 );
812 });
813 });
814});