Skip to content

Commit f3e9913

Browse files
refactor(linter/plugins): reimplement isSpaceBetween() isSpaceBetweenTokens() using tokens (#16055)
- Follow up to #15498 and #15861 - Note: JSX special case (as part of the `isSpaceBetweenTokens()` method) is being removed in ESLint v10 (see [eslint/eslint#20137](https://github.com/eslint/eslint/pull/20137/files#diff-e85eaebdcf6af45c06572019cbd900cff91518c789049c3bd0e8724c18645ab0)) ### Tasks: - [x] Token-based reimplementation of `isSpaceBetween()` - [x] Token-based reimplementation of `isSpaceBetweenTokens()` - [x] Pass existing tests from #15498 - [x] New tests for JS syntax - [x] New tests for JSX syntax --------- Co-authored-by: overlookmotel <[email protected]>
1 parent 9097167 commit f3e9913

File tree

5 files changed

+274
-58
lines changed

5 files changed

+274
-58
lines changed

apps/oxlint/src-js/plugins/tokens.ts

Lines changed: 110 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
*/
44

55
import { createRequire } from "node:module";
6-
import { sourceText, initSourceText } from "./source_code.js";
6+
import { sourceText } from "./source_code.js";
77
import { debugAssert, debugAssertIsNonNull } from "../utils/asserts.js";
88

99
import type { Comment, Node, NodeOrToken } from "./types.ts";
@@ -1699,9 +1699,7 @@ export function getTokenByRangeStart(
16991699
return null;
17001700
}
17011701

1702-
// Regex that tests for whitespace.
1703-
// TODO: Is this too liberal? Should it be a more constrained set of whitespace characters?
1704-
const WHITESPACE_REGEXP = /\s/;
1702+
const JSX_WHITESPACE_REGEXP = /\s/u;
17051703

17061704
/**
17071705
* Determine if two nodes or tokens have at least one whitespace character between them.
@@ -1712,23 +1710,25 @@ const WHITESPACE_REGEXP = /\s/;
17121710
* Checks for whitespace *between tokens*, not including whitespace *inside tokens*.
17131711
* e.g. Returns `false` for `isSpaceBetween(x, y)` in `x+" "+y`.
17141712
*
1715-
* TODO: Implementation is not quite right at present.
1716-
* We don't use tokens, so return `true` for `isSpaceBetween(x, y)` in `x+" "+y`, but should return `false`.
1717-
* Note: `checkInsideOfJSXText === false` in ESLint's implementation of `sourceCode.isSpaceBetween`.
1718-
* https://github.com/eslint/eslint/blob/523c076866400670fb2192a3f55dbf7ad3469247/lib/languages/js/source-code/source-code.js#L182-L230
1719-
*
1720-
* @param nodeOrToken1 - The first node or token to check between.
1721-
* @param nodeOrToken2 - The second node or token to check between.
1713+
* @param first - The first node or token to check between.
1714+
* @param second - The second node or token to check between.
17221715
* @returns `true` if there is a whitespace character between
17231716
* any of the tokens found between the two given nodes or tokens.
17241717
*/
1725-
export function isSpaceBetween(nodeOrToken1: NodeOrToken, nodeOrToken2: NodeOrToken): boolean {
1726-
const range1 = nodeOrToken1.range,
1727-
range2 = nodeOrToken2.range,
1728-
start1 = range1[0],
1729-
start2 = range2[0];
1718+
export function isSpaceBetween(first: NodeOrToken, second: NodeOrToken): boolean {
1719+
if (tokensWithComments === null) {
1720+
if (tokens === null) initTokens();
1721+
initTokensWithComments();
1722+
}
1723+
debugAssertIsNonNull(tokensWithComments);
17301724

1731-
// Find the gap between the two nodes/tokens.
1725+
const range1 = first.range,
1726+
range2 = second.range;
1727+
1728+
// Find the range between the two nodes/tokens.
1729+
//
1730+
// Unlike other methods which require the user to pass the nodes in order of appearance,
1731+
// `isSpaceBetween()` is invariant over the sequence of the two nodes.
17321732
//
17331733
// 1 node/token can completely enclose another, but they can't *partially* overlap.
17341734
// ```
@@ -1741,23 +1741,43 @@ export function isSpaceBetween(nodeOrToken1: NodeOrToken, nodeOrToken2: NodeOrTo
17411741
// |------------|
17421742
// ```
17431743
// We use that invariant to reduce this to a single branch.
1744-
let gapStart, gapEnd;
1745-
if (start1 < start2) {
1746-
gapStart = range1[1]; // end1
1747-
gapEnd = start2;
1744+
let rangeStart: number = range1[0],
1745+
rangeEnd: number = range2[0];
1746+
if (rangeStart < rangeEnd) {
1747+
rangeStart = range1[1];
17481748
} else {
1749-
gapStart = range2[1]; // end2;
1750-
gapEnd = start1;
1749+
rangeEnd = rangeStart;
1750+
rangeStart = range2[1];
1751+
}
1752+
1753+
// Binary search for the first token past `rangeStart`.
1754+
// Unless `first` and `second` are adjacent or overlapping,
1755+
// the token will be the first token between the two nodes.
1756+
const tokensWithCommentsLength = tokensWithComments.length;
1757+
let tokenBetweenIndex = tokensWithCommentsLength;
1758+
for (let lo = 0; lo < tokenBetweenIndex; ) {
1759+
const mid = (lo + tokenBetweenIndex) >> 1;
1760+
if (tokensWithComments[mid].range[0] < rangeStart) {
1761+
lo = mid + 1;
1762+
} else {
1763+
tokenBetweenIndex = mid;
1764+
}
17511765
}
17521766

1753-
// If `gapStart >= gapEnd`, one node encloses the other, or the two are directly adjacent
1754-
if (gapStart >= gapEnd) return false;
1755-
1756-
// Check if there's any whitespace in the gap
1757-
if (sourceText === null) initSourceText();
1758-
debugAssertIsNonNull(sourceText);
1767+
for (
1768+
let lastTokenEnd = rangeStart;
1769+
tokenBetweenIndex < tokensWithCommentsLength;
1770+
tokenBetweenIndex++
1771+
) {
1772+
const { range } = tokensWithComments[tokenBetweenIndex];
1773+
const tokenStart = range[0];
1774+
// The first token of the later node should undergo the check in the second branch
1775+
if (tokenStart > rangeEnd) break;
1776+
if (tokenStart !== lastTokenEnd) return true;
1777+
lastTokenEnd = range[1];
1778+
}
17591779

1760-
return WHITESPACE_REGEXP.test(sourceText.slice(gapStart, gapEnd));
1780+
return false;
17611781
}
17621782

17631783
/**
@@ -1775,13 +1795,67 @@ export function isSpaceBetween(nodeOrToken1: NodeOrToken, nodeOrToken2: NodeOrTo
17751795
*
17761796
* @deprecated Use `sourceCode.isSpaceBetween` instead.
17771797
*
1778-
* TODO: Implementation is not quite right at present, for same reasons as `SourceCode#isSpaceBetween`.
1779-
*
1780-
* @param nodeOrToken1 - The first node or token to check between.
1781-
* @param nodeOrToken2 - The second node or token to check between.
1798+
* @param first - The first node or token to check between.
1799+
* @param second - The second node or token to check between.
17821800
* @returns `true` if there is a whitespace character between
17831801
* any of the tokens found between the two given nodes or tokens.
17841802
*/
1785-
export function isSpaceBetweenTokens(token1: NodeOrToken, token2: NodeOrToken): boolean {
1786-
return isSpaceBetween(token1, token2);
1803+
export function isSpaceBetweenTokens(first: NodeOrToken, second: NodeOrToken): boolean {
1804+
if (tokensWithComments === null) {
1805+
if (tokens === null) initTokens();
1806+
initTokensWithComments();
1807+
}
1808+
debugAssertIsNonNull(tokensWithComments);
1809+
1810+
const range1 = first.range,
1811+
range2 = second.range;
1812+
1813+
// Find the range between the two nodes/tokens.
1814+
// Unlike other methods which require the user to pass the nodes in order of appearance,
1815+
// `isSpaceBetweenTokens()` is invariant over the sequence of the two nodes.
1816+
// See comment in `isSpaceBetween` about why this is a single branch.
1817+
let rangeStart: number = range1[0],
1818+
rangeEnd: number = range2[0];
1819+
if (rangeStart < rangeEnd) {
1820+
rangeStart = range1[1];
1821+
} else {
1822+
rangeEnd = rangeStart;
1823+
rangeStart = range2[1];
1824+
}
1825+
1826+
// Binary search for the first token past `rangeStart`.
1827+
// Unless `first` and `second` are adjacent or overlapping,
1828+
// the token will be the first token between the two nodes.
1829+
const tokensWithCommentsLength = tokensWithComments.length;
1830+
let tokenBetweenIndex = tokensWithCommentsLength;
1831+
for (let lo = 0; lo < tokenBetweenIndex; ) {
1832+
const mid = (lo + tokenBetweenIndex) >> 1;
1833+
if (tokensWithComments[mid].range[0] < rangeStart) {
1834+
lo = mid + 1;
1835+
} else {
1836+
tokenBetweenIndex = mid;
1837+
}
1838+
}
1839+
1840+
for (
1841+
let lastTokenEnd = rangeStart;
1842+
tokenBetweenIndex < tokensWithCommentsLength;
1843+
tokenBetweenIndex++
1844+
) {
1845+
const token = tokensWithComments[tokenBetweenIndex],
1846+
{ range } = token,
1847+
tokenStart = range[0];
1848+
1849+
// The first token of the later node should undergo the check in the second branch
1850+
if (tokenStart > rangeEnd) break;
1851+
if (
1852+
tokenStart !== lastTokenEnd ||
1853+
(token.type === "JSXText" && JSX_WHITESPACE_REGEXP.test(token.value))
1854+
) {
1855+
return true;
1856+
}
1857+
lastTokenEnd = range[1];
1858+
}
1859+
1860+
return false;
17871861
}

apps/oxlint/test/fixtures/isSpaceBetween/files/index.js

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,5 @@ newlineAfter
2121
// prettier-ignore
2222
nested = 7 + 8;
2323

24-
// We should return `false` for `isSpaceBetween(beforeString, afterString)`, but we currently return `true`
2524
// prettier-ignore
2625
beforeString," ",afterString;
Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,7 @@
11
<Foo>aaa</Foo>;
22

3-
// We should return `false` for `isSpaceBetween(openingElement, closingElement)`, but we currently return `true`
43
<Bar>b c</Bar>;
54

6-
// We should return `false` for `isSpaceBetween(openingElement, closingElement)`, but we currently return `true`
75
// prettier-ignore
86
<Qux>d
97
e</Qux>;

apps/oxlint/test/fixtures/isSpaceBetween/output.snap.md

Lines changed: 19 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -168,13 +168,13 @@
168168
`----
169169
170170
x test-plugin(is-space-between):
171-
| isSpaceBetween(beforeString, afterString): true
172-
| isSpaceBetweenTokens(beforeString, afterString): true
173-
| isSpaceBetween(afterString, beforeString): true
174-
| isSpaceBetweenTokens(afterString, beforeString): true
175-
,-[files/index.js:26:1]
176-
25 | // prettier-ignore
177-
26 | beforeString," ",afterString;
171+
| isSpaceBetween(beforeString, afterString): false
172+
| isSpaceBetweenTokens(beforeString, afterString): false
173+
| isSpaceBetween(afterString, beforeString): false
174+
| isSpaceBetweenTokens(afterString, beforeString): false
175+
,-[files/index.js:25:1]
176+
24 | // prettier-ignore
177+
25 | beforeString," ",afterString;
178178
: ^^^^^^^^^^^^^^^^^^^^^^^^^^^^
179179
`----
180180
@@ -190,26 +190,26 @@
190190
`----
191191
192192
x test-plugin(is-space-between):
193-
| isSpaceBetween(openingElement, closingElement): true
193+
| isSpaceBetween(openingElement, closingElement): false
194194
| isSpaceBetweenTokens(openingElement, closingElement): true
195-
| isSpaceBetween(closingElement, openingElement): true
195+
| isSpaceBetween(closingElement, openingElement): false
196196
| isSpaceBetweenTokens(closingElement, openingElement): true
197-
,-[files/index.jsx:4:1]
198-
3 | // We should return `false` for `isSpaceBetween(openingElement, closingElement)`, but we currently return `true`
199-
4 | <Bar>b c</Bar>;
197+
,-[files/index.jsx:3:1]
198+
2 |
199+
3 | <Bar>b c</Bar>;
200200
: ^^^^^^^^^^^^^^
201-
5 |
201+
4 |
202202
`----
203203
204204
x test-plugin(is-space-between):
205-
| isSpaceBetween(openingElement, closingElement): true
205+
| isSpaceBetween(openingElement, closingElement): false
206206
| isSpaceBetweenTokens(openingElement, closingElement): true
207-
| isSpaceBetween(closingElement, openingElement): true
207+
| isSpaceBetween(closingElement, openingElement): false
208208
| isSpaceBetweenTokens(closingElement, openingElement): true
209-
,-[files/index.jsx:8:1]
210-
7 | // prettier-ignore
211-
8 | ,-> <Qux>d
212-
9 | `-> e</Qux>;
209+
,-[files/index.jsx:6:1]
210+
5 | // prettier-ignore
211+
6 | ,-> <Qux>d
212+
7 | `-> e</Qux>;
213213
`----
214214
215215
Found 0 warnings and 13 errors.

0 commit comments

Comments
 (0)