pve-eslint/eslint/tests/lib/linter/report-translator.js
Thomas Lamprecht 609c276fc2 import 8.3.0 source
Signed-off-by: Thomas Lamprecht <t.lamprecht@proxmox.com>
2021-12-01 13:39:06 +01:00

1095 lines
37 KiB
JavaScript

/**
* @fileoverview Tests for createReportTranslator
* @author Teddy Katz
*/
"use strict";
//------------------------------------------------------------------------------
// Requirements
//------------------------------------------------------------------------------
const assert = require("chai").assert;
const { SourceCode } = require("../../../lib/source-code");
const espree = require("espree");
const createReportTranslator = require("../../../lib/linter/report-translator");
//------------------------------------------------------------------------------
// Tests
//------------------------------------------------------------------------------
describe("createReportTranslator", () => {
/**
* Creates a SourceCode instance out of JavaScript text
* @param {string} text Source text
* @returns {SourceCode} A SourceCode instance for that text
*/
function createSourceCode(text) {
return new SourceCode(
text,
espree.parse(
text.replace(/^\uFEFF/u, ""),
{
loc: true,
range: true,
raw: true,
tokens: true,
comment: true
}
)
);
}
let node, location, message, translateReport, suggestion1, suggestion2;
beforeEach(() => {
const sourceCode = createSourceCode("foo\nbar");
node = sourceCode.ast.body[0];
location = sourceCode.ast.body[1].loc.start;
message = "foo";
suggestion1 = "First suggestion";
suggestion2 = "Second suggestion {{interpolated}}";
translateReport = createReportTranslator({
ruleId: "foo-rule",
severity: 2,
sourceCode,
messageIds: {
testMessage: message,
suggestion1,
suggestion2
}
});
});
describe("old-style call with location", () => {
it("should extract the location correctly", () => {
assert.deepStrictEqual(
translateReport(node, location, message, {}),
{
ruleId: "foo-rule",
severity: 2,
message: "foo",
line: 2,
column: 1,
nodeType: "ExpressionStatement"
}
);
});
});
describe("old-style call without location", () => {
it("should use the start location and end location of the node", () => {
assert.deepStrictEqual(
translateReport(node, message, {}),
{
ruleId: "foo-rule",
severity: 2,
message: "foo",
line: 1,
column: 1,
endLine: 1,
endColumn: 4,
nodeType: "ExpressionStatement"
}
);
});
});
describe("new-style call with all options", () => {
it("should include the new-style options in the report", () => {
const reportDescriptor = {
node,
loc: location,
message,
fix: () => ({ range: [1, 2], text: "foo" }),
suggest: [{
desc: "suggestion 1",
fix: () => ({ range: [2, 3], text: "s1" })
}, {
desc: "suggestion 2",
fix: () => ({ range: [3, 4], text: "s2" })
}]
};
assert.deepStrictEqual(
translateReport(reportDescriptor),
{
ruleId: "foo-rule",
severity: 2,
message: "foo",
line: 2,
column: 1,
nodeType: "ExpressionStatement",
fix: {
range: [1, 2],
text: "foo"
},
suggestions: [{
desc: "suggestion 1",
fix: { range: [2, 3], text: "s1" }
}, {
desc: "suggestion 2",
fix: { range: [3, 4], text: "s2" }
}]
}
);
});
it("should translate the messageId into a message", () => {
const reportDescriptor = {
node,
loc: location,
messageId: "testMessage",
fix: () => ({ range: [1, 2], text: "foo" })
};
assert.deepStrictEqual(
translateReport(reportDescriptor),
{
ruleId: "foo-rule",
severity: 2,
message: "foo",
messageId: "testMessage",
line: 2,
column: 1,
nodeType: "ExpressionStatement",
fix: {
range: [1, 2],
text: "foo"
}
}
);
});
it("should throw when both messageId and message are provided", () => {
const reportDescriptor = {
node,
loc: location,
messageId: "testMessage",
message: "bar",
fix: () => ({ range: [1, 2], text: "foo" })
};
assert.throws(
() => translateReport(reportDescriptor),
TypeError,
"context.report() called with a message and a messageId. Please only pass one."
);
});
it("should throw when an invalid messageId is provided", () => {
const reportDescriptor = {
node,
loc: location,
messageId: "thisIsNotASpecifiedMessageId",
fix: () => ({ range: [1, 2], text: "foo" })
};
assert.throws(
() => translateReport(reportDescriptor),
TypeError,
/^context\.report\(\) called with a messageId of '[^']+' which is not present in the 'messages' config:/u
);
});
it("should throw when no message is provided", () => {
const reportDescriptor = { node };
assert.throws(
() => translateReport(reportDescriptor),
TypeError,
"Missing `message` property in report() call; add a message that describes the linting problem."
);
});
it("should support messageIds for suggestions and output resulting descriptions", () => {
const reportDescriptor = {
node,
loc: location,
message,
suggest: [{
messageId: "suggestion1",
fix: () => ({ range: [2, 3], text: "s1" })
}, {
messageId: "suggestion2",
data: { interpolated: "'interpolated value'" },
fix: () => ({ range: [3, 4], text: "s2" })
}]
};
assert.deepStrictEqual(
translateReport(reportDescriptor),
{
ruleId: "foo-rule",
severity: 2,
message: "foo",
line: 2,
column: 1,
nodeType: "ExpressionStatement",
suggestions: [{
messageId: "suggestion1",
desc: "First suggestion",
fix: { range: [2, 3], text: "s1" }
}, {
messageId: "suggestion2",
data: { interpolated: "'interpolated value'" },
desc: "Second suggestion 'interpolated value'",
fix: { range: [3, 4], text: "s2" }
}]
}
);
});
it("should throw when a suggestion defines both a desc and messageId", () => {
const reportDescriptor = {
node,
loc: location,
message,
suggest: [{
desc: "The description",
messageId: "suggestion1",
fix: () => ({ range: [2, 3], text: "s1" })
}]
};
assert.throws(
() => translateReport(reportDescriptor),
TypeError,
"context.report() called with a suggest option that defines both a 'messageId' and an 'desc'. Please only pass one."
);
});
it("should throw when a suggestion uses an invalid messageId", () => {
const reportDescriptor = {
node,
loc: location,
message,
suggest: [{
messageId: "noMatchingMessage",
fix: () => ({ range: [2, 3], text: "s1" })
}]
};
assert.throws(
() => translateReport(reportDescriptor),
TypeError,
/^context\.report\(\) called with a suggest option with a messageId '[^']+' which is not present in the 'messages' config:/u
);
});
it("should throw when a suggestion does not provide either a desc or messageId", () => {
const reportDescriptor = {
node,
loc: location,
message,
suggest: [{
fix: () => ({ range: [2, 3], text: "s1" })
}]
};
assert.throws(
() => translateReport(reportDescriptor),
TypeError,
"context.report() called with a suggest option that doesn't have either a `desc` or `messageId`"
);
});
it("should throw when a suggestion does not provide a fix function", () => {
const reportDescriptor = {
node,
loc: location,
message,
suggest: [{
desc: "The description",
fix: false
}]
};
assert.throws(
() => translateReport(reportDescriptor),
TypeError,
/^context\.report\(\) called with a suggest option without a fix function. See:/u
);
});
});
describe("combining autofixes", () => {
it("should merge fixes to one if 'fix' function returns an array of fixes.", () => {
const reportDescriptor = {
node,
loc: location,
message,
fix: () => [{ range: [1, 2], text: "foo" }, { range: [4, 5], text: "bar" }]
};
assert.deepStrictEqual(
translateReport(reportDescriptor),
{
ruleId: "foo-rule",
severity: 2,
message: "foo",
line: 2,
column: 1,
nodeType: "ExpressionStatement",
fix: {
range: [1, 5],
text: "fooo\nbar"
}
}
);
});
it("should merge fixes to one if 'fix' function returns an iterator of fixes.", () => {
const reportDescriptor = {
node,
loc: location,
message,
*fix() {
yield { range: [1, 2], text: "foo" };
yield { range: [4, 5], text: "bar" };
}
};
assert.deepStrictEqual(
translateReport(reportDescriptor),
{
ruleId: "foo-rule",
severity: 2,
message: "foo",
line: 2,
column: 1,
nodeType: "ExpressionStatement",
fix: {
range: [1, 5],
text: "fooo\nbar"
}
}
);
});
it("should respect ranges of empty insertions when merging fixes to one.", () => {
const reportDescriptor = {
node,
loc: location,
message,
*fix() {
yield { range: [4, 5], text: "cd" };
yield { range: [2, 2], text: "" };
yield { range: [7, 7], text: "" };
}
};
assert.deepStrictEqual(
translateReport(reportDescriptor),
{
ruleId: "foo-rule",
severity: 2,
message: "foo",
line: 2,
column: 1,
nodeType: "ExpressionStatement",
fix: {
range: [2, 7],
text: "o\ncdar"
}
}
);
});
it("should pass through fixes if only one is present", () => {
const reportDescriptor = {
node,
loc: location,
message,
fix: () => [{ range: [1, 2], text: "foo" }]
};
assert.deepStrictEqual(
translateReport(reportDescriptor),
{
ruleId: "foo-rule",
severity: 2,
message: "foo",
line: 2,
column: 1,
nodeType: "ExpressionStatement",
fix: {
range: [1, 2],
text: "foo"
}
}
);
});
it("should handle inserting BOM correctly.", () => {
const reportDescriptor = {
node,
loc: location,
message,
fix: () => [{ range: [0, 3], text: "\uFEFFfoo" }, { range: [4, 5], text: "x" }]
};
assert.deepStrictEqual(
translateReport(reportDescriptor),
{
ruleId: "foo-rule",
severity: 2,
message: "foo",
line: 2,
column: 1,
nodeType: "ExpressionStatement",
fix: {
range: [0, 5],
text: "\uFEFFfoo\nx"
}
}
);
});
it("should handle removing BOM correctly.", () => {
const sourceCode = createSourceCode("\uFEFFfoo\nbar");
node = sourceCode.ast.body[0];
const reportDescriptor = {
node,
message,
fix: () => [{ range: [-1, 3], text: "foo" }, { range: [4, 5], text: "x" }]
};
assert.deepStrictEqual(
createReportTranslator({ ruleId: "foo-rule", severity: 1, sourceCode })(reportDescriptor),
{
ruleId: "foo-rule",
severity: 1,
message: "foo",
line: 1,
column: 1,
endLine: 1,
endColumn: 4,
nodeType: "ExpressionStatement",
fix: {
range: [-1, 5],
text: "foo\nx"
}
}
);
});
it("should throw an assertion error if ranges are overlapped.", () => {
const reportDescriptor = {
node,
loc: location,
message,
fix: () => [{ range: [0, 3], text: "\uFEFFfoo" }, { range: [2, 5], text: "x" }]
};
assert.throws(
translateReport.bind(null, reportDescriptor),
"Fix objects must not be overlapped in a report."
);
});
it("should include a fix passed as the last argument when location is passed", () => {
assert.deepStrictEqual(
translateReport(
node,
{ line: 42, column: 23 },
"my message {{1}}{{0}}",
["!", "testing"],
() => ({ range: [1, 1], text: "" })
),
{
ruleId: "foo-rule",
severity: 2,
message: "my message testing!",
line: 42,
column: 24,
nodeType: "ExpressionStatement",
fix: {
range: [1, 1],
text: ""
}
}
);
});
});
describe("suggestions", () => {
it("should support multiple suggestions.", () => {
const reportDescriptor = {
node,
loc: location,
message,
suggest: [{
desc: "A first suggestion for the issue",
fix: () => [{ range: [1, 2], text: "foo" }]
}, {
desc: "A different suggestion for the issue",
fix: () => [{ range: [1, 3], text: "foobar" }]
}]
};
assert.deepStrictEqual(
translateReport(reportDescriptor),
{
ruleId: "foo-rule",
severity: 2,
message: "foo",
line: 2,
column: 1,
nodeType: "ExpressionStatement",
suggestions: [{
desc: "A first suggestion for the issue",
fix: { range: [1, 2], text: "foo" }
}, {
desc: "A different suggestion for the issue",
fix: { range: [1, 3], text: "foobar" }
}]
}
);
});
it("should merge suggestion fixes to one if 'fix' function returns an array of fixes.", () => {
const reportDescriptor = {
node,
loc: location,
message,
suggest: [{
desc: "A suggestion for the issue",
fix: () => [{ range: [1, 2], text: "foo" }, { range: [4, 5], text: "bar" }]
}]
};
assert.deepStrictEqual(
translateReport(reportDescriptor),
{
ruleId: "foo-rule",
severity: 2,
message: "foo",
line: 2,
column: 1,
nodeType: "ExpressionStatement",
suggestions: [{
desc: "A suggestion for the issue",
fix: {
range: [1, 5],
text: "fooo\nbar"
}
}]
}
);
});
it("should remove the whole suggestion if 'fix' function returned `null`.", () => {
const reportDescriptor = {
node,
loc: location,
message,
suggest: [{
desc: "A suggestion for the issue",
fix: () => null
}]
};
assert.deepStrictEqual(
translateReport(reportDescriptor),
{
ruleId: "foo-rule",
severity: 2,
message: "foo",
line: 2,
column: 1,
nodeType: "ExpressionStatement"
}
);
});
it("should remove the whole suggestion if 'fix' function returned an empty array.", () => {
const reportDescriptor = {
node,
loc: location,
message,
suggest: [{
desc: "A suggestion for the issue",
fix: () => []
}]
};
assert.deepStrictEqual(
translateReport(reportDescriptor),
{
ruleId: "foo-rule",
severity: 2,
message: "foo",
line: 2,
column: 1,
nodeType: "ExpressionStatement"
}
);
});
it("should remove the whole suggestion if 'fix' function returned an empty sequence.", () => {
const reportDescriptor = {
node,
loc: location,
message,
suggest: [{
desc: "A suggestion for the issue",
*fix() {}
}]
};
assert.deepStrictEqual(
translateReport(reportDescriptor),
{
ruleId: "foo-rule",
severity: 2,
message: "foo",
line: 2,
column: 1,
nodeType: "ExpressionStatement"
}
);
});
// This isn't officially supported, but autofix works the same way
it("should remove the whole suggestion if 'fix' function didn't return anything.", () => {
const reportDescriptor = {
node,
loc: location,
message,
suggest: [{
desc: "A suggestion for the issue",
fix() {}
}]
};
assert.deepStrictEqual(
translateReport(reportDescriptor),
{
ruleId: "foo-rule",
severity: 2,
message: "foo",
line: 2,
column: 1,
nodeType: "ExpressionStatement"
}
);
});
it("should keep suggestion before a removed suggestion.", () => {
const reportDescriptor = {
node,
loc: location,
message,
suggest: [{
desc: "Suggestion with a fix",
fix: () => ({ range: [1, 2], text: "foo" })
}, {
desc: "Suggestion without a fix",
fix: () => null
}]
};
assert.deepStrictEqual(
translateReport(reportDescriptor),
{
ruleId: "foo-rule",
severity: 2,
message: "foo",
line: 2,
column: 1,
nodeType: "ExpressionStatement",
suggestions: [{
desc: "Suggestion with a fix",
fix: { range: [1, 2], text: "foo" }
}]
}
);
});
it("should keep suggestion after a removed suggestion.", () => {
const reportDescriptor = {
node,
loc: location,
message,
suggest: [{
desc: "Suggestion without a fix",
fix: () => null
}, {
desc: "Suggestion with a fix",
fix: () => ({ range: [1, 2], text: "foo" })
}]
};
assert.deepStrictEqual(
translateReport(reportDescriptor),
{
ruleId: "foo-rule",
severity: 2,
message: "foo",
line: 2,
column: 1,
nodeType: "ExpressionStatement",
suggestions: [{
desc: "Suggestion with a fix",
fix: { range: [1, 2], text: "foo" }
}]
}
);
});
it("should remove multiple suggestions that didn't provide a fix and keep those that did.", () => {
const reportDescriptor = {
node,
loc: location,
message,
suggest: [{
desc: "Keep #1",
fix: () => ({ range: [1, 2], text: "foo" })
}, {
desc: "Remove #1",
fix() {
return null;
}
}, {
desc: "Keep #2",
fix: () => ({ range: [1, 2], text: "bar" })
}, {
desc: "Remove #2",
fix() {
return [];
}
}, {
desc: "Keep #3",
fix: () => ({ range: [1, 2], text: "baz" })
}, {
desc: "Remove #3",
*fix() {}
}, {
desc: "Keep #4",
fix: () => ({ range: [1, 2], text: "quux" })
}]
};
assert.deepStrictEqual(
translateReport(reportDescriptor),
{
ruleId: "foo-rule",
severity: 2,
message: "foo",
line: 2,
column: 1,
nodeType: "ExpressionStatement",
suggestions: [{
desc: "Keep #1",
fix: { range: [1, 2], text: "foo" }
}, {
desc: "Keep #2",
fix: { range: [1, 2], text: "bar" }
}, {
desc: "Keep #3",
fix: { range: [1, 2], text: "baz" }
}, {
desc: "Keep #4",
fix: { range: [1, 2], text: "quux" }
}]
}
);
});
});
describe("message interpolation", () => {
it("should correctly parse a message when being passed all options in an old-style report", () => {
assert.deepStrictEqual(
translateReport(node, node.loc.end, "hello {{dynamic}}", { dynamic: node.type }),
{
severity: 2,
ruleId: "foo-rule",
message: "hello ExpressionStatement",
nodeType: "ExpressionStatement",
line: 1,
column: 4
}
);
});
it("should correctly parse a message when being passed all options in a new-style report", () => {
assert.deepStrictEqual(
translateReport({ node, loc: node.loc.end, message: "hello {{dynamic}}", data: { dynamic: node.type } }),
{
severity: 2,
ruleId: "foo-rule",
message: "hello ExpressionStatement",
nodeType: "ExpressionStatement",
line: 1,
column: 4
}
);
});
it("should correctly parse a message with object keys as numbers", () => {
assert.strictEqual(
translateReport(node, "my message {{name}}{{0}}", { 0: "!", name: "testing" }).message,
"my message testing!"
);
});
it("should correctly parse a message with array", () => {
assert.strictEqual(
translateReport(node, "my message {{1}}{{0}}", ["!", "testing"]).message,
"my message testing!"
);
});
it("should allow template parameter with inner whitespace", () => {
assert.strictEqual(
translateReport(node, "message {{parameter name}}", { "parameter name": "yay!" }).message,
"message yay!"
);
});
it("should allow template parameter with non-identifier characters", () => {
assert.strictEqual(
translateReport(node, "message {{parameter-name}}", { "parameter-name": "yay!" }).message,
"message yay!"
);
});
it("should allow template parameter wrapped in braces", () => {
assert.strictEqual(
translateReport(node, "message {{{param}}}", { param: "yay!" }).message,
"message {yay!}"
);
});
it("should ignore template parameter with no specified value", () => {
assert.strictEqual(
translateReport(node, "message {{parameter}}", {}).message,
"message {{parameter}}"
);
});
it("should handle leading whitespace in template parameter", () => {
assert.strictEqual(
translateReport({ node, message: "message {{ parameter}}", data: { parameter: "yay!" } }).message,
"message yay!"
);
});
it("should handle trailing whitespace in template parameter", () => {
assert.strictEqual(
translateReport({ node, message: "message {{parameter }}", data: { parameter: "yay!" } }).message,
"message yay!"
);
});
it("should still allow inner whitespace as well as leading/trailing", () => {
assert.strictEqual(
translateReport(node, "message {{ parameter name }}", { "parameter name": "yay!" }).message,
"message yay!"
);
});
it("should still allow non-identifier characters as well as leading/trailing whitespace", () => {
assert.strictEqual(
translateReport(node, "message {{ parameter-name }}", { "parameter-name": "yay!" }).message,
"message yay!"
);
});
});
describe("location inference", () => {
it("should use the provided location when given in an old-style call", () => {
assert.deepStrictEqual(
translateReport(node, { line: 42, column: 13 }, "hello world"),
{
severity: 2,
ruleId: "foo-rule",
message: "hello world",
nodeType: "ExpressionStatement",
line: 42,
column: 14
}
);
});
it("should use the provided location when given in an new-style call", () => {
assert.deepStrictEqual(
translateReport({ node, loc: { line: 42, column: 13 }, message: "hello world" }),
{
severity: 2,
ruleId: "foo-rule",
message: "hello world",
nodeType: "ExpressionStatement",
line: 42,
column: 14
}
);
});
it("should extract the start and end locations from a node if no location is provided", () => {
assert.deepStrictEqual(
translateReport(node, "hello world"),
{
severity: 2,
ruleId: "foo-rule",
message: "hello world",
nodeType: "ExpressionStatement",
line: 1,
column: 1,
endLine: 1,
endColumn: 4
}
);
});
it("should have 'endLine' and 'endColumn' when 'loc' property has 'end' property.", () => {
assert.deepStrictEqual(
translateReport({ loc: node.loc, message: "hello world" }),
{
severity: 2,
ruleId: "foo-rule",
message: "hello world",
nodeType: null,
line: 1,
column: 1,
endLine: 1,
endColumn: 4
}
);
});
it("should not have 'endLine' and 'endColumn' when 'loc' property does not have 'end' property.", () => {
assert.deepStrictEqual(
translateReport({ loc: node.loc.start, message: "hello world" }),
{
severity: 2,
ruleId: "foo-rule",
message: "hello world",
nodeType: null,
line: 1,
column: 1
}
);
});
it("should infer an 'endLine' and 'endColumn' property when using the object-based context.report API", () => {
assert.deepStrictEqual(
translateReport({ node, message: "hello world" }),
{
severity: 2,
ruleId: "foo-rule",
message: "hello world",
nodeType: "ExpressionStatement",
line: 1,
column: 1,
endLine: 1,
endColumn: 4
}
);
});
});
describe("converting old-style calls", () => {
it("should include a fix passed as the last argument when location is not passed", () => {
assert.deepStrictEqual(
translateReport(node, "my message {{1}}{{0}}", ["!", "testing"], () => ({ range: [1, 1], text: "" })),
{
severity: 2,
ruleId: "foo-rule",
message: "my message testing!",
nodeType: "ExpressionStatement",
line: 1,
column: 1,
endLine: 1,
endColumn: 4,
fix: { range: [1, 1], text: "" }
}
);
});
});
describe("validation", () => {
it("should throw an error if node is not an object", () => {
assert.throws(
() => translateReport("not a node", "hello world"),
"Node must be an object"
);
});
it("should not throw an error if location is provided and node is not in an old-style call", () => {
assert.deepStrictEqual(
translateReport(null, { line: 1, column: 1 }, "hello world"),
{
severity: 2,
ruleId: "foo-rule",
message: "hello world",
nodeType: null,
line: 1,
column: 2
}
);
});
it("should not throw an error if location is provided and node is not in a new-style call", () => {
assert.deepStrictEqual(
translateReport({ loc: { line: 1, column: 1 }, message: "hello world" }),
{
severity: 2,
ruleId: "foo-rule",
message: "hello world",
nodeType: null,
line: 1,
column: 2
}
);
});
it("should throw an error if neither node nor location is provided", () => {
assert.throws(
() => translateReport(null, "hello world"),
"Node must be provided when reporting error if location is not provided"
);
});
it("should throw an error if fix range is invalid", () => {
assert.throws(
() => translateReport({ node, messageId: "testMessage", fix: () => ({ text: "foo" }) }),
"Fix has invalid range"
);
for (const badRange of [[0], [0, null], [null, 0], [void 0, 1], [0, void 0], [void 0, void 0], []]) {
assert.throws(
// eslint-disable-next-line no-loop-func -- Using arrow functions
() => translateReport(
{ node, messageId: "testMessage", fix: () => ({ range: badRange, text: "foo" }) }
),
"Fix has invalid range"
);
assert.throws(
// eslint-disable-next-line no-loop-func -- Using arrow functions
() => translateReport(
{
node,
messageId: "testMessage",
fix: () => [
{ range: [0, 0], text: "foo" },
{ range: badRange, text: "bar" },
{ range: [1, 1], text: "baz" }
]
}
),
"Fix has invalid range"
);
}
});
});
});