Skip to content

Commit c667055

Browse files
mdjermanovicnzakas
andauthored
fix: provide unique fix and fix.range objects in lint messages (#17332)
* fix: provide unique `fix` and `fix.range` objects in lint messages Fixes #16716 * Update lib/linter/report-translator.js Co-authored-by: Nicholas C. Zakas <nicholas@humanwhocodes.com> --------- Co-authored-by: Nicholas C. Zakas <nicholas@humanwhocodes.com>
1 parent 138c096 commit c667055

File tree

3 files changed

+362
-2
lines changed

3 files changed

+362
-2
lines changed

lib/linter/report-translator.js

Lines changed: 18 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -100,6 +100,22 @@ function normalizeReportLoc(descriptor) {
100100
return descriptor.node.loc;
101101
}
102102

103+
/**
104+
* Clones the given fix object.
105+
* @param {Fix|null} fix The fix to clone.
106+
* @returns {Fix|null} Deep cloned fix object or `null` if `null` or `undefined` was passed in.
107+
*/
108+
function cloneFix(fix) {
109+
if (!fix) {
110+
return null;
111+
}
112+
113+
return {
114+
range: [fix.range[0], fix.range[1]],
115+
text: fix.text
116+
};
117+
}
118+
103119
/**
104120
* Check that a fix has a valid range.
105121
* @param {Fix|null} fix The fix to validate.
@@ -137,7 +153,7 @@ function mergeFixes(fixes, sourceCode) {
137153
return null;
138154
}
139155
if (fixes.length === 1) {
140-
return fixes[0];
156+
return cloneFix(fixes[0]);
141157
}
142158

143159
fixes.sort(compareFixesByRange);
@@ -183,7 +199,7 @@ function normalizeFixes(descriptor, sourceCode) {
183199
}
184200

185201
assertValidFix(fix);
186-
return fix;
202+
return cloneFix(fix);
187203
}
188204

189205
/**

tests/lib/linter/linter.js

Lines changed: 165 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15917,6 +15917,171 @@ var a = "test2";
1591715917

1591815918
assert.strictEqual(suppressedMessages.length, 0);
1591915919
});
15920+
15921+
// https://github.com/eslint/eslint/issues/16716
15922+
it("should receive unique range arrays in suggestions", () => {
15923+
const configs = [
15924+
{
15925+
plugins: {
15926+
"test-processors": {
15927+
processors: {
15928+
"line-processor": (() => {
15929+
const blocksMap = new Map();
15930+
15931+
return {
15932+
preprocess(text, fileName) {
15933+
const lines = text.split("\n");
15934+
15935+
blocksMap.set(fileName, lines);
15936+
15937+
return lines.map((line, index) => ({
15938+
text: line,
15939+
filename: `${index}.js`
15940+
}));
15941+
},
15942+
15943+
postprocess(messageLists, fileName) {
15944+
const lines = blocksMap.get(fileName);
15945+
let rangeOffset = 0;
15946+
15947+
// intentionaly mutates objects and arrays
15948+
messageLists.forEach((messages, index) => {
15949+
messages.forEach(message => {
15950+
message.line += index;
15951+
if (typeof message.endLine === "number") {
15952+
message.endLine += index;
15953+
}
15954+
if (message.fix) {
15955+
message.fix.range[0] += rangeOffset;
15956+
message.fix.range[1] += rangeOffset;
15957+
}
15958+
if (message.suggestions) {
15959+
message.suggestions.forEach(suggestion => {
15960+
suggestion.fix.range[0] += rangeOffset;
15961+
suggestion.fix.range[1] += rangeOffset;
15962+
});
15963+
}
15964+
});
15965+
rangeOffset += lines[index].length + 1;
15966+
});
15967+
15968+
return messageLists.flat();
15969+
},
15970+
15971+
supportsAutofix: true
15972+
};
15973+
})()
15974+
}
15975+
},
15976+
15977+
"test-rules": {
15978+
rules: {
15979+
"no-foo": {
15980+
meta: {
15981+
hasSuggestions: true,
15982+
messages: {
15983+
unexpected: "Don't use 'foo'.",
15984+
replaceWithBar: "Replace with 'bar'",
15985+
replaceWithBaz: "Replace with 'baz'"
15986+
}
15987+
15988+
},
15989+
create(context) {
15990+
return {
15991+
Identifier(node) {
15992+
const { range } = node;
15993+
15994+
if (node.name === "foo") {
15995+
context.report({
15996+
node,
15997+
messageId: "unexpected",
15998+
suggest: [
15999+
{
16000+
messageId: "replaceWithBar",
16001+
fix: () => ({ range, text: "bar" })
16002+
},
16003+
{
16004+
messageId: "replaceWithBaz",
16005+
fix: () => ({ range, text: "baz" })
16006+
}
16007+
]
16008+
});
16009+
}
16010+
}
16011+
};
16012+
}
16013+
}
16014+
}
16015+
}
16016+
}
16017+
},
16018+
{
16019+
files: ["**/*.txt"],
16020+
processor: "test-processors/line-processor"
16021+
},
16022+
{
16023+
files: ["**/*.js"],
16024+
rules: {
16025+
"test-rules/no-foo": 2
16026+
}
16027+
}
16028+
];
16029+
16030+
const result = linter.verifyAndFix(
16031+
"var a = 5;\nvar foo;\nfoo = a;",
16032+
configs,
16033+
{ filename: "a.txt" }
16034+
);
16035+
16036+
assert.deepStrictEqual(result.messages, [
16037+
{
16038+
ruleId: "test-rules/no-foo",
16039+
severity: 2,
16040+
message: "Don't use 'foo'.",
16041+
line: 2,
16042+
column: 5,
16043+
nodeType: "Identifier",
16044+
messageId: "unexpected",
16045+
endLine: 2,
16046+
endColumn: 8,
16047+
suggestions: [
16048+
{
16049+
messageId: "replaceWithBar",
16050+
fix: { range: [15, 18], text: "bar" },
16051+
desc: "Replace with 'bar'"
16052+
},
16053+
{
16054+
messageId: "replaceWithBaz",
16055+
fix: { range: [15, 18], text: "baz" },
16056+
desc: "Replace with 'baz'"
16057+
}
16058+
]
16059+
},
16060+
{
16061+
ruleId: "test-rules/no-foo",
16062+
severity: 2,
16063+
message: "Don't use 'foo'.",
16064+
line: 3,
16065+
column: 1,
16066+
nodeType: "Identifier",
16067+
messageId: "unexpected",
16068+
endLine: 3,
16069+
endColumn: 4,
16070+
suggestions: [
16071+
{
16072+
messageId: "replaceWithBar",
16073+
fix: { range: [20, 23], text: "bar" },
16074+
desc: "Replace with 'bar'"
16075+
},
16076+
{
16077+
messageId: "replaceWithBaz",
16078+
fix: { range: [20, 23], text: "baz" },
16079+
desc: "Replace with 'baz'"
16080+
}
16081+
]
16082+
}
16083+
]);
16084+
});
1592016085
});
1592116086
});
1592216087

