diff --git a/CHANGELOG.md b/CHANGELOG.md index ec63cda6c3..84238623a1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -14,6 +14,7 @@ This change log adheres to standards from [Keep a CHANGELOG](http://keepachangel * [`destructuring-assignment`], [`no-multi-comp`], [`no-unstable-nested-components`], component detection: improve component detection ([#3001][] @vedadeepta) * [`no-deprecated`]: fix crash on rest elements ([#3016][] @ljharb) * [`destructuring-assignment`]: get the contextName correctly ([#3025][] @ohhoney1) +* [`no-typos`]: prevent crash on styled components and forwardRefs ([#3036][] @ljharb) ### Changed * [Docs] [`jsx-no-bind`]: updates discussion of refs ([#2998][] @dimitropoulos) @@ -21,6 +22,7 @@ This change log adheres to standards from [Keep a CHANGELOG](http://keepachangel * [Docs] [`jsx-uses-react`], [`react-in-jsx-scope`]: document [`react/jsx-runtime`] config ([#3018][] @pkuczynski @ljharb) * [Docs] [`require-default-props`]: fix small typo ([#2994][] @evsasse) +[#3036]: https://github.com/yannickcr/eslint-plugin-react/issues/3036 [#3026]: https://github.com/yannickcr/eslint-plugin-react/pull/3026 [#3025]: https://github.com/yannickcr/eslint-plugin-react/pull/3025 [#3018]: https://github.com/yannickcr/eslint-plugin-react/pull/3018 diff --git a/lib/util/Components.js b/lib/util/Components.js index 8bf9372678..bb33b6baf5 100644 --- a/lib/util/Components.js +++ b/lib/util/Components.js @@ -410,11 +410,11 @@ function componentRule(rule, context) { }, isReturningJSX(ASTNode, strict) { - return jsxUtil.isReturningJSX(this.isCreateElement.bind(this), ASTNode, strict, true); + return jsxUtil.isReturningJSX(this.isCreateElement.bind(this), ASTNode, context, strict, true); }, isReturningJSXOrNull(ASTNode, strict) { - return jsxUtil.isReturningJSX(this.isCreateElement.bind(this), ASTNode, strict); + return jsxUtil.isReturningJSX(this.isCreateElement.bind(this), ASTNode, context, strict); }, getPragmaComponentWrapper(node) { diff --git a/lib/util/ast.js b/lib/util/ast.js index fb740f0b72..afc0e8ece5 100644 --- a/lib/util/ast.js +++ b/lib/util/ast.js @@ -5,6 +5,7 @@ 'use strict'; const estraverse = require('estraverse'); +// const pragmaUtil = require('./pragma'); /** * Wrapper for estraverse.traverse @@ -65,10 +66,11 @@ function findReturnStatement(node) { * returned expression in the case of an arrow function) of a function * * @param {ASTNode} ASTNode The AST node being checked + * @param {Context} context The context of `ASTNode`. * @param {function} enterFunc Function to execute for each returnStatement found * @returns {undefined} */ -function traverseReturns(ASTNode, enterFunc) { +function traverseReturns(ASTNode, context, enterFunc) { const nodeType = ASTNode.type; if (nodeType === 'ReturnStatement') { @@ -79,12 +81,31 @@ function traverseReturns(ASTNode, enterFunc) { return enterFunc(ASTNode.body); } - if (nodeType !== 'FunctionExpression' - && nodeType !== 'FunctionDeclaration' - && nodeType !== 'ArrowFunctionExpression' - && nodeType !== 'MethodDefinition' + /* TODO: properly warn on React.forwardRefs having typo properties + if (nodeType === 'CallExpression') { + const callee = ASTNode.callee; + const pragma = pragmaUtil.getFromContext(context); + if ( + callee.type === 'MemberExpression' + && callee.object.type === 'Identifier' + && callee.object.name === pragma + && callee.property.type === 'Identifier' + && callee.property.name === 'forwardRef' + && ASTNode.arguments.length > 0 + ) { + return enterFunc(ASTNode.arguments[0]); + } + return; + } + */ + + if ( + nodeType !== 'FunctionExpression' + && nodeType !== 'FunctionDeclaration' + && nodeType !== 'ArrowFunctionExpression' + && nodeType !== 'MethodDefinition' ) { - throw new TypeError('only function nodes are expected'); + return; } traverse(ASTNode.body, { diff --git a/lib/util/jsx.js b/lib/util/jsx.js index bd01c969be..56bdf2214e 100644 --- a/lib/util/jsx.js +++ b/lib/util/jsx.js @@ -88,13 +88,14 @@ function isWhiteSpaces(value) { * @param {Function} isCreateElement Function to determine if a CallExpresion is * a createElement one * @param {ASTNode} ASTnode The AST node being checked + * @param {Context} context The context of `ASTNode`. * @param {Boolean} [strict] If true, in a ternary condition the node must return JSX in both cases * @param {Boolean} [ignoreNull] If true, null return values will be ignored * @returns {Boolean} True if the node is returning JSX or null, false if not */ -function isReturningJSX(isCreateElement, ASTnode, strict, ignoreNull) { +function isReturningJSX(isCreateElement, ASTnode, context, strict, ignoreNull) { let found = false; - astUtil.traverseReturns(ASTnode, (node) => { + astUtil.traverseReturns(ASTnode, context, (node) => { // Traverse return statement astUtil.traverse(node, { enter(childNode) { diff --git a/tests/lib/rules/no-typos.js b/tests/lib/rules/no-typos.js index f95c576d5c..977b65ae0b 100644 --- a/tests/lib/rules/no-typos.js +++ b/tests/lib/rules/no-typos.js @@ -642,6 +642,20 @@ ruleTester.run('no-typos', rule, { } `, parserOptions + }, { + code: ` + const MyComponent = React.forwardRef((props, ref) =>
); + MyComponent.defaultProps = { value: "" }; + `, + parserOptions + }, { + code: ` + import styled from "styled-components"; + + const MyComponent = styled.div; + MyComponent.defaultProps = { value: "" }; + `, + parserOptions }), invalid: [].concat({ diff --git a/tests/util/ast.js b/tests/util/ast.js index ca110b5d13..7bd1106cc8 100644 --- a/tests/util/ast.js +++ b/tests/util/ast.js @@ -18,6 +18,8 @@ const parseCode = (code) => { return ASTnode.body[0]; }; +const mockContext = {}; + describe('ast', () => { describe('traverseReturnStatements', () => { it('Correctly traverses function declarations', () => { @@ -26,7 +28,7 @@ describe('ast', () => { function foo({prop}) { return; } - `), spy); + `), mockContext, spy); assert(spy.calledOnce); }); @@ -37,7 +39,7 @@ describe('ast', () => { const foo = function({prop}) { return; } - `).declarations[0].init, spy); + `).declarations[0].init, mockContext, spy); assert(spy.calledOnce); }); @@ -48,7 +50,7 @@ describe('ast', () => { ({prop}) => { return; } - `).expression, spy); + `).expression, mockContext, spy); assert(spy.calledOnce); @@ -56,7 +58,7 @@ describe('ast', () => { traverseReturns(parseCode(` ({prop}) => 'someething' - `).expression, spy); + `).expression, mockContext, spy); assert(spy.calledOnce); }); @@ -88,7 +90,7 @@ describe('ast', () => { const foo = () => 'not valid'; } - `), spy); + `), mockContext, spy); const enterCalls = spy.getCalls(); diff --git a/tests/util/jsx.js b/tests/util/jsx.js index 9f6df9703b..1062cebb1a 100644 --- a/tests/util/jsx.js +++ b/tests/util/jsx.js @@ -20,13 +20,15 @@ const parseCode = (code) => { return ASTnode.body[0]; }; +const mockContext = {}; + describe('jsxUtil', () => { describe('isReturningJSX', () => { const assertValid = (codeStr) => assert( - isReturningJSX(() => false, parseCode(codeStr)) + isReturningJSX(() => false, parseCode(codeStr), mockContext) ); const assertInValid = (codeStr) => assert( - !!isReturningJSX(() => false, parseCode(codeStr)) + !!isReturningJSX(() => false, parseCode(codeStr), mockContext) ); it('Works when returning JSX', () => { assertValid(`