From bc435a93afd6ba4def1b53993ef7cf8220f3f070 Mon Sep 17 00:00:00 2001 From: Toru Nagashima Date: Sun, 1 Dec 2019 01:43:13 +0900 Subject: [PATCH] Fix: isSpaceBetweenTokens() recognizes spaces in JSXText (fixes #12614) (#12616) * Fix: isSpaceBetween() recognizes spaces in JSXText (fixes #12614) * apply this fix only for isSpaceBetweenTokens() * move tests to the section for nodes * add tests for isSpaceBetween() * add tests for reversed order --- lib/source-code/source-code.js | 82 +++++++++++------ tests/lib/source-code/source-code.js | 126 +++++++++++++++++++++++++++ 2 files changed, 183 insertions(+), 25 deletions(-) diff --git a/lib/source-code/source-code.js b/lib/source-code/source-code.js index 20b442f2367..30b4e9ab5c2 100644 --- a/lib/source-code/source-code.js +++ b/lib/source-code/source-code.js @@ -90,6 +90,56 @@ function nodesOrTokensOverlap(first, second) { (second.range[0] <= first.range[0] && second.range[1] >= first.range[0]); } +/** + * Determines if two nodes or tokens have at least one whitespace character + * between them. Order does not matter. Returns false if the given nodes or + * tokens overlap. + * @param {SourceCode} sourceCode The source code object. + * @param {ASTNode|Token} first The first node or token to check between. + * @param {ASTNode|Token} second The second node or token to check between. + * @param {boolean} checkInsideOfJSXText If `true` is present, check inside of JSXText tokens for backward compatibility. + * @returns {boolean} True if there is a whitespace character between + * any of the tokens found between the two given nodes or tokens. + * @public + */ +function isSpaceBetween(sourceCode, first, second, checkInsideOfJSXText) { + if (nodesOrTokensOverlap(first, second)) { + return false; + } + + const [startingNodeOrToken, endingNodeOrToken] = first.range[1] <= second.range[0] + ? [first, second] + : [second, first]; + const firstToken = sourceCode.getLastToken(startingNodeOrToken) || startingNodeOrToken; + const finalToken = sourceCode.getFirstToken(endingNodeOrToken) || endingNodeOrToken; + let currentToken = firstToken; + + while (currentToken !== finalToken) { + const nextToken = sourceCode.getTokenAfter(currentToken, { includeComments: true }); + + if ( + currentToken.range[1] !== nextToken.range[0] || + + /* + * For backward compatibility, check speces in JSXText. + * https://github.com/eslint/eslint/issues/12614 + */ + ( + checkInsideOfJSXText && + nextToken !== finalToken && + nextToken.type === "JSXText" && + /\s/u.test(nextToken.value) + ) + ) { + return true; + } + + currentToken = nextToken; + } + + return false; +} + //------------------------------------------------------------------------------ // Public Interface //------------------------------------------------------------------------------ @@ -433,42 +483,24 @@ class SourceCode extends TokenStore { * @public */ isSpaceBetween(first, second) { - if (nodesOrTokensOverlap(first, second)) { - return false; - } - - const [startingNodeOrToken, endingNodeOrToken] = first.range[1] <= second.range[0] - ? [first, second] - : [second, first]; - const firstToken = this.getLastToken(startingNodeOrToken) || startingNodeOrToken; - const finalToken = this.getFirstToken(endingNodeOrToken) || endingNodeOrToken; - let currentToken = firstToken; - - while (currentToken !== finalToken) { - const nextToken = this.getTokenAfter(currentToken, { includeComments: true }); - - if (currentToken.range[1] !== nextToken.range[0]) { - return true; - } - - currentToken = nextToken; - } - - return false; + return isSpaceBetween(this, first, second, false); } /** * Determines if two nodes or tokens have at least one whitespace character * between them. Order does not matter. Returns false if the given nodes or * tokens overlap. - * @param {...ASTNode|Token} args The nodes or tokens to check between. + * For backward compatibility, this method returns true if there are + * `JSXText` tokens that contain whitespaces between the two. + * @param {ASTNode|Token} first The first node or token to check between. + * @param {ASTNode|Token} second The second node or token to check between. * @returns {boolean} True if there is a whitespace character between * any of the tokens found between the two given nodes or tokens. * @deprecated in favor of isSpaceBetween(). * @public */ - isSpaceBetweenTokens(...args) { - return this.isSpaceBetween(...args); + isSpaceBetweenTokens(first, second) { + return isSpaceBetween(this, first, second, true); } /** diff --git a/tests/lib/source-code/source-code.js b/tests/lib/source-code/source-code.js index 29dbf1d6f43..e89f9a3283d 100644 --- a/tests/lib/source-code/source-code.js +++ b/tests/lib/source-code/source-code.js @@ -2076,6 +2076,69 @@ describe("SourceCode", () => { }); }); }); + + it("JSXText tokens that contain only whitespaces should NOT be handled as space", () => { + const code = "let jsx =
\n {content}\n
"; + const ast = espree.parse(code, { ...DEFAULT_CONFIG, ecmaFeatures: { jsx: true } }); + const sourceCode = new SourceCode(code, ast); + const jsx = ast.body[0].declarations[0].init; + const interpolation = jsx.children[1]; + + assert.strictEqual( + sourceCode.isSpaceBetween(jsx.openingElement, interpolation), + false + ); + assert.strictEqual( + sourceCode.isSpaceBetween(interpolation, jsx.closingElement), + false + ); + + // Reversed order + assert.strictEqual( + sourceCode.isSpaceBetween(interpolation, jsx.openingElement), + false + ); + assert.strictEqual( + sourceCode.isSpaceBetween(jsx.closingElement, interpolation), + false + ); + }); + + it("JSXText tokens that contain both letters and whitespaces should NOT be handled as space", () => { + const code = "let jsx =
\n Hello\n
"; + const ast = espree.parse(code, { ...DEFAULT_CONFIG, ecmaFeatures: { jsx: true } }); + const sourceCode = new SourceCode(code, ast); + const jsx = ast.body[0].declarations[0].init; + + assert.strictEqual( + sourceCode.isSpaceBetween(jsx.openingElement, jsx.closingElement), + false + ); + + // Reversed order + assert.strictEqual( + sourceCode.isSpaceBetween(jsx.closingElement, jsx.openingElement), + false + ); + }); + + it("JSXText tokens that contain only letters should NOT be handled as space", () => { + const code = "let jsx =
Hello
"; + const ast = espree.parse(code, { ...DEFAULT_CONFIG, ecmaFeatures: { jsx: true } }); + const sourceCode = new SourceCode(code, ast); + const jsx = ast.body[0].declarations[0].init; + + assert.strictEqual( + sourceCode.isSpaceBetween(jsx.openingElement, jsx.closingElement), + false + ); + + // Reversed order + assert.strictEqual( + sourceCode.isSpaceBetween(jsx.closingElement, jsx.openingElement), + false + ); + }); }); describe("should return false either of the arguments' location is inside the other one", () => { @@ -2409,6 +2472,69 @@ describe("SourceCode", () => { }); }); }); + + it("JSXText tokens that contain only whitespaces should be handled as space", () => { + const code = "let jsx =
\n {content}\n
"; + const ast = espree.parse(code, { ...DEFAULT_CONFIG, ecmaFeatures: { jsx: true } }); + const sourceCode = new SourceCode(code, ast); + const jsx = ast.body[0].declarations[0].init; + const interpolation = jsx.children[1]; + + assert.strictEqual( + sourceCode.isSpaceBetweenTokens(jsx.openingElement, interpolation), + true + ); + assert.strictEqual( + sourceCode.isSpaceBetweenTokens(interpolation, jsx.closingElement), + true + ); + + // Reversed order + assert.strictEqual( + sourceCode.isSpaceBetweenTokens(interpolation, jsx.openingElement), + true + ); + assert.strictEqual( + sourceCode.isSpaceBetweenTokens(jsx.closingElement, interpolation), + true + ); + }); + + it("JSXText tokens that contain both letters and whitespaces should be handled as space", () => { + const code = "let jsx =
\n Hello\n
"; + const ast = espree.parse(code, { ...DEFAULT_CONFIG, ecmaFeatures: { jsx: true } }); + const sourceCode = new SourceCode(code, ast); + const jsx = ast.body[0].declarations[0].init; + + assert.strictEqual( + sourceCode.isSpaceBetweenTokens(jsx.openingElement, jsx.closingElement), + true + ); + + // Reversed order + assert.strictEqual( + sourceCode.isSpaceBetweenTokens(jsx.closingElement, jsx.openingElement), + true + ); + }); + + it("JSXText tokens that contain only letters should NOT be handled as space", () => { + const code = "let jsx =
Hello
"; + const ast = espree.parse(code, { ...DEFAULT_CONFIG, ecmaFeatures: { jsx: true } }); + const sourceCode = new SourceCode(code, ast); + const jsx = ast.body[0].declarations[0].init; + + assert.strictEqual( + sourceCode.isSpaceBetweenTokens(jsx.openingElement, jsx.closingElement), + false + ); + + // Reversed order + assert.strictEqual( + sourceCode.isSpaceBetweenTokens(jsx.closingElement, jsx.openingElement), + false + ); + }); }); describe("should return false either of the arguments' location is inside the other one", () => {