tests/lib/linter/report-translator.js

Lines changed: 179 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1091,4 +1091,183 @@ describe("createReportTranslator", () => {
10911091
}
10921092
});
10931093
});
1094+
1095+
// https://github.com/eslint/eslint/issues/16716
1096+
describe("unique `fix` and `fix.range` objects", () => {
1097+
const range = [0, 3];
1098+
const fix = { range, text: "baz" };
1099+
const additionalRange = [4, 7];
1100+
const additionalFix = { range: additionalRange, text: "qux" };
1101+
1102+
it("should deep clone returned fix object", () => {
1103+
const translatedReport = translateReport({
1104+
node,
1105+
messageId: "testMessage",
1106+
fix: () => fix
1107+
});
1108+
1109+
assert.deepStrictEqual(translatedReport.fix, fix);
1110+
assert.notStrictEqual(translatedReport.fix, fix);
1111+
assert.notStrictEqual(translatedReport.fix.range, fix.range);
1112+
});
1113+
1114+
it("should create a new fix object with a new range array when `fix()` returns an array with a single item", () => {
1115+
const translatedReport = translateReport({
1116+
node,
1117+
messageId: "testMessage",
1118+
fix: () => [fix]
1119+
});
1120+
1121+
assert.deepStrictEqual(translatedReport.fix, fix);
1122+
assert.notStrictEqual(translatedReport.fix, fix);
1123+
assert.notStrictEqual(translatedReport.fix.range, fix.range);
1124+
});
1125+
1126+
it("should create a new fix object with a new range array when `fix()` returns an array with multiple items", () => {
1127+
const translatedReport = translateReport({
1128+
node,
1129+
messageId: "testMessage",
1130+
fix: () => [fix, additionalFix]
1131+
});
1132+
1133+
assert.notStrictEqual(translatedReport.fix, fix);
1134+
assert.notStrictEqual(translatedReport.fix.range, fix.range);
1135+
assert.notStrictEqual(translatedReport.fix, additionalFix);
1136+
assert.notStrictEqual(translatedReport.fix.range, additionalFix.range);
1137+
});
1138+
1139+
it("should create a new fix object with a new range array when `fix()` generator yields a single item", () => {
1140+
const translatedReport = translateReport({
1141+
node,
1142+
messageId: "testMessage",
1143+
*fix() {
1144+
yield fix;
1145+
}
1146+
});
1147+
1148+
assert.deepStrictEqual(translatedReport.fix, fix);
1149+
assert.notStrictEqual(translatedReport.fix, fix);
1150+
assert.notStrictEqual(translatedReport.fix.range, fix.range);
1151+
});
1152+
1153+
it("should create a new fix object with a new range array when `fix()` generator yields multiple items", () => {
1154+
const translatedReport = translateReport({
1155+
node,
1156+
messageId: "testMessage",
1157+
*fix() {
1158+
yield fix;
1159+
yield additionalFix;
1160+
}
1161+
});
1162+
1163+
assert.notStrictEqual(translatedReport.fix, fix);
1164+
assert.notStrictEqual(translatedReport.fix.range, fix.range);
1165+
assert.notStrictEqual(translatedReport.fix, additionalFix);
1166+
assert.notStrictEqual(translatedReport.fix.range, additionalFix.range);
1167+
});
1168+
1169+
it("should deep clone returned suggestion fix object", () => {
1170+
const translatedReport = translateReport({
1171+
node,
1172+
messageId: "testMessage",
1173+
suggest: [{
1174+
messageId: "suggestion1",
1175+
fix: () => fix
1176+
}]
1177+
});
1178+
1179+
assert.deepStrictEqual(translatedReport.suggestions[0].fix, fix);
1180+
assert.notStrictEqual(translatedReport.suggestions[0].fix, fix);
1181+
assert.notStrictEqual(translatedReport.suggestions[0].fix.range, fix.range);
1182+
});
1183+
1184+
it("should create a new fix object with a new range array when suggestion `fix()` returns an array with a single item", () => {
1185+
const translatedReport = translateReport({
1186+
node,
1187+
messageId: "testMessage",
1188+
suggest: [{
1189+
messageId: "suggestion1",
1190+
fix: () => [fix]
1191+
}]
1192+
});
1193+
1194+
assert.deepStrictEqual(translatedReport.suggestions[0].fix, fix);
1195+
assert.notStrictEqual(translatedReport.suggestions[0].fix, fix);
1196+
assert.notStrictEqual(translatedReport.suggestions[0].fix.range, fix.range);
1197+
});
1198+
1199+
it("should create a new fix object with a new range array when suggestion `fix()` returns an array with multiple items", () => {
1200+
const translatedReport = translateReport({
1201+
node,
1202+
messageId: "testMessage",
1203+
suggest: [{
1204+
messageId: "suggestion1",
1205+
fix: () => [fix, additionalFix]
1206+
}]
1207+
});
1208+
1209+
assert.notStrictEqual(translatedReport.suggestions[0].fix, fix);
1210+
assert.notStrictEqual(translatedReport.suggestions[0].fix.range, fix.range);
1211+
assert.notStrictEqual(translatedReport.suggestions[0].fix, additionalFix);
1212+
assert.notStrictEqual(translatedReport.suggestions[0].fix.range, additionalFix.range);
1213+
});
1214+
1215+
it("should create a new fix object with a new range array when suggestion `fix()` generator yields a single item", () => {
1216+
const translatedReport = translateReport({
1217+
node,
1218+
messageId: "testMessage",
1219+
suggest: [{
1220+
messageId: "suggestion1",
1221+
*fix() {
1222+
yield fix;
1223+
}
1224+
}]
1225+
});
1226+
1227+
assert.deepStrictEqual(translatedReport.suggestions[0].fix, fix);
1228+
assert.notStrictEqual(translatedReport.suggestions[0].fix, fix);
1229+
assert.notStrictEqual(translatedReport.suggestions[0].fix.range, fix.range);
1230+
});
1231+
1232+
it("should create a new fix object with a new range array when suggestion `fix()` generator yields multiple items", () => {
1233+
const translatedReport = translateReport({
1234+
node,
1235+
messageId: "testMessage",
1236+
suggest: [{
1237+
messageId: "suggestion1",
1238+
*fix() {
1239+
yield fix;
1240+
yield additionalFix;
1241+
}
1242+
}]
1243+
});
1244+
1245+
assert.notStrictEqual(translatedReport.suggestions[0].fix, fix);
1246+
assert.notStrictEqual(translatedReport.suggestions[0].fix.range, fix.range);
1247+
assert.notStrictEqual(translatedReport.suggestions[0].fix, additionalFix);
1248+
assert.notStrictEqual(translatedReport.suggestions[0].fix.range, additionalFix.range);
1249+
});
1250+
1251+
it("should create different instances of range arrays when suggestions reuse the same instance", () => {
1252+
const translatedReport = translateReport({
1253+
node,
1254+
messageId: "testMessage",
1255+
suggest: [
1256+
{
1257+
messageId: "suggestion1",
1258+
fix: () => ({ range, text: "baz" })
1259+
},
1260+
{
1261+
messageId: "suggestion2",
1262+
data: { interpolated: "'interpolated value'" },
1263+
fix: () => ({ range, text: "qux" })
1264+
}
1265+
]
1266+
});
1267+
1268+
assert.deepStrictEqual(translatedReport.suggestions[0].fix.range, range);
1269+
assert.deepStrictEqual(translatedReport.suggestions[1].fix.range, range);
1270+
assert.notStrictEqual(translatedReport.suggestions[0].fix.range, translatedReport.suggestions[1].fix.range);
1271+
});
1272+
});
10941273
});

0 commit comments

Comments
 (0)
pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy