Skip to content

Commit 2f83818

Browse files
fix: Improve handling of significant whitespace in JSX text nodes.
1 parent e0e3841 commit 2f83818

File tree

2 files changed

+144
-8
lines changed

2 files changed

+144
-8
lines changed

transforms/__tests__/suppress-eslint-errors.js

Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -315,6 +315,70 @@ export const Component = (a, b) => {
315315
}`);
316316
});
317317

318+
test('does not split JSX lines containing multiple nodes', () => {
319+
const program = `export function Component({ a, b }) {
320+
return (
321+
<div>
322+
Some text <span>{a == b}</span>.
323+
</div>
324+
);
325+
}`;
326+
327+
expect(modifySource(program)).toBe(`export function Component({ a, b }) {
328+
return (
329+
(<div>
330+
{/* TODO: Fix this the next time the file is edited. */}
331+
{/* eslint-disable-next-line eqeqeq */}
332+
Some text <span>{a == b}</span>.
333+
</div>)
334+
);
335+
}`);
336+
});
337+
338+
test('handles trailing text on the previous line', () => {
339+
const program = `export function Component({ a, b }) {
340+
return (
341+
<div>
342+
<div />Some text
343+
<span>{a == b}</span>.
344+
</div>
345+
);
346+
}`;
347+
348+
expect(modifySource(program)).toBe(`export function Component({ a, b }) {
349+
return (
350+
(<div>
351+
<div />Some text
352+
{/* TODO: Fix this the next time the file is edited. */}
353+
{/* eslint-disable-next-line eqeqeq */}
354+
<span>{a == b}</span>.
355+
</div>)
356+
);
357+
}`);
358+
});
359+
360+
test('preserves significant whitespace in jsx text nodes', () => {
361+
const program = `export function Component({ a, b }) {
362+
return (
363+
<div>
364+
Some text <span>next to a span</span>
365+
<span onClick={() => a == b}>hi</span>.
366+
</div>
367+
);
368+
}`;
369+
370+
expect(modifySource(program)).toBe(`export function Component({ a, b }) {
371+
return (
372+
(<div>
373+
Some text <span>next to a span</span>
374+
{/* TODO: Fix this the next time the file is edited. */}
375+
{/* eslint-disable-next-line eqeqeq */}
376+
<span onClick={() => a == b}>hi</span>.
377+
</div>)
378+
);
379+
}`);
380+
});
381+
318382
const defaultPath = path.resolve(__dirname, 'examples', 'index.js');
319383
function modifySource(source, options) {
320384
const transformOptions = { ...options };

transforms/suppress-eslint-errors.js

Lines changed: 80 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -106,14 +106,82 @@ function addDisableComment(filePath, api, commentText, targetLine, ruleId, path)
106106
}
107107

108108
const { children } = targetPath.parent.value;
109-
if (tryRewriteJsxEslintDisable(children, children.indexOf(targetPath.value), ruleId)) {
109+
110+
// jscodeshift has some bugs around how it handles JSXText nodes that can cause
111+
// it to swallow significant whitespace. Creating whitespace only nodes appears to
112+
// solve the issue.
113+
for (let siblingIndex = children.length - 1; siblingIndex >= 0; siblingIndex--) {
114+
const sibling = children[siblingIndex];
115+
if (sibling.type !== 'JSXText') {
116+
continue;
117+
}
118+
119+
if (sibling.value[0] === '\n' && sibling.value.trim().length === 0) {
120+
continue;
121+
}
122+
123+
const segments = sibling.value.split('\n').flatMap((line) => {
124+
const trimmedLine = line.trimEnd();
125+
if (trimmedLine.length === 0) {
126+
return ['\n'];
127+
}
128+
if (trimmedLine.length === line.length) {
129+
return [line, '\n'];
130+
}
131+
132+
return [trimmedLine, line.substr(trimmedLine.length), '\n'];
133+
});
134+
135+
segments.pop();
136+
137+
children.splice(siblingIndex, 1, ...segments.map((segment) => api.j.jsxText(segment)));
138+
}
139+
140+
let targetIndex = children.indexOf(targetPath.value);
141+
for (let siblingIndex = targetIndex - 1; siblingIndex >= 0; siblingIndex--) {
142+
const sibling = children[siblingIndex];
143+
if (sibling.type === 'JSXText') {
144+
if (sibling.value.indexOf('\n') !== -1) {
145+
break;
146+
}
147+
148+
targetIndex = siblingIndex;
149+
} else if (sibling.loc) {
150+
if (sibling.loc.start.line !== targetLine) {
151+
break;
152+
}
153+
154+
targetIndex = siblingIndex;
155+
}
156+
}
157+
158+
if (tryRewriteJsxEslintDisable(children, targetIndex, ruleId)) {
110159
return;
111160
}
112161

113-
targetPath.insertBefore(createJsxComment(api, commentText));
114-
targetPath.insertBefore(api.j.jsxText('\n'));
115-
targetPath.insertBefore(createJsxComment(api, `eslint-disable-next-line ${ruleId}`));
116-
targetPath.insertBefore(api.j.jsxText('\n'));
162+
const previousSibling = children[targetIndex - 1];
163+
164+
if (previousSibling && previousSibling.type === 'JSXText') {
165+
const textValue = previousSibling.value;
166+
const lastNewline = textValue.lastIndexOf('\n');
167+
if (
168+
lastNewline !== textValue.length - 1 &&
169+
textValue.substr(lastNewline + 1).trim().length === 0
170+
) {
171+
previousSibling.value = textValue.substr(0, lastNewline);
172+
children.splice(targetIndex, 0, api.j.jsxText(textValue.substr(lastNewline)));
173+
targetIndex++;
174+
}
175+
}
176+
177+
children.splice(
178+
targetIndex,
179+
0,
180+
createJsxComment(api, commentText),
181+
api.j.jsxText('\n'),
182+
createJsxComment(api, `eslint-disable-next-line ${ruleId}`),
183+
api.j.jsxText('\n')
184+
);
117185

118186
return;
119187
}
@@ -141,7 +209,7 @@ function tryRewriteJsxEslintDisable(children, targetIndex, ruleId) {
141209

142210
while (currentIndex >= 0) {
143211
const sibling = children[currentIndex];
144-
if (sibling.type === 'JSXText') {
212+
if (sibling.type === 'JSXText' && sibling.value.trim().length === 0) {
145213
currentIndex--;
146214
} else {
147215
if (
@@ -199,6 +267,10 @@ function tryRewriteEslintDisable(targetNode, ruleId) {
199267
function createJsxComment(api, text) {
200268
// The <element> around the curly braces causes this to be parsed as a JSXExpressionContainer
201269
// instead of as a BlockExpression.
202-
return api.j(`<element>{/* ${text} */}</element>`).paths()[0].value.program.body[0].expression
203-
.children[0];
270+
const expressionContainer = api.j(`<element>{/* a comment */}</element>`).paths()[0].value.program
271+
.body[0].expression.children[0];
272+
273+
expressionContainer.expression.innerComments[0].value = ` ${text} `;
274+
275+
return expressionContainer;
204276
}

0 commit comments

Comments
 (0)