From 85423f904777d4a54309e29568a61795629734b2 Mon Sep 17 00:00:00 2001 From: Jordan Harband Date: Thu, 19 Aug 2021 21:21:20 -0700 Subject: [PATCH 01/29] [Tests] upgrade babel-eslint to v10, test on v8 and v9 --- .github/workflows/node-4+.yml | 14 +++++++++++++- package.json | 2 +- tests/lib/rules/forbid-prop-types.js | 19 +++++++++++-------- tests/lib/rules/jsx-sort-default-props.js | 11 +++++++---- tests/lib/rules/no-unused-prop-types.js | 9 ++++++--- tests/lib/rules/prop-types.js | 7 +++++-- tests/lib/rules/sort-prop-types.js | 11 +++++++---- 7 files changed, 50 insertions(+), 23 deletions(-) diff --git a/.github/workflows/node-4+.yml b/.github/workflows/node-4+.yml index cbc44fe913..9deefc37e7 100644 --- a/.github/workflows/node-4+.yml +++ b/.github/workflows/node-4+.yml @@ -30,7 +30,19 @@ jobs: - 6 - 5 - 4 + babel-eslint: + - 10 + - 9 + - 8 exclude: + - node-version: 5 + babel-eslint: 10 + - node-version: 5 + babel-eslint: 9 + - node-version: 4 + babel-eslint: 10 + - node-version: 4 + babel-eslint: 9 - node-version: 9 eslint: 7 - node-version: 8 @@ -62,7 +74,7 @@ jobs: name: 'nvm install ${{ matrix.node-version }} && npm install' with: node-version: ${{ matrix.node-version }} - after_install: npm install --no-save "eslint@${{ matrix.eslint }}" "@typescript-eslint/parser@${{ matrix.node-version >= 10 && '3' || '2' }}" + after_install: npm install --no-save "eslint@${{ matrix.eslint }}" "@typescript-eslint/parser@${{ matrix.node-version >= 10 && '3' || '2' }}" "babel-eslint@${{ matrix.babel-eslint }}" skip-ls-check: true env: NPM_CONFIG_LEGACY_PEER_DEPS: true diff --git a/package.json b/package.json index bdfb9f7cd1..d4d1b5c25f 100644 --- a/package.json +++ b/package.json @@ -48,7 +48,7 @@ "@types/node": "^14.14.37", "@typescript-eslint/parser": "^2.34.0 || ^3.10.1", "aud": "^1.1.5", - "babel-eslint": "^8.2.6", + "babel-eslint": "^8 || ^9 || ^10.1.0", "eslint": "^3 || ^4 || ^5 || ^6 || ^7", "eslint-config-airbnb-base": "^14.2.1", "eslint-plugin-eslint-plugin": "^2.3.0 || ^3.3.1", diff --git a/tests/lib/rules/forbid-prop-types.js b/tests/lib/rules/forbid-prop-types.js index 910c7cddc4..dfd848bdde 100644 --- a/tests/lib/rules/forbid-prop-types.js +++ b/tests/lib/rules/forbid-prop-types.js @@ -8,7 +8,10 @@ // Requirements // ----------------------------------------------------------------------------- +const babelEslintVersion = require('babel-eslint/package.json').version; +const semver = require('semver'); const RuleTester = require('eslint').RuleTester; + const rule = require('../../../lib/rules/forbid-prop-types'); const parsers = require('../../helpers/parsers'); @@ -28,7 +31,7 @@ const parserOptions = { const ruleTester = new RuleTester({parserOptions}); ruleTester.run('forbid-prop-types', rule, { - valid: [{ + valid: [].concat({ code: [ 'var First = createReactClass({', ' render: function() {', @@ -137,7 +140,7 @@ ruleTester.run('forbid-prop-types', rule, { '};' ].join('\n'), parser: parsers.BABEL_ESLINT - }, { + }, semver.satisfies(babelEslintVersion, '< 9') ? { // Invalid code, should not be validated code: [ 'class Component extends React.Component {', @@ -152,7 +155,7 @@ ruleTester.run('forbid-prop-types', rule, { '}' ].join('\n'), parser: parsers.BABEL_ESLINT - }, { + } : [], { code: [ 'var Hello = createReactClass({', ' render: function() {', @@ -306,7 +309,7 @@ ruleTester.run('forbid-prop-types', rule, { ].join('\n'), parser: parsers.BABEL_ESLINT, options: [{checkContextTypes: true}] - }, { + }, semver.satisfies(babelEslintVersion, '< 9') ? { // Invalid code, should not be validated code: [ 'class Component extends React.Component {', @@ -322,7 +325,7 @@ ruleTester.run('forbid-prop-types', rule, { ].join('\n'), parser: parsers.BABEL_ESLINT, options: [{checkContextTypes: true}] - }, { + } : [], { code: [ 'var Hello = createReactClass({', ' render: function() {', @@ -480,7 +483,7 @@ ruleTester.run('forbid-prop-types', rule, { ].join('\n'), parser: parsers.BABEL_ESLINT, options: [{checkChildContextTypes: true}] - }, { + }, semver.satisfies(babelEslintVersion, '< 9') ? { // Invalid code, should not be validated code: [ 'class Component extends React.Component {', @@ -496,7 +499,7 @@ ruleTester.run('forbid-prop-types', rule, { ].join('\n'), parser: parsers.BABEL_ESLINT, options: [{checkChildContextTypes: true}] - }, { + } : [], { code: [ 'var Hello = createReactClass({', ' render: function() {', @@ -577,7 +580,7 @@ ruleTester.run('forbid-prop-types', rule, { ' bar: PropTypes.shape(Foo),', '};' ].join('\n') - }], + }), invalid: [{ code: [ diff --git a/tests/lib/rules/jsx-sort-default-props.js b/tests/lib/rules/jsx-sort-default-props.js index 96eaaf8d46..3b50078267 100644 --- a/tests/lib/rules/jsx-sort-default-props.js +++ b/tests/lib/rules/jsx-sort-default-props.js @@ -9,7 +9,10 @@ // Requirements // ----------------------------------------------------------------------------- +const babelEslintVersion = require('babel-eslint/package.json').version; +const semver = require('semver'); const RuleTester = require('eslint').RuleTester; + const rule = require('../../../lib/rules/jsx-sort-default-props'); const parsers = require('../../helpers/parsers'); @@ -28,7 +31,7 @@ const parserOptions = { const ruleTester = new RuleTester({parserOptions}); ruleTester.run('jsx-sort-default-props', rule, { - valid: [{ + valid: [].concat({ code: [ 'var First = createReactClass({', ' render: function() {', @@ -194,7 +197,7 @@ ruleTester.run('jsx-sort-default-props', rule, { options: [{ ignoreCase: true }] - }, { + }, semver.satisfies(babelEslintVersion, '< 9') ? { // Invalid code, should not be validated code: [ 'class Component extends React.Component {', @@ -214,7 +217,7 @@ ruleTester.run('jsx-sort-default-props', rule, { '}' ].join('\n'), parser: parsers.BABEL_ESLINT - }, { + } : [], { code: [ 'var Hello = createReactClass({', ' render: function() {', @@ -346,7 +349,7 @@ ruleTester.run('jsx-sort-default-props', rule, { 'First.propTypes = propTypes;', 'First.defaultProps = defaultProps;' ].join('\n') - }], + }), invalid: [{ code: [ diff --git a/tests/lib/rules/no-unused-prop-types.js b/tests/lib/rules/no-unused-prop-types.js index 5b41d0e777..fa5f732927 100644 --- a/tests/lib/rules/no-unused-prop-types.js +++ b/tests/lib/rules/no-unused-prop-types.js @@ -9,7 +9,10 @@ // Requirements // ------------------------------------------------------------------------------ +const babelEslintVersion = require('babel-eslint/package.json').version; +const semver = require('semver'); const RuleTester = require('eslint').RuleTester; + const rule = require('../../../lib/rules/no-unused-prop-types'); const parsers = require('../../helpers/parsers'); @@ -4452,7 +4455,7 @@ ruleTester.run('no-unused-prop-types', rule, { code: [ 'class Hello extends React.Component {', ' props: {', - ' unused: PropTypes.string', + ' unused: string', ' };', ' render () {', ' return
Hello {this.props.name}
;', @@ -4463,7 +4466,7 @@ ruleTester.run('no-unused-prop-types', rule, { errors: [ {message: '\'unused\' PropType is defined but prop is never used'} ] - }, { + }, semver.satisfies(babelEslintVersion, '< 9') ? [{ code: [ 'class Hello extends React.Component {', ' props: {', @@ -4478,7 +4481,7 @@ ruleTester.run('no-unused-prop-types', rule, { errors: [ {message: '\'unused\' PropType is defined but prop is never used'} ] - }, { + }] : [], { code: [ 'type Props = {unused: Object;};', 'class Hello extends React.Component {', diff --git a/tests/lib/rules/prop-types.js b/tests/lib/rules/prop-types.js index 2475d85257..689e8e14da 100644 --- a/tests/lib/rules/prop-types.js +++ b/tests/lib/rules/prop-types.js @@ -9,7 +9,10 @@ // Requirements // ------------------------------------------------------------------------------ +const babelEslintVersion = require('babel-eslint/package.json').version; +const semver = require('semver'); const RuleTester = require('eslint').RuleTester; + const rule = require('../../../lib/rules/prop-types'); const parsers = require('../../helpers/parsers'); @@ -3508,7 +3511,7 @@ ruleTester.run('prop-types', rule, { messageId: 'missingPropType', data: {name: 'lastname'} }] - }, { + }, semver.satisfies(babelEslintVersion, '< 9') ? { code: [ 'class Hello extends React.Component {', ' static propTypes: { ', @@ -3524,7 +3527,7 @@ ruleTester.run('prop-types', rule, { messageId: 'missingPropType', data: {name: 'firstname'} }] - }, { + } : [], { code: [ 'class Hello extends React.Component {', ' render() {', diff --git a/tests/lib/rules/sort-prop-types.js b/tests/lib/rules/sort-prop-types.js index 905857e6ea..1d7a184b54 100644 --- a/tests/lib/rules/sort-prop-types.js +++ b/tests/lib/rules/sort-prop-types.js @@ -8,7 +8,10 @@ // Requirements // ----------------------------------------------------------------------------- +const babelEslintVersion = require('babel-eslint/package.json').version; +const semver = require('semver'); const RuleTester = require('eslint').RuleTester; + const rule = require('../../../lib/rules/sort-prop-types'); const parsers = require('../../helpers/parsers'); @@ -28,7 +31,7 @@ const parserOptions = { const ruleTester = new RuleTester({parserOptions}); ruleTester.run('sort-prop-types', rule, { - valid: [{ + valid: [].concat({ code: [ 'var First = createReactClass({', ' render: function() {', @@ -155,7 +158,7 @@ ruleTester.run('sort-prop-types', rule, { options: [{ ignoreCase: true }] - }, { + }, semver.satisfies(babelEslintVersion, '< 9') ? { // Invalid code, should not be validated code: [ 'class Component extends React.Component {', @@ -170,7 +173,7 @@ ruleTester.run('sort-prop-types', rule, { '}' ].join('\n'), parser: parsers.BABEL_ESLINT - }, { + } : [], { code: [ 'var Hello = createReactClass({', ' render: function() {', @@ -463,7 +466,7 @@ ruleTester.run('sort-prop-types', rule, { options: [{ sortShapeProp: true }] - }], + }), invalid: [{ code: [ From c66c0736fcde23d3f3a64e080f6c82b06a298ff9 Mon Sep 17 00:00:00 2001 From: Jordan Harband Date: Thu, 19 Aug 2021 22:33:22 -0700 Subject: [PATCH 02/29] [Fix] `no-typos`: fix crash on private methods Fixes #3043. --- CHANGELOG.md | 2 ++ lib/rules/no-typos.js | 9 +++++---- tests/lib/rules/no-typos.js | 30 +++++++++++++++++++++++++++++- 3 files changed, 36 insertions(+), 5 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 91344f511c..ae56c3f841 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -19,6 +19,7 @@ This change log adheres to standards from [Keep a CHANGELOG](http://keepachangel * [`destructuring-assignment`]: get the contextName correctly ([#3025][] @ohhoney1) * [`no-typos`]: prevent crash on styled components and forwardRefs ([#3036][] @ljharb) * [`destructuring-assignment`], component detection: handle default exports edge case ([#3038][] @vedadeepta) +* [`no-typos`]: fix crash on private methods ([#3043][] @ljharb) ### Changed * [Docs] [`jsx-no-bind`]: updates discussion of refs ([#2998][] @dimitropoulos) @@ -27,6 +28,7 @@ This change log adheres to standards from [Keep a CHANGELOG](http://keepachangel * [Docs] [`require-default-props`]: fix small typo ([#2994][] @evsasse) * [Tests] add weekly scheduled smoke tests ([#2963][] @AriPerkkio) +[#3043]: https://github.com/yannickcr/eslint-plugin-react/issues/3043 [#3039]: https://github.com/yannickcr/eslint-plugin-react/pull/3039 [#3038]: https://github.com/yannickcr/eslint-plugin-react/pull/3038 [#3036]: https://github.com/yannickcr/eslint-plugin-react/issues/3036 diff --git a/lib/rules/no-typos.js b/lib/rules/no-typos.js index 052d63f48e..5e531fe355 100644 --- a/lib/rules/no-typos.js +++ b/lib/rules/no-typos.js @@ -157,11 +157,12 @@ module.exports = { } function reportErrorIfLifecycleMethodCasingTypo(node) { - let nodeKeyName = node.key.name; - if (node.key.type === 'Literal') { - nodeKeyName = node.key.value; + const key = node.key; + let nodeKeyName = key.name; + if (key.type === 'Literal') { + nodeKeyName = key.value; } - if (node.computed && typeof nodeKeyName !== 'string') { + if (key.type === 'PrivateName' || (node.computed && typeof nodeKeyName !== 'string')) { return; } diff --git a/tests/lib/rules/no-typos.js b/tests/lib/rules/no-typos.js index 977b65ae0b..bf67682454 100644 --- a/tests/lib/rules/no-typos.js +++ b/tests/lib/rules/no-typos.js @@ -8,7 +8,10 @@ // Requirements // ----------------------------------------------------------------------------- +const babelEslintVersion = require('babel-eslint/package.json').version; +const semver = require('semver'); const RuleTester = require('eslint').RuleTester; + const rule = require('../../../lib/rules/no-typos'); const parsers = require('../../helpers/parsers'); @@ -656,7 +659,32 @@ ruleTester.run('no-typos', rule, { MyComponent.defaultProps = { value: "" }; `, parserOptions - }), + }, semver.satisfies(babelEslintVersion, '>= 9') ? { + code: ` + class Editor extends React.Component { + #somethingPrivate() { + // ... + } + + render() { + const { value = '' } = this.props; + + return ( + + ); + } + } + `, + parser: require.resolve('babel-eslint'), + parserOptions: { + babelOptions: { + classPrivateMethods: true + }, + shippedProposals: true + } + } : []), invalid: [].concat({ code: ` From 49b7182e9f54bd507c238287c99f8ce163b188cf Mon Sep 17 00:00:00 2001 From: Patrick Gingras <775.pg.12@gmail.com> Date: Tue, 24 Aug 2021 09:09:39 -0400 Subject: [PATCH 03/29] [Fix] `jsx-no-bind`: handle local function declarations Fixes #3047 --- CHANGELOG.md | 2 ++ docs/rules/jsx-no-bind.md | 9 +++++++++ lib/rules/jsx-no-bind.js | 14 +++++++++++++- tests/lib/rules/jsx-no-bind.js | 28 ++++++++++++++++++++++++++++ 4 files changed, 52 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index ae56c3f841..2c5b3498be 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -20,6 +20,7 @@ This change log adheres to standards from [Keep a CHANGELOG](http://keepachangel * [`no-typos`]: prevent crash on styled components and forwardRefs ([#3036][] @ljharb) * [`destructuring-assignment`], component detection: handle default exports edge case ([#3038][] @vedadeepta) * [`no-typos`]: fix crash on private methods ([#3043][] @ljharb) +* [`jsx-no-bind`]: handle local function declarations ([#3048][] @p7g) ### Changed * [Docs] [`jsx-no-bind`]: updates discussion of refs ([#2998][] @dimitropoulos) @@ -28,6 +29,7 @@ This change log adheres to standards from [Keep a CHANGELOG](http://keepachangel * [Docs] [`require-default-props`]: fix small typo ([#2994][] @evsasse) * [Tests] add weekly scheduled smoke tests ([#2963][] @AriPerkkio) +[#3048]: https://github.com/yannickcr/eslint-plugin-react/pull/3048 [#3043]: https://github.com/yannickcr/eslint-plugin-react/issues/3043 [#3039]: https://github.com/yannickcr/eslint-plugin-react/pull/3039 [#3038]: https://github.com/yannickcr/eslint-plugin-react/pull/3038 diff --git a/docs/rules/jsx-no-bind.md b/docs/rules/jsx-no-bind.md index fe7833a3c7..b07c5a9368 100644 --- a/docs/rules/jsx-no-bind.md +++ b/docs/rules/jsx-no-bind.md @@ -14,6 +14,10 @@ Examples of **incorrect** code for this rule: ```jsx console.log('Hello!')}> ``` +```jsx +function onClick() { console.log('Hello!'); } + +``` Examples of **correct** code for this rule: ```jsx @@ -76,6 +80,11 @@ Examples of **correct** code for this rule, when `allowFunctions` is `true`: ``` +```jsx +function onClick() { alert("1337"); } + +``` + ### `allowBind` Examples of **correct** code for this rule, when `allowBind` is `true`: diff --git a/lib/rules/jsx-no-bind.js b/lib/rules/jsx-no-bind.js index 1b14050326..cdc499bdb0 100644 --- a/lib/rules/jsx-no-bind.js +++ b/lib/rules/jsx-no-bind.js @@ -96,7 +96,10 @@ module.exports = { if (!configuration.allowArrowFunctions && nodeType === 'ArrowFunctionExpression') { return 'arrowFunc'; } - if (!configuration.allowFunctions && nodeType === 'FunctionExpression') { + if ( + !configuration.allowFunctions + && (nodeType === 'FunctionExpression' || nodeType === 'FunctionDeclaration') + ) { return 'func'; } if (!configuration.allowBind && nodeType === 'BindExpression') { @@ -144,6 +147,15 @@ module.exports = { setBlockVariableNameSet(node.range[0]); }, + FunctionDeclaration(node) { + const blockAncestors = getBlockStatementAncestors(node); + const variableViolationType = getNodeViolationType(node); + + if (blockAncestors.length > 0 && variableViolationType) { + addVariableNameToSet(variableViolationType, node.id.name, blockAncestors[0].range[0]); + } + }, + VariableDeclarator(node) { if (!node.init) { return; diff --git a/tests/lib/rules/jsx-no-bind.js b/tests/lib/rules/jsx-no-bind.js index 7e045851c4..4c663b43f6 100644 --- a/tests/lib/rules/jsx-no-bind.js +++ b/tests/lib/rules/jsx-no-bind.js @@ -301,6 +301,19 @@ ruleTester.run('jsx-no-bind', rule, { code: '
', options: [{ignoreDOMComponents: true}], parser: parsers.BABEL_ESLINT + }, + + // Local function declaration + { + code: [ + 'function click() { return true; }', + 'class Hello23 extends React.Component {', + ' renderDiv() {', + ' return
Hello
;', + ' }', + '};' + ].join('\n'), + errors: [] } ], @@ -806,6 +819,21 @@ ruleTester.run('jsx-no-bind', rule, { parser: parsers.BABEL_ESLINT }, + // Local function declaration + { + code: [ + 'class Hello23 extends React.Component {', + ' renderDiv() {', + ' function click() { return true; }', + ' return
Hello
;', + ' }', + '};' + ].join('\n'), + errors: [ + {messageId: 'func'} + ] + }, + // ignore DOM components { code: '', From 66b88a8ce6cc52e5073e3e95a3e9197a4bcd703e Mon Sep 17 00:00:00 2001 From: Jordan Harband Date: Tue, 24 Aug 2021 11:19:02 -0700 Subject: [PATCH 04/29] [Dev Deps] update `eslint-plugin-import`, `markdown-magic` --- package.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/package.json b/package.json index d4d1b5c25f..777ab3bce6 100644 --- a/package.json +++ b/package.json @@ -52,12 +52,12 @@ "eslint": "^3 || ^4 || ^5 || ^6 || ^7", "eslint-config-airbnb-base": "^14.2.1", "eslint-plugin-eslint-plugin": "^2.3.0 || ^3.3.1", - "eslint-plugin-import": "^2.23.4", + "eslint-plugin-import": "^2.24.1", "eslint-remote-tester": "^1.3.0", "eslint-remote-tester-repositories": "^0.0.2", "espree": "^3.5.4", "istanbul": "^0.4.5", - "markdown-magic": "^2.0.0", + "markdown-magic": "^2.5.2", "mocha": "^5.2.0", "semver": "^6.3.0", "sinon": "^7.5.0", From 18612558bb09470d7e42c70d35b3afca9b58e35e Mon Sep 17 00:00:00 2001 From: Jordan Harband Date: Tue, 24 Aug 2021 12:53:16 -0700 Subject: [PATCH 05/29] [Tests] test on v4 of the TS parser Pinned to v4.0 pending https://github.com/typescript-eslint/typescript-eslint/issues/3788 --- .github/workflows/node-4+.yml | 2 +- package.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/node-4+.yml b/.github/workflows/node-4+.yml index 9deefc37e7..732f2273c6 100644 --- a/.github/workflows/node-4+.yml +++ b/.github/workflows/node-4+.yml @@ -74,7 +74,7 @@ jobs: name: 'nvm install ${{ matrix.node-version }} && npm install' with: node-version: ${{ matrix.node-version }} - after_install: npm install --no-save "eslint@${{ matrix.eslint }}" "@typescript-eslint/parser@${{ matrix.node-version >= 10 && '3' || '2' }}" "babel-eslint@${{ matrix.babel-eslint }}" + after_install: npm install --no-save "eslint@${{ matrix.eslint }}" "@typescript-eslint/parser@${{ matrix.node-version >= 10 && '4.0' || (matrix.node-version >= 8 && '3' || '2') }}" "babel-eslint@${{ matrix.babel-eslint }}" skip-ls-check: true env: NPM_CONFIG_LEGACY_PEER_DEPS: true diff --git a/package.json b/package.json index 777ab3bce6..fb6cd69467 100644 --- a/package.json +++ b/package.json @@ -46,7 +46,7 @@ "@types/eslint": "=7.2.10", "@types/estree": "^0.0.47", "@types/node": "^14.14.37", - "@typescript-eslint/parser": "^2.34.0 || ^3.10.1", + "@typescript-eslint/parser": "^2.34.0 || ^3.10.1 || ~4.0", "aud": "^1.1.5", "babel-eslint": "^8 || ^9 || ^10.1.0", "eslint": "^3 || ^4 || ^5 || ^6 || ^7", From 3d23496e1427afa5a5b7c63675d46178f4524ce7 Mon Sep 17 00:00:00 2001 From: Jordan Harband Date: Tue, 24 Aug 2021 13:25:40 -0700 Subject: [PATCH 06/29] [Tests] `destructuring-assignment`: add passing test --- tests/lib/rules/destructuring-assignment.js | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/tests/lib/rules/destructuring-assignment.js b/tests/lib/rules/destructuring-assignment.js index aaa00f5659..f5cc13b410 100644 --- a/tests/lib/rules/destructuring-assignment.js +++ b/tests/lib/rules/destructuring-assignment.js @@ -243,7 +243,24 @@ ruleTester.run('destructuring-assignment', rule, { return null; }; ` + }, { + code: ` + class C extends React.Component { + componentDidMount() { + const { forwardRef } = this.props; + this.ref.current.focus(); + + if (typeof forwardRef === 'function') { + forwardRef(this.ref); + } + } + render() { + return
; + } + } + `, + parser: parsers.BABEL_ESLINT }], invalid: [{ From d74a7d82d92ba984f7bcbf62d7ebe9272b3d5154 Mon Sep 17 00:00:00 2001 From: vedadeepta Date: Wed, 25 Aug 2021 17:45:35 +0530 Subject: [PATCH 07/29] [Fix] `prop-types`, `propTypes`: handle React.* TypeScript types --- CHANGELOG.md | 2 + lib/util/propTypes.js | 61 +++++++++++++++++++------ tests/lib/rules/prop-types.js | 85 +++++++++++++++++++++++++++++++++++ 3 files changed, 135 insertions(+), 13 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 2c5b3498be..fccd3b7465 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -21,6 +21,7 @@ This change log adheres to standards from [Keep a CHANGELOG](http://keepachangel * [`destructuring-assignment`], component detection: handle default exports edge case ([#3038][] @vedadeepta) * [`no-typos`]: fix crash on private methods ([#3043][] @ljharb) * [`jsx-no-bind`]: handle local function declarations ([#3048][] @p7g) +* [`prop-types`], `propTypes`: handle React.* TypeScript types ([#3049][] @vedadeepta) ### Changed * [Docs] [`jsx-no-bind`]: updates discussion of refs ([#2998][] @dimitropoulos) @@ -29,6 +30,7 @@ This change log adheres to standards from [Keep a CHANGELOG](http://keepachangel * [Docs] [`require-default-props`]: fix small typo ([#2994][] @evsasse) * [Tests] add weekly scheduled smoke tests ([#2963][] @AriPerkkio) +[#3049]: https://github.com/yannickcr/eslint-plugin-react/pull/3049 [#3048]: https://github.com/yannickcr/eslint-plugin-react/pull/3048 [#3043]: https://github.com/yannickcr/eslint-plugin-react/issues/3043 [#3039]: https://github.com/yannickcr/eslint-plugin-react/pull/3039 diff --git a/lib/util/propTypes.js b/lib/util/propTypes.js index 95671b9a2f..27ed04b7be 100644 --- a/lib/util/propTypes.js +++ b/lib/util/propTypes.js @@ -23,8 +23,8 @@ function isFunctionType(node) { if (!node) return false; const nodeType = node.type; return nodeType === 'FunctionDeclaration' - || nodeType === 'FunctionExpression' - || nodeType === 'ArrowFunctionExpression'; + || nodeType === 'FunctionExpression' + || nodeType === 'ArrowFunctionExpression'; } /** @@ -100,6 +100,7 @@ module.exports = function propTypesInstructions(context, components, utils) { const defaults = {customValidators: []}; const configuration = Object.assign({}, defaults, context.options[0] || {}); const customValidators = configuration.customValidators; + const allowedGenericTypes = ['React.SFC', 'React.StatelessComponent', 'React.FunctionComponent', 'React.FC']; /** * Returns the full scope. @@ -547,6 +548,11 @@ module.exports = function propTypesInstructions(context, components, utils) { let typeName; if (astUtil.isTSTypeReference(node)) { typeName = node.typeName.name; + if (!typeName && node.typeParameters && node.typeParameters.length !== 0) { + const nextNode = node.typeParameters.params[0]; + this.visitTSNode(nextNode); + return; + } } else if (astUtil.isTSInterfaceHeritage(node)) { if (!node.expression && node.id) { typeName = node.id.name; @@ -887,7 +893,15 @@ module.exports = function propTypesInstructions(context, components, utils) { * FunctionDeclaration, or FunctionExpression */ function markAnnotatedFunctionArgumentsAsDeclared(node) { - if (!node.params || !node.params.length || !annotations.isAnnotatedFunctionPropsDeclaration(node, context)) { + if (!node.params || !node.params.length) { + return; + } + + const siblingIdentifier = node.parent && node.parent.id; + const siblingHasTypeAnnotation = siblingIdentifier && siblingIdentifier.typeAnnotation; + const isNodeAnnotated = annotations.isAnnotatedFunctionPropsDeclaration(node, context); + + if (!isNodeAnnotated && !siblingHasTypeAnnotation) { return; } @@ -901,17 +915,38 @@ module.exports = function propTypesInstructions(context, components, utils) { return; } - const param = node.params[0]; - if (param.typeAnnotation && param.typeAnnotation.typeAnnotation && param.typeAnnotation.typeAnnotation.type === 'UnionTypeAnnotation') { - param.typeAnnotation.typeAnnotation.types.forEach((annotation) => { - if (annotation.type === 'GenericTypeAnnotation') { - markPropTypesAsDeclared(node, resolveTypeAnnotation(annotation)); - } else { - markPropTypesAsDeclared(node, annotation); - } - }); + if (isNodeAnnotated) { + const param = node.params[0]; + if (param.typeAnnotation && param.typeAnnotation.typeAnnotation && param.typeAnnotation.typeAnnotation.type === 'UnionTypeAnnotation') { + param.typeAnnotation.typeAnnotation.types.forEach((annotation) => { + if (annotation.type === 'GenericTypeAnnotation') { + markPropTypesAsDeclared(node, resolveTypeAnnotation(annotation)); + } else { + markPropTypesAsDeclared(node, annotation); + } + }); + } else { + markPropTypesAsDeclared(node, resolveTypeAnnotation(param)); + } } else { - markPropTypesAsDeclared(node, resolveTypeAnnotation(param)); + // implements what's discussed here: https://github.com/yannickcr/eslint-plugin-react/issues/2777#issuecomment-683944481 + const annotation = siblingIdentifier.typeAnnotation.typeAnnotation; + + if ( + annotation + && annotation.type !== 'TSTypeReference' + && annotation.typeParameters == null + ) { + return; + } + + const typeName = context.getSourceCode().getText(annotation.typeName).replace(/ /g, ''); + + if (allowedGenericTypes.indexOf(typeName) === -1) { + return; + } + + markPropTypesAsDeclared(node, resolveTypeAnnotation(siblingIdentifier)); } } diff --git a/tests/lib/rules/prop-types.js b/tests/lib/rules/prop-types.js index 689e8e14da..27dff143d2 100644 --- a/tests/lib/rules/prop-types.js +++ b/tests/lib/rules/prop-types.js @@ -3147,6 +3147,55 @@ ruleTester.run('prop-types', rule, { } `, parser: parsers['@TYPESCRIPT_ESLINT'] + }, + { + code: ` + interface PersonProps { + username: string; + } + const Person: React.FunctionComponent = (props): React.ReactElement => ( +
{props.username}
+ ); + `, + parser: parsers['@TYPESCRIPT_ESLINT'] + }, + { + code: ` + const Person: React.FunctionComponent = (props): React.ReactElement => ( +
{props.username}
+ ); + `, + parser: parsers['@TYPESCRIPT_ESLINT'] + }, + { + code: ` + interface PersonProps { + username: string; + } + const Person: React.FC = (props): React.ReactElement => ( +
{props.username}
+ ); + `, + parser: parsers['@TYPESCRIPT_ESLINT'] + }, + { + code: ` + const Person: React.FunctionComponent<{ username: string }> = (props): React.ReactElement => ( +
{props.username}
+ ); + `, + parser: parsers['@TYPESCRIPT_ESLINT'] + }, + { + code: ` + type PersonProps = { + username: string; + } + const Person: React.FunctionComponent = (props): React.ReactElement => ( +
{props.username}
+ ); + `, + parser: parsers['@TYPESCRIPT_ESLINT'] } ]), { @@ -6621,6 +6670,42 @@ ruleTester.run('prop-types', rule, { } ], parser: parsers['@TYPESCRIPT_ESLINT'] + }, + { + code: ` + import React from 'react'; + interface PersonProps { + test: string; + } + const Person: React.FunctionComponent = (props): React.ReactElement => ( +
{props.username}
+ ); + `, + errors: [ + { + messageId: 'missingPropType', + data: {name: 'username'} + } + ], + parser: parsers['@TYPESCRIPT_ESLINT'] + }, + { + code: ` + import React from 'react'; + interface PersonProps { + username: string; + } + const Person: React.FunctionComponent = (props): React.ReactElement => ( +
{props.test}
+ ); + `, + errors: [ + { + messageId: 'missingPropType', + data: {name: 'test'} + } + ], + parser: parsers['@TYPESCRIPT_ESLINT'] } ]) ) From 77813f04e447f48239c9689de1e7043b118e3d43 Mon Sep 17 00:00:00 2001 From: Jordan Harband Date: Thu, 26 Aug 2021 16:46:34 -0700 Subject: [PATCH 08/29] [Docs] improve instructions for `jsx-runtime` config Closes #3052 --- CHANGELOG.md | 2 ++ README.md | 2 +- docs/rules/jsx-uses-react.md | 2 +- docs/rules/react-in-jsx-scope.md | 2 +- 4 files changed, 5 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index fccd3b7465..67199f5526 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -29,7 +29,9 @@ 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) * [Tests] add weekly scheduled smoke tests ([#2963][] @AriPerkkio) +* [Docs] improve instructions for `jsx-runtime` config ([#3052][] @ljharb) +[#3052]: https://github.com/yannickcr/eslint-plugin-react/issues/3052 [#3049]: https://github.com/yannickcr/eslint-plugin-react/pull/3049 [#3048]: https://github.com/yannickcr/eslint-plugin-react/pull/3048 [#3043]: https://github.com/yannickcr/eslint-plugin-react/issues/3043 diff --git a/README.md b/README.md index ad95c7e922..baee650534 100644 --- a/README.md +++ b/README.md @@ -31,7 +31,7 @@ Use [our preset](#recommended) to get reasonable defaults: ] ``` -If you are using the [new JSX transform from React 17](https://reactjs.org/blog/2020/09/22/introducing-the-new-jsx-transform.html#removing-unused-react-imports), extend [`react/jsx-runtime`](https://github.com/yannickcr/eslint-plugin-react/blob/HEAD/index.js#L163-L176) in your eslint config to disable the relevant rules. +If you are using the [new JSX transform from React 17](https://reactjs.org/blog/2020/09/22/introducing-the-new-jsx-transform.html#removing-unused-react-imports), extend [`react/jsx-runtime`](https://github.com/yannickcr/eslint-plugin-react/blob/HEAD/index.js#L163-L176) in your eslint config (add `"plugin:react/jsx-runtime"` to `"extends"`) to disable the relevant rules. You should also specify settings that will be shared across all the plugin rules. ([More about eslint shared settings](https://eslint.org/docs/user-guide/configuring/configuration-files#adding-shared-settings)) diff --git a/docs/rules/jsx-uses-react.md b/docs/rules/jsx-uses-react.md index 049cae4245..a79d6a844f 100644 --- a/docs/rules/jsx-uses-react.md +++ b/docs/rules/jsx-uses-react.md @@ -45,4 +45,4 @@ var Hello =
Hello {this.props.name}
; If you are not using JSX, if React is declared as global variable, or if you do not use the `no-unused-vars` rule. -If you are using the [new JSX transform from React 17](https://reactjs.org/blog/2020/09/22/introducing-the-new-jsx-transform.html#removing-unused-react-imports), you should disable this rule by extending [`react/jsx-runtime`](https://github.com/yannickcr/eslint-plugin-react/blob/HEAD/index.js#L163-L176) in your eslint config. \ No newline at end of file +If you are using the [new JSX transform from React 17](https://reactjs.org/blog/2020/09/22/introducing-the-new-jsx-transform.html#removing-unused-react-imports), you should disable this rule by extending [`react/jsx-runtime`](https://github.com/yannickcr/eslint-plugin-react/blob/HEAD/index.js#L163-L176) in your eslint config (add `"plugin:react/jsx-runtime"` to `"extends"`). \ No newline at end of file diff --git a/docs/rules/react-in-jsx-scope.md b/docs/rules/react-in-jsx-scope.md index 7cb286702b..b3357dc671 100644 --- a/docs/rules/react-in-jsx-scope.md +++ b/docs/rules/react-in-jsx-scope.md @@ -44,4 +44,4 @@ var Hello =
Hello {this.props.name}
; If you are not using JSX, or if you are setting `React` as a global variable. -If you are using the [new JSX transform from React 17](https://reactjs.org/blog/2020/09/22/introducing-the-new-jsx-transform.html#removing-unused-react-imports), you should disable this rule by extending [`react/jsx-runtime`](https://github.com/yannickcr/eslint-plugin-react/blob/HEAD/index.js#L163-L176) in your eslint config. \ No newline at end of file +If you are using the [new JSX transform from React 17](https://reactjs.org/blog/2020/09/22/introducing-the-new-jsx-transform.html#removing-unused-react-imports), you should disable this rule by extending [`react/jsx-runtime`](https://github.com/yannickcr/eslint-plugin-react/blob/HEAD/index.js#L163-L176) in your eslint config (add `"plugin:react/jsx-runtime"` to `"extends"`). \ No newline at end of file From 130afb11335615aec623577b7e3ad33de2b9abec Mon Sep 17 00:00:00 2001 From: vedadeepta Date: Thu, 26 Aug 2021 17:32:55 +0530 Subject: [PATCH 09/29] [Fix] `prop-types`, `propTypes`: add handling for `FC`, improve tests --- CHANGELOG.md | 2 + lib/util/propTypes.js | 42 +++++++++-- tests/lib/rules/prop-types.js | 136 ++++++++++++++++++++++++++++++++++ 3 files changed, 174 insertions(+), 6 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 67199f5526..a4487cb307 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -22,6 +22,7 @@ This change log adheres to standards from [Keep a CHANGELOG](http://keepachangel * [`no-typos`]: fix crash on private methods ([#3043][] @ljharb) * [`jsx-no-bind`]: handle local function declarations ([#3048][] @p7g) * [`prop-types`], `propTypes`: handle React.* TypeScript types ([#3049][] @vedadeepta) +* [`prop-types`], `propTypes`: add handling for `FC`, improve tests ([#3051][] @vedadeepta) ### Changed * [Docs] [`jsx-no-bind`]: updates discussion of refs ([#2998][] @dimitropoulos) @@ -32,6 +33,7 @@ This change log adheres to standards from [Keep a CHANGELOG](http://keepachangel * [Docs] improve instructions for `jsx-runtime` config ([#3052][] @ljharb) [#3052]: https://github.com/yannickcr/eslint-plugin-react/issues/3052 +[#3051]: https://github.com/yannickcr/eslint-plugin-react/pull/3051 [#3049]: https://github.com/yannickcr/eslint-plugin-react/pull/3049 [#3048]: https://github.com/yannickcr/eslint-plugin-react/pull/3048 [#3043]: https://github.com/yannickcr/eslint-plugin-react/issues/3043 diff --git a/lib/util/propTypes.js b/lib/util/propTypes.js index 27ed04b7be..df24de496e 100644 --- a/lib/util/propTypes.js +++ b/lib/util/propTypes.js @@ -100,7 +100,8 @@ module.exports = function propTypesInstructions(context, components, utils) { const defaults = {customValidators: []}; const configuration = Object.assign({}, defaults, context.options[0] || {}); const customValidators = configuration.customValidators; - const allowedGenericTypes = ['React.SFC', 'React.StatelessComponent', 'React.FunctionComponent', 'React.FC']; + const allowedGenericTypes = new Set(['SFC', 'StatelessComponent', 'FunctionComponent', 'FC']); + const genericReactTypesImport = new Set(); /** * Returns the full scope. @@ -548,7 +549,8 @@ module.exports = function propTypesInstructions(context, components, utils) { let typeName; if (astUtil.isTSTypeReference(node)) { typeName = node.typeName.name; - if (!typeName && node.typeParameters && node.typeParameters.length !== 0) { + const shouldTraverseTypeParams = !typeName || genericReactTypesImport.has(typeName); + if (shouldTraverseTypeParams && node.typeParameters && node.typeParameters.length !== 0) { const nextNode = node.typeParameters.params[0]; this.visitTSNode(nextNode); return; @@ -940,12 +942,19 @@ module.exports = function propTypesInstructions(context, components, utils) { return; } - const typeName = context.getSourceCode().getText(annotation.typeName).replace(/ /g, ''); + if (annotation.typeName.name) { // if FC + const typeName = annotation.typeName.name; + if (!genericReactTypesImport.has(typeName)) { + return; + } + } else if (annotation.typeName.right.name) { // if React.FC + const right = annotation.typeName.right.name; + const left = annotation.typeName.left.name; - if (allowedGenericTypes.indexOf(typeName) === -1) { - return; + if (!genericReactTypesImport.has(left) || !allowedGenericTypes.has(right)) { + return; + } } - markPropTypesAsDeclared(node, resolveTypeAnnotation(siblingIdentifier)); } } @@ -1036,6 +1045,27 @@ module.exports = function propTypesInstructions(context, components, utils) { } }, + ImportDeclaration(node) { + // parse `import ... from 'react` + if (node.source.value === 'react') { + node.specifiers.forEach((specifier) => { + if ( + // handles import * as X from 'react' + specifier.type === 'ImportNamespaceSpecifier' + // handles import React from 'react' + || specifier.type === 'ImportDefaultSpecifier' + ) { + genericReactTypesImport.add(specifier.local.name); + } + + // handles import { FC } from 'react' or import { FC as X } from 'react' + if (specifier.type === 'ImportSpecifier' && allowedGenericTypes.has(specifier.imported.name)) { + genericReactTypesImport.add(specifier.local.name); + } + }); + } + }, + FunctionDeclaration: markAnnotatedFunctionArgumentsAsDeclared, ArrowFunctionExpression: markAnnotatedFunctionArgumentsAsDeclared, diff --git a/tests/lib/rules/prop-types.js b/tests/lib/rules/prop-types.js index 27dff143d2..3eaa9277d2 100644 --- a/tests/lib/rules/prop-types.js +++ b/tests/lib/rules/prop-types.js @@ -3150,6 +3150,8 @@ ruleTester.run('prop-types', rule, { }, { code: ` + import React from 'react'; + interface PersonProps { username: string; } @@ -3161,6 +3163,8 @@ ruleTester.run('prop-types', rule, { }, { code: ` + import React from 'react'; + const Person: React.FunctionComponent = (props): React.ReactElement => (
{props.username}
); @@ -3169,6 +3173,8 @@ ruleTester.run('prop-types', rule, { }, { code: ` + import React from 'react'; + interface PersonProps { username: string; } @@ -3180,6 +3186,7 @@ ruleTester.run('prop-types', rule, { }, { code: ` + import React from 'react'; const Person: React.FunctionComponent<{ username: string }> = (props): React.ReactElement => (
{props.username}
); @@ -3188,6 +3195,7 @@ ruleTester.run('prop-types', rule, { }, { code: ` + import React from 'react'; type PersonProps = { username: string; } @@ -3196,6 +3204,82 @@ ruleTester.run('prop-types', rule, { ); `, parser: parsers['@TYPESCRIPT_ESLINT'] + }, + { + code: ` + import { FunctionComponent } from 'react'; + + type PersonProps = { + username: string; + } + const Person: FunctionComponent = (props): React.ReactElement => ( +
{props.username}
+ ); + `, + parser: parsers['@TYPESCRIPT_ESLINT'] + }, + { + code: ` + import { FC } from 'react'; + type PersonProps = { + username: string; + } + const Person: FC = (props): React.ReactElement => ( +
{props.username}
+ ); + `, + parser: parsers['@TYPESCRIPT_ESLINT'] + }, + { + code: ` + import type { FC } from 'react'; + type PersonProps = { + username: string; + } + const Person: FC = (props): React.ReactElement => ( +
{props.username}
+ ); + `, + parser: parsers['@TYPESCRIPT_ESLINT'] + }, + { + code: ` + import { FC as X } from 'react'; + interface PersonProps { + username: string; + } + const Person: X = (props): React.ReactElement => ( +
{props.username}
+ ); + `, + parser: parsers['@TYPESCRIPT_ESLINT'] + }, + { + code: ` + import * as X from 'react'; + interface PersonProps { + username: string; + } + const Person: X.FC = (props): React.ReactElement => ( +
{props.username}
+ ); + `, + parser: parsers['@TYPESCRIPT_ESLINT'] + }, + { + // issue: https://github.com/yannickcr/eslint-plugin-react/issues/2786 + code: ` + import React from 'react'; + + interface Props { + item: any; + } + + const SomeComponent: React.FC = ({ item }: Props) => { + return item ? <> : <>; + }; + `, + parser: parsers['@TYPESCRIPT_ESLINT'] } ]), { @@ -6706,6 +6790,58 @@ ruleTester.run('prop-types', rule, { } ], parser: parsers['@TYPESCRIPT_ESLINT'] + }, + { + code: ` + import React from 'react'; + interface PersonProps { + username: string; + } + const Person: FunctionComponent = (props): React.ReactElement => ( +
{props.test}
+ ); + `, + errors: [ + { + messageId: 'missingPropType', + data: {name: 'test'} + } + ], + parser: parsers['@TYPESCRIPT_ESLINT'] + }, + { + code: ` + interface PersonProps { + username: string; + } + const Person: X.FC = (props): React.ReactElement => ( +
{props.username}
+ ); + `, + errors: [ + { + messageId: 'missingPropType', + data: {name: 'username'} + } + ], + parser: parsers['@TYPESCRIPT_ESLINT'] + }, + { + code: ` + interface PersonProps { + username: string; + } + const Person: FC = (props): React.ReactElement => ( +
{props.username}
+ ); + `, + errors: [ + { + messageId: 'missingPropType', + data: {name: 'username'} + } + ], + parser: parsers['@TYPESCRIPT_ESLINT'] } ]) ) From bb64df6505b3e9a01da5b61626ab9f544caea438 Mon Sep 17 00:00:00 2001 From: Jordan Harband Date: Tue, 24 Aug 2021 22:17:06 -0700 Subject: [PATCH 10/29] [New] `jsx-runtime`: set `parserOptions.jsxPragma` for `@typescript-eslint/parser` See https://github.com/typescript-eslint/typescript-eslint/issues/3788#issuecomment-905094436 --- CHANGELOG.md | 2 ++ index.js | 3 ++- 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index a4487cb307..37b4f0ed41 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,6 +11,7 @@ This change log adheres to standards from [Keep a CHANGELOG](http://keepachangel * [`jsx-no-target-blank`]: add `forms` option ([#1617][] @jaaberg) * [`jsx-pascal-case`]: add `allowLeadingUnderscore` option ([#3039][] @pangaeatech) * [`no-children-prop`]: Add `allowFunctions` option ([#1903][] @alexzherdev) +* [`jsx-runtime`]: set `parserOptions.jsxPragma` for `@typescript-eslint/parser` ([e382d3d3][] @ljharb) ### Fixed * component detection: use `estraverse` to improve component detection ([#2992][] @Wesitos) @@ -32,6 +33,7 @@ This change log adheres to standards from [Keep a CHANGELOG](http://keepachangel * [Tests] add weekly scheduled smoke tests ([#2963][] @AriPerkkio) * [Docs] improve instructions for `jsx-runtime` config ([#3052][] @ljharb) +[e382d3d3]: https://github.com/yannickcr/eslint-plugin-react/commit/e382d3d3283fcdaf0ae34662554bfee157a2fc3b [#3052]: https://github.com/yannickcr/eslint-plugin-react/issues/3052 [#3051]: https://github.com/yannickcr/eslint-plugin-react/pull/3051 [#3049]: https://github.com/yannickcr/eslint-plugin-react/pull/3049 diff --git a/index.js b/index.js index e89b0c8ea4..15fa8f0465 100644 --- a/index.js +++ b/index.js @@ -168,7 +168,8 @@ module.exports = { parserOptions: { ecmaFeatures: { jsx: true - } + }, + jsxPragma: null // for @typescript/eslint-parser }, rules: { 'react/react-in-jsx-scope': 0, From eb5f0d572bf5509b99dbb889202078a3785cfd02 Mon Sep 17 00:00:00 2001 From: Jordan Harband Date: Fri, 27 Aug 2021 16:36:59 -0700 Subject: [PATCH 11/29] [meta] update sha --- CHANGELOG.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 37b4f0ed41..fae1848cc7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,7 +11,7 @@ This change log adheres to standards from [Keep a CHANGELOG](http://keepachangel * [`jsx-no-target-blank`]: add `forms` option ([#1617][] @jaaberg) * [`jsx-pascal-case`]: add `allowLeadingUnderscore` option ([#3039][] @pangaeatech) * [`no-children-prop`]: Add `allowFunctions` option ([#1903][] @alexzherdev) -* [`jsx-runtime`]: set `parserOptions.jsxPragma` for `@typescript-eslint/parser` ([e382d3d3][] @ljharb) +* [`jsx-runtime`]: set `parserOptions.jsxPragma` for `@typescript-eslint/parser` ([bb64df65][] @ljharb) ### Fixed * component detection: use `estraverse` to improve component detection ([#2992][] @Wesitos) @@ -33,7 +33,7 @@ This change log adheres to standards from [Keep a CHANGELOG](http://keepachangel * [Tests] add weekly scheduled smoke tests ([#2963][] @AriPerkkio) * [Docs] improve instructions for `jsx-runtime` config ([#3052][] @ljharb) -[e382d3d3]: https://github.com/yannickcr/eslint-plugin-react/commit/e382d3d3283fcdaf0ae34662554bfee157a2fc3b +[bb64df65]: https://github.com/yannickcr/eslint-plugin-react/commit/bb64df6505b3e9a01da5b61626ab9f544caea438 [#3052]: https://github.com/yannickcr/eslint-plugin-react/issues/3052 [#3051]: https://github.com/yannickcr/eslint-plugin-react/pull/3051 [#3049]: https://github.com/yannickcr/eslint-plugin-react/pull/3049 From 6f78b757bfd3bbf1d57a5a7e9d2218141040fb61 Mon Sep 17 00:00:00 2001 From: Jordan Harband Date: Fri, 27 Aug 2021 16:27:03 -0700 Subject: [PATCH 12/29] [Dev Deps] update `eslint-plugin-import` --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index fb6cd69467..25f98c6e10 100644 --- a/package.json +++ b/package.json @@ -52,7 +52,7 @@ "eslint": "^3 || ^4 || ^5 || ^6 || ^7", "eslint-config-airbnb-base": "^14.2.1", "eslint-plugin-eslint-plugin": "^2.3.0 || ^3.3.1", - "eslint-plugin-import": "^2.24.1", + "eslint-plugin-import": "^2.24.2", "eslint-remote-tester": "^1.3.0", "eslint-remote-tester-repositories": "^0.0.2", "espree": "^3.5.4", From 4b509afda0188edfa0909d779b79a9ac01aab2cd Mon Sep 17 00:00:00 2001 From: Jordan Harband Date: Sat, 28 Aug 2021 22:37:45 -0700 Subject: [PATCH 13/29] [Fix] `prop-types`, `propTypes`: prevent crash introduced in #3051 Fixes #3053 --- CHANGELOG.md | 2 ++ lib/util/propTypes.js | 22 ++++++++++++---------- tests/lib/rules/prop-types.js | 12 ++++++++++++ 3 files changed, 26 insertions(+), 10 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index fae1848cc7..275af82699 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -24,6 +24,7 @@ This change log adheres to standards from [Keep a CHANGELOG](http://keepachangel * [`jsx-no-bind`]: handle local function declarations ([#3048][] @p7g) * [`prop-types`], `propTypes`: handle React.* TypeScript types ([#3049][] @vedadeepta) * [`prop-types`], `propTypes`: add handling for `FC`, improve tests ([#3051][] @vedadeepta) +* [`prop-types`], `propTypes`: prevent crash introduced in [#3051][] ([#3053][] @ljharb) ### Changed * [Docs] [`jsx-no-bind`]: updates discussion of refs ([#2998][] @dimitropoulos) @@ -34,6 +35,7 @@ This change log adheres to standards from [Keep a CHANGELOG](http://keepachangel * [Docs] improve instructions for `jsx-runtime` config ([#3052][] @ljharb) [bb64df65]: https://github.com/yannickcr/eslint-plugin-react/commit/bb64df6505b3e9a01da5b61626ab9f544caea438 +[#3053]: https://github.com/yannickcr/eslint-plugin-react/issues/3053 [#3052]: https://github.com/yannickcr/eslint-plugin-react/issues/3052 [#3051]: https://github.com/yannickcr/eslint-plugin-react/pull/3051 [#3049]: https://github.com/yannickcr/eslint-plugin-react/pull/3049 diff --git a/lib/util/propTypes.js b/lib/util/propTypes.js index df24de496e..889d84596e 100644 --- a/lib/util/propTypes.js +++ b/lib/util/propTypes.js @@ -942,17 +942,19 @@ module.exports = function propTypesInstructions(context, components, utils) { return; } - if (annotation.typeName.name) { // if FC - const typeName = annotation.typeName.name; - if (!genericReactTypesImport.has(typeName)) { - return; - } - } else if (annotation.typeName.right.name) { // if React.FC - const right = annotation.typeName.right.name; - const left = annotation.typeName.left.name; + if (annotation.typeName) { + if (annotation.typeName.name) { // if FC + const typeName = annotation.typeName.name; + if (!genericReactTypesImport.has(typeName)) { + return; + } + } else if (annotation.typeName.right.name) { // if React.FC + const right = annotation.typeName.right.name; + const left = annotation.typeName.left.name; - if (!genericReactTypesImport.has(left) || !allowedGenericTypes.has(right)) { - return; + if (!genericReactTypesImport.has(left) || !allowedGenericTypes.has(right)) { + return; + } } } markPropTypesAsDeclared(node, resolveTypeAnnotation(siblingIdentifier)); diff --git a/tests/lib/rules/prop-types.js b/tests/lib/rules/prop-types.js index 3eaa9277d2..1e778596ec 100644 --- a/tests/lib/rules/prop-types.js +++ b/tests/lib/rules/prop-types.js @@ -3280,6 +3280,18 @@ ruleTester.run('prop-types', rule, { }; `, parser: parsers['@TYPESCRIPT_ESLINT'] + }, + { + code: ` + export const EuiSuperSelectControl: ( + props: EuiSuperSelectControlProps + ) => ReturnType>> = ({ + ...rest + }) => { + return null; + } + `, + parser: parsers['@TYPESCRIPT_ESLINT'] } ]), { From 261d93afe76dbd03056877af3b3a47e1c7760306 Mon Sep 17 00:00:00 2001 From: Jordan Harband Date: Fri, 27 Aug 2021 21:31:55 -0700 Subject: [PATCH 14/29] Update CHANGELOG and bump version --- CHANGELOG.md | 3 +++ package.json | 2 +- 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 275af82699..60f4e36c46 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,8 @@ This change log adheres to standards from [Keep a CHANGELOG](http://keepachangel ## Unreleased +## [7.25.0] - 2021.08.27 + ### Added * [`jsx-no-useless-fragments`]: add option to allow single expressions in fragments ([#3006][] @mattdarveniza) * add [`prefer-exact-props`] rule ([#1547][] @jomasti) @@ -34,6 +36,7 @@ This change log adheres to standards from [Keep a CHANGELOG](http://keepachangel * [Tests] add weekly scheduled smoke tests ([#2963][] @AriPerkkio) * [Docs] improve instructions for `jsx-runtime` config ([#3052][] @ljharb) +[7.25.0]: https://github.com/yannickcr/eslint-plugin-react/compare/v7.24.0...v7.25.0 [bb64df65]: https://github.com/yannickcr/eslint-plugin-react/commit/bb64df6505b3e9a01da5b61626ab9f544caea438 [#3053]: https://github.com/yannickcr/eslint-plugin-react/issues/3053 [#3052]: https://github.com/yannickcr/eslint-plugin-react/issues/3052 diff --git a/package.json b/package.json index 25f98c6e10..3f9d0e35bf 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "eslint-plugin-react", - "version": "7.24.0", + "version": "7.25.0", "author": "Yannick Croissant ", "description": "React specific linting rules for ESLint", "main": "index.js", From 179550babbe3621bd857bb3b8b7910a5fc968c96 Mon Sep 17 00:00:00 2001 From: Pedro Palacios Avila Date: Sun, 29 Aug 2021 17:20:55 -0500 Subject: [PATCH 15/29] [Fix] `no-this-in-sfc`, component detection: Improve stateless component detection Fixes #3054. --- CHANGELOG.md | 5 +++ lib/util/Components.js | 51 ++++++++++++++++++++----------- tests/lib/rules/no-this-in-sfc.js | 18 +++++++++-- 3 files changed, 54 insertions(+), 20 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 60f4e36c46..b7b22ecdca 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,11 @@ This change log adheres to standards from [Keep a CHANGELOG](http://keepachangel ## Unreleased +### Fixed +* [`no-this-in-sfc`], component detection: Improve stateless component detection ([#3056][] @Wesitos) + +[#3056]: https://github.com/yannickcr/eslint-plugin-react/pull/3056 + ## [7.25.0] - 2021.08.27 ### Added diff --git a/lib/util/Components.js b/lib/util/Components.js index 15627285dd..c0e62b46bf 100644 --- a/lib/util/Components.js +++ b/lib/util/Components.js @@ -608,6 +608,7 @@ function componentRule(rule, context) { * @returns {ASTNode | undefined} */ getStatelessComponent(node) { + const parent = node.parent; if ( node.type === 'FunctionDeclaration' && (!node.id || isFirstLetterCapitalized(node.id.name)) @@ -617,6 +618,13 @@ function componentRule(rule, context) { } if (node.type === 'FunctionExpression' || node.type === 'ArrowFunctionExpression') { + const isMethod = parent.type === 'Property' && parent.method; + const isPropertyAssignment = parent.type === 'AssignmentExpression' + && parent.left.type === 'MemberExpression'; + const isModuleExportsAssignment = isPropertyAssignment + && parent.left.object.name === 'module' + && parent.left.property.name === 'exports'; + if (node.parent.type === 'ExportDefaultDeclaration') { if (utils.isReturningJSX(node)) { return node; @@ -630,31 +638,38 @@ function componentRule(rule, context) { } return undefined; } - if (utils.isInAllowedPositionForComponent(node) && utils.isReturningJSXOrNull(node)) { - if (utils.isParentComponentNotStatelessComponent(node)) return undefined; - const isMethod = node.parent.type === 'Property' && node.parent.method; + // Case like `React.memo(() => <>)` or `React.forwardRef(...)` + const pragmaComponentWrapper = utils.getPragmaComponentWrapper(node); + if (pragmaComponentWrapper) { + return pragmaComponentWrapper; + } - if (isMethod && !isFirstLetterCapitalized(node.parent.key.name)) { - return utils.isReturningJSX(node) ? node : undefined; - } + if (!(utils.isInAllowedPositionForComponent(node) && utils.isReturningJSXOrNull(node))) { + return undefined; + } - if (node.id && isFirstLetterCapitalized(node.id.name)) { - return node; - } + if (utils.isParentComponentNotStatelessComponent(node)) { + return undefined; + } - if (!node.id) { - return node; - } + if (isMethod && !isFirstLetterCapitalized(node.parent.key.name)) { + return utils.isReturningJSX(node) ? node : undefined; + } - return undefined; + if (node.id) { + return isFirstLetterCapitalized(node.id.name) ? node : undefined; } - // Case like `React.memo(() => <>)` or `React.forwardRef(...)` - const pragmaComponentWrapper = utils.getPragmaComponentWrapper(node); - if (pragmaComponentWrapper) { - return pragmaComponentWrapper; + if ( + isPropertyAssignment + && !isModuleExportsAssignment + && !isFirstLetterCapitalized(parent.left.property.name) + ) { + return undefined; } + + return node; } return undefined; @@ -783,7 +798,7 @@ function componentRule(rule, context) { }, isParentComponentNotStatelessComponent(node) { - return ( + return !!( node.parent && node.parent.key && node.parent.key.type === 'Identifier' diff --git a/tests/lib/rules/no-this-in-sfc.js b/tests/lib/rules/no-this-in-sfc.js index cd53447734..724825d544 100644 --- a/tests/lib/rules/no-this-in-sfc.js +++ b/tests/lib/rules/no-this-in-sfc.js @@ -23,7 +23,7 @@ const parserOptions = { const ruleTester = new RuleTester({parserOptions}); ruleTester.run('no-this-in-sfc', rule, { - valid: [{ + valid: [].concat({ code: ` function Foo(props) { const { foo } = props; @@ -141,7 +141,21 @@ ruleTester.run('no-this-in-sfc', rule, { }, }); ` - }], + }, { + code: ` + obj.notAComponent = function () { + return this.a || null; + };` + }, parsers.TS([{ + code: ` + $.fn.getValueAsStringWeak = function (): string | null { + const val = this.length === 1 ? this.val() : null; + + return typeof val === 'string' ? val : null; + }; + `, + parser: parsers['@TYPESCRIPT_ESLINT'] + }])), invalid: [{ code: ` function Foo(props) { From 9487a1789e5153f6a2a6cd46958f2e7e92ae7457 Mon Sep 17 00:00:00 2001 From: Jordan Harband Date: Sun, 29 Aug 2021 22:02:04 -0700 Subject: [PATCH 16/29] Update CHANGELOG and bump version --- CHANGELOG.md | 3 +++ package.json | 2 +- 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index b7b22ecdca..199c8f52d5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,9 +5,12 @@ This change log adheres to standards from [Keep a CHANGELOG](http://keepachangel ## Unreleased +## [7.25.1] - 2021.08.29 + ### Fixed * [`no-this-in-sfc`], component detection: Improve stateless component detection ([#3056][] @Wesitos) +[7.25.1]: https://github.com/yannickcr/eslint-plugin-react/compare/v7.25.0...v7.25.1 [#3056]: https://github.com/yannickcr/eslint-plugin-react/pull/3056 ## [7.25.0] - 2021.08.27 diff --git a/package.json b/package.json index 3f9d0e35bf..7553a6d168 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "eslint-plugin-react", - "version": "7.25.0", + "version": "7.25.1", "author": "Yannick Croissant ", "description": "React specific linting rules for ESLint", "main": "index.js", From cc95a82e388c14c1e5898a450c59524213f5f3fe Mon Sep 17 00:00:00 2001 From: Benjamin Dobson <35073256+benj-dobs@users.noreply.github.com> Date: Mon, 30 Aug 2021 12:07:07 +0100 Subject: [PATCH 17/29] [Fix] `jsx-no-useless-fragment`: Allow insignificant whitespace when `allowExpressions: true` --- CHANGELOG.md | 5 +++++ docs/rules/jsx-no-useless-fragment.md | 4 ++++ lib/rules/jsx-no-useless-fragment.js | 13 +++++++++---- tests/lib/rules/jsx-no-useless-fragment.js | 9 +++++++++ 4 files changed, 27 insertions(+), 4 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 199c8f52d5..f7a78e0f40 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,11 @@ This change log adheres to standards from [Keep a CHANGELOG](http://keepachangel ## Unreleased +### Fixed +* [`jsx-no-useless-fragments`]: Handle insignificant whitespace correctly when `allowExpressions` is `true` ([#3061][] @benj-dobs) + +[#3061]: https://github.com/yannickcr/eslint-plugin-react/pull/3061 + ## [7.25.1] - 2021.08.29 ### Fixed diff --git a/docs/rules/jsx-no-useless-fragment.md b/docs/rules/jsx-no-useless-fragment.md index b92b83d8ec..a9e6992384 100644 --- a/docs/rules/jsx-no-useless-fragment.md +++ b/docs/rules/jsx-no-useless-fragment.md @@ -64,4 +64,8 @@ Examples of **correct** code for the rule, when `"allowExpressions"` is `true`: ```jsx <>{foo} + +<> + {foo} + ``` diff --git a/lib/rules/jsx-no-useless-fragment.js b/lib/rules/jsx-no-useless-fragment.js index 8cf654ebea..d02e38846d 100644 --- a/lib/rules/jsx-no-useless-fragment.js +++ b/lib/rules/jsx-no-useless-fragment.js @@ -77,10 +77,6 @@ function containsCallExpression(node) { && node.expression.type === 'CallExpression'; } -function isFragmentWithSingleExpression(node) { - return node && node.children.length === 1 && node.children[0].type === 'JSXExpressionContainer'; -} - module.exports = { meta: { type: 'suggestion', @@ -115,6 +111,15 @@ module.exports = { && arrayIncludes(node.raw, '\n'); } + function isFragmentWithSingleExpression(node) { + const children = node && node.children.filter((child) => !isPaddingSpaces(child)); + return ( + children + && children.length === 1 + && children[0].type === 'JSXExpressionContainer' + ); + } + /** * Test whether a JSXElement has less than two children, excluding paddings spaces. * @param {JSXElement|JSXFragment} node diff --git a/tests/lib/rules/jsx-no-useless-fragment.js b/tests/lib/rules/jsx-no-useless-fragment.js index 9dd3487ef5..0557fcdcb0 100644 --- a/tests/lib/rules/jsx-no-useless-fragment.js +++ b/tests/lib/rules/jsx-no-useless-fragment.js @@ -72,6 +72,15 @@ ruleTester.run('jsx-no-useless-fragment', rule, { code: '<>{moo}', parser: parsers.BABEL_ESLINT, options: [{allowExpressions: true}] + }, + { + code: ` + <> + {moo} + + `, + parser: parsers.BABEL_ESLINT, + options: [{allowExpressions: true}] } ], invalid: [ From 810806e58d5a611e6743227c91d859c2c02563bb Mon Sep 17 00:00:00 2001 From: vedadeepta Date: Tue, 31 Aug 2021 00:46:39 +0530 Subject: [PATCH 18/29] [Fix] `prop-types`, `propTypes`: handle implicit `children` prop in react's generic types --- CHANGELOG.md | 2 + lib/util/propTypes.js | 72 +++++++++++++++++++++++++++-------- tests/lib/rules/prop-types.js | 11 ++++++ 3 files changed, 69 insertions(+), 16 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index f7a78e0f40..b5d9057878 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,7 +7,9 @@ This change log adheres to standards from [Keep a CHANGELOG](http://keepachangel ### Fixed * [`jsx-no-useless-fragments`]: Handle insignificant whitespace correctly when `allowExpressions` is `true` ([#3061][] @benj-dobs) +* [`prop-types`], `propTypes`: handle implicit `children` prop in react's generic types ([#3064][] @vedadeepta) +[#3064]: https://github.com/yannickcr/eslint-plugin-react/pull/3064 [#3061]: https://github.com/yannickcr/eslint-plugin-react/pull/3061 ## [7.25.1] - 2021.08.29 diff --git a/lib/util/propTypes.js b/lib/util/propTypes.js index 889d84596e..bb0804a10a 100644 --- a/lib/util/propTypes.js +++ b/lib/util/propTypes.js @@ -100,7 +100,7 @@ module.exports = function propTypesInstructions(context, components, utils) { const defaults = {customValidators: []}; const configuration = Object.assign({}, defaults, context.options[0] || {}); const customValidators = configuration.customValidators; - const allowedGenericTypes = new Set(['SFC', 'StatelessComponent', 'FunctionComponent', 'FC']); + const allowedGenericTypes = new Set(['PropsWithChildren', 'SFC', 'StatelessComponent', 'FunctionComponent', 'FC']); const genericReactTypesImport = new Set(); /** @@ -496,6 +496,36 @@ module.exports = function propTypesInstructions(context, components, utils) { return {}; } + function isValidReactGenericTypeAnnotation(annotation) { + if (annotation.typeName) { + if (annotation.typeName.name) { // if FC + const typeName = annotation.typeName.name; + if (!genericReactTypesImport.has(typeName)) { + return false; + } + } else if (annotation.typeName.right.name) { // if React.FC + const right = annotation.typeName.right.name; + const left = annotation.typeName.left.name; + + if (!genericReactTypesImport.has(left) || !allowedGenericTypes.has(right)) { + return false; + } + } + } + return true; + } + + /** + * Returns the left most typeName of a node, e.g: FC, React.FC + * The representation is used to verify nested used properties. + * @param {ASTNode} node + * @return {string | undefined} + */ + function getTypeName(node) { + if (node.name) return node.name; + if (node.left) return getTypeName(node.left); + } + class DeclarePropTypesForTSTypeAnnotation { constructor(propTypes, declaredPropTypes) { this.propTypes = propTypes; @@ -549,8 +579,13 @@ module.exports = function propTypesInstructions(context, components, utils) { let typeName; if (astUtil.isTSTypeReference(node)) { typeName = node.typeName.name; - const shouldTraverseTypeParams = !typeName || genericReactTypesImport.has(typeName); + const shouldTraverseTypeParams = genericReactTypesImport.has(getTypeName(node.typeName)); if (shouldTraverseTypeParams && node.typeParameters && node.typeParameters.length !== 0) { + // All react Generic types are derived from: + // type PropsWithChildren

= P & { children?: ReactNode | undefined } + // So we should construct an optional children prop + this.shouldSpecifyOptionalChildrenProps = true; + const nextNode = node.typeParameters.params[0]; this.visitTSNode(nextNode); return; @@ -725,6 +760,14 @@ module.exports = function propTypesInstructions(context, components, utils) { } endAndStructDeclaredPropTypes() { + if (this.shouldSpecifyOptionalChildrenProps) { + this.declaredPropTypes.children = { + fullName: 'children', + name: 'children', + node: {}, + isRequired: false + }; + } this.foundDeclaredPropertiesList.forEach((tsInterfaceBody) => { if (tsInterfaceBody && (tsInterfaceBody.type === 'TSPropertySignature' || tsInterfaceBody.type === 'TSMethodSignature')) { let accessor = 'name'; @@ -928,6 +971,16 @@ module.exports = function propTypesInstructions(context, components, utils) { } }); } else { + // check if its a valid generic type when `X<{...}>` + if ( + param.typeAnnotation + && param.typeAnnotation.typeAnnotation + && param.typeAnnotation.typeAnnotation.type === 'TSTypeReference' + && param.typeAnnotation.typeAnnotation.typeParameters != null + && !isValidReactGenericTypeAnnotation(param.typeAnnotation.typeAnnotation) + ) { + return; + } markPropTypesAsDeclared(node, resolveTypeAnnotation(param)); } } else { @@ -942,21 +995,8 @@ module.exports = function propTypesInstructions(context, components, utils) { return; } - if (annotation.typeName) { - if (annotation.typeName.name) { // if FC - const typeName = annotation.typeName.name; - if (!genericReactTypesImport.has(typeName)) { - return; - } - } else if (annotation.typeName.right.name) { // if React.FC - const right = annotation.typeName.right.name; - const left = annotation.typeName.left.name; + if (!isValidReactGenericTypeAnnotation(annotation)) return; - if (!genericReactTypesImport.has(left) || !allowedGenericTypes.has(right)) { - return; - } - } - } markPropTypesAsDeclared(node, resolveTypeAnnotation(siblingIdentifier)); } } diff --git a/tests/lib/rules/prop-types.js b/tests/lib/rules/prop-types.js index 1e778596ec..d02c5f94a1 100644 --- a/tests/lib/rules/prop-types.js +++ b/tests/lib/rules/prop-types.js @@ -3292,6 +3292,17 @@ ruleTester.run('prop-types', rule, { } `, parser: parsers['@TYPESCRIPT_ESLINT'] + }, + { + code: ` + import React from 'react'; + + const MyComponent = (props: React.PropsWithChildren<{ username: string }>): React.ReactElement => { + return <>{props.children}{props.username}; + }; + + `, + parser: parsers['@TYPESCRIPT_ESLINT'] } ]), { From 1a5a970f8a7a57680eed29150505309f5597d25c Mon Sep 17 00:00:00 2001 From: Daniel Finke Date: Tue, 31 Aug 2021 16:48:14 -0700 Subject: [PATCH 19/29] [Fix] `display-name`: fix misinterpreted function components - when determining if an `ArrowFunctionExpression` returns JSX, do not check arguments to a returned `CallExpression` --- CHANGELOG.md | 2 ++ lib/util/jsx.js | 1 + tests/util/jsx.js | 22 ++++++++++++++++++---- 3 files changed, 21 insertions(+), 4 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index b5d9057878..1de52aec52 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,7 +8,9 @@ This change log adheres to standards from [Keep a CHANGELOG](http://keepachangel ### Fixed * [`jsx-no-useless-fragments`]: Handle insignificant whitespace correctly when `allowExpressions` is `true` ([#3061][] @benj-dobs) * [`prop-types`], `propTypes`: handle implicit `children` prop in react's generic types ([#3064][] @vedadeepta) +* [`display-name`]: fix arrow function returning result of function call with JSX arguments being interpreted as component ([#3065][], @danielfinke) +[#3065]: https://github.com/yannickcr/eslint-plugin-react/pull/3065 [#3064]: https://github.com/yannickcr/eslint-plugin-react/pull/3064 [#3061]: https://github.com/yannickcr/eslint-plugin-react/pull/3061 diff --git a/lib/util/jsx.js b/lib/util/jsx.js index 56bdf2214e..3afac915e3 100644 --- a/lib/util/jsx.js +++ b/lib/util/jsx.js @@ -130,6 +130,7 @@ function isReturningJSX(isCreateElement, ASTnode, context, strict, ignoreNull) { if (isCreateElement(childNode)) { setFound(); } + this.skip(); break; case 'Literal': if (!ignoreNull && childNode.value === null) { diff --git a/tests/util/jsx.js b/tests/util/jsx.js index 1062cebb1a..dce540f066 100644 --- a/tests/util/jsx.js +++ b/tests/util/jsx.js @@ -27,9 +27,7 @@ describe('jsxUtil', () => { const assertValid = (codeStr) => assert( isReturningJSX(() => false, parseCode(codeStr), mockContext) ); - const assertInValid = (codeStr) => assert( - !!isReturningJSX(() => false, parseCode(codeStr), mockContext) - ); + it('Works when returning JSX', () => { assertValid(` function Test() { @@ -71,11 +69,27 @@ describe('jsxUtil', () => { }); it('Can ignore null', () => { - assertInValid(` + assertValid(` function Test() { return null; } `); }); + + it('Ignores JSX arguments to function calls used as return value of arrow functions', () => { + let astNode = parseCode(`const obj = { + prop: () => test(something) + }`); + let arrowFunctionExpression = astNode.declarations[0].init.properties[0].value; + + assert(!isReturningJSX(() => false, arrowFunctionExpression, mockContext)); + + astNode = parseCode(`const obj = { + prop: () => { return test(something); } + }`); + arrowFunctionExpression = astNode.declarations[0].init.properties[0].value; + + assert(!isReturningJSX(() => false, arrowFunctionExpression, mockContext)); + }); }); }); From 952a7684ea6094637b57c06f8d058c9b81af7224 Mon Sep 17 00:00:00 2001 From: Jordan Harband Date: Thu, 2 Sep 2021 16:40:46 -0700 Subject: [PATCH 20/29] [Fix] `jsx-no-target-blank`: avoid crash on attr-only href Fixes #3066 Co-authored-by: Jordan Harband Co-authored-by: Gary Butler --- CHANGELOG.md | 4 +++- lib/rules/jsx-no-target-blank.js | 2 +- tests/lib/rules/jsx-no-target-blank.js | 3 +++ 3 files changed, 7 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 1de52aec52..cbd453efd6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,8 +8,10 @@ This change log adheres to standards from [Keep a CHANGELOG](http://keepachangel ### Fixed * [`jsx-no-useless-fragments`]: Handle insignificant whitespace correctly when `allowExpressions` is `true` ([#3061][] @benj-dobs) * [`prop-types`], `propTypes`: handle implicit `children` prop in react's generic types ([#3064][] @vedadeepta) -* [`display-name`]: fix arrow function returning result of function call with JSX arguments being interpreted as component ([#3065][], @danielfinke) +* [`display-name`]: fix arrow function returning result of function call with JSX arguments being interpreted as component ([#3065][] @danielfinke) +* [`jsx-no-target-blank`]: avoid crash on attr-only href ([#3066][] @ljharb @gaz77a) +[#3066]: https://github.com/yannickcr/eslint-plugin-react/issue/3066 [#3065]: https://github.com/yannickcr/eslint-plugin-react/pull/3065 [#3064]: https://github.com/yannickcr/eslint-plugin-react/pull/3064 [#3061]: https://github.com/yannickcr/eslint-plugin-react/pull/3061 diff --git a/lib/rules/jsx-no-target-blank.js b/lib/rules/jsx-no-target-blank.js index 6cd4ee3152..5e54504e81 100644 --- a/lib/rules/jsx-no-target-blank.js +++ b/lib/rules/jsx-no-target-blank.js @@ -49,7 +49,7 @@ function attributeValuePossiblyBlank(attribute) { function hasExternalLink(node, linkAttribute, warnOnSpreadAttributes, spreadAttributeIndex) { const linkIndex = findLastIndex(node.attributes, (attr) => attr.name && attr.name.name === linkAttribute); - const foundExternalLink = linkIndex !== -1 && ((attr) => attr.value.type === 'Literal' && /^(?:\w+:|\/\/)/.test(attr.value.value))( + const foundExternalLink = linkIndex !== -1 && ((attr) => attr.value && attr.value.type === 'Literal' && /^(?:\w+:|\/\/)/.test(attr.value.value))( node.attributes[linkIndex]); return foundExternalLink || (warnOnSpreadAttributes && linkIndex < spreadAttributeIndex); } diff --git a/tests/lib/rules/jsx-no-target-blank.js b/tests/lib/rules/jsx-no-target-blank.js index 22503c5e78..ce2402325f 100644 --- a/tests/lib/rules/jsx-no-target-blank.js +++ b/tests/lib/rules/jsx-no-target-blank.js @@ -135,6 +135,9 @@ ruleTester.run('jsx-no-target-blank', rule, { { code: '

', options: [{forms: true, links: false}] + }, + { + code: '' } ], invalid: [ From 9325fa5052bdb71f009c06ef303d35eb9ec19d84 Mon Sep 17 00:00:00 2001 From: Jordan Harband Date: Sat, 4 Sep 2021 23:34:34 -0700 Subject: [PATCH 21/29] [Fix] `propTypes`, `no-unused-prop-types`: remove the fake `node` added in #3064 Fixes #3068 --- lib/util/propTypes.js | 1 - lib/util/usedPropTypes.js | 2 +- tests/lib/rules/no-unused-prop-types.js | 16 ++++++++++++++++ 3 files changed, 17 insertions(+), 2 deletions(-) diff --git a/lib/util/propTypes.js b/lib/util/propTypes.js index bb0804a10a..24b83a579e 100644 --- a/lib/util/propTypes.js +++ b/lib/util/propTypes.js @@ -764,7 +764,6 @@ module.exports = function propTypesInstructions(context, components, utils) { this.declaredPropTypes.children = { fullName: 'children', name: 'children', - node: {}, isRequired: false }; } diff --git a/lib/util/usedPropTypes.js b/lib/util/usedPropTypes.js index dc62c8b3ba..aad4798b68 100644 --- a/lib/util/usedPropTypes.js +++ b/lib/util/usedPropTypes.js @@ -454,7 +454,7 @@ module.exports = function usedPropTypesInstructions(context, components, utils) Object.keys(propTypes).forEach((key) => { const node = propTypes[key].node; - if (node.value && astUtil.isFunctionLikeExpression(node.value)) { + if (node && node.value && astUtil.isFunctionLikeExpression(node.value)) { markPropTypesAsUsed(node.value); } }); diff --git a/tests/lib/rules/no-unused-prop-types.js b/tests/lib/rules/no-unused-prop-types.js index fa5f732927..589de12fad 100644 --- a/tests/lib/rules/no-unused-prop-types.js +++ b/tests/lib/rules/no-unused-prop-types.js @@ -3908,6 +3908,22 @@ ruleTester.run('no-unused-prop-types', rule, { type StateProps = ReturnType type DispatchProps = ReturnType`, parser: parsers['@TYPESCRIPT_ESLINT'] + }, + { + code: [ + 'import React from "react";', + '', + 'interface Props {', + ' name: string;', + '}', + '', + 'const MyComponent: React.FC = ({ name }) => {', + ' return
{name}
;', + '};', + '', + 'export default MyComponent;' + ].join('\n'), + parser: parsers['@TYPESCRIPT_ESLINT'] } ]) ), From 230ffe1a18057e682c76e57241a85683b7d0e637 Mon Sep 17 00:00:00 2001 From: Jordan Harband Date: Sun, 5 Sep 2021 09:15:00 -0700 Subject: [PATCH 22/29] [Fix] `prefer-read-only-props`: avoid crash from #3064 See #3068 --- lib/rules/prefer-read-only-props.js | 2 +- tests/lib/rules/prefer-read-only-props.js | 37 +++++++++++++++++++++-- 2 files changed, 35 insertions(+), 4 deletions(-) diff --git a/lib/rules/prefer-read-only-props.js b/lib/rules/prefer-read-only-props.js index 028cf304cb..d97df5af67 100644 --- a/lib/rules/prefer-read-only-props.js +++ b/lib/rules/prefer-read-only-props.js @@ -51,7 +51,7 @@ module.exports = { Object.keys(component.declaredPropTypes).forEach((propName) => { const prop = component.declaredPropTypes[propName]; - if (!isFlowPropertyType(prop.node)) { + if (!prop.node || !isFlowPropertyType(prop.node)) { return; } diff --git a/tests/lib/rules/prefer-read-only-props.js b/tests/lib/rules/prefer-read-only-props.js index b6e66dfeed..4f27fce90c 100644 --- a/tests/lib/rules/prefer-read-only-props.js +++ b/tests/lib/rules/prefer-read-only-props.js @@ -29,7 +29,7 @@ const parserOptions = { const ruleTester = new RuleTester({parserOptions}); ruleTester.run('prefer-read-only-props', rule, { - valid: [ + valid: [].concat( { // Class component with type parameter code: ` @@ -162,8 +162,39 @@ ruleTester.run('prefer-read-only-props', rule, { } `, parser: parsers.BABEL_ESLINT - } - ], + }, + parsers.TS([{ + code: ` + import React from "react"; + + interface Props { + name: string; + } + + const MyComponent: React.FC = ({ name }) => { + return
{name}
; + }; + + export default MyComponent; + `, + parser: parsers.TYPESCRIPT_ESLINT + }, { + code: ` + import React from "react"; + + interface Props { + name: string; + } + + const MyComponent: React.FC = ({ name }) => { + return
{name}
; + }; + + export default MyComponent; + `, + parser: parsers['@TYPESCRIPT_ESLINT'] + }]) + ), invalid: [ { From c1c284fc79dfdf755fc491b581d183706ba317a0 Mon Sep 17 00:00:00 2001 From: Jordan Harband Date: Sun, 5 Sep 2021 09:17:00 -0700 Subject: [PATCH 23/29] [Fix] `require-default-props`: avoid crash from #3064 Fixes #3068. Fixes #3069 --- lib/rules/require-default-props.js | 3 ++ tests/lib/rules/require-default-props.js | 37 ++++++++++++++++++++++-- 2 files changed, 37 insertions(+), 3 deletions(-) diff --git a/lib/rules/require-default-props.js b/lib/rules/require-default-props.js index b6330b116b..ec718b626f 100644 --- a/lib/rules/require-default-props.js +++ b/lib/rules/require-default-props.js @@ -60,6 +60,9 @@ module.exports = { Object.keys(propTypes).forEach((propName) => { const prop = propTypes[propName]; + if (!prop.node) { + return; + } if (prop.isRequired) { if (forbidDefaultForRequired && defaultProps[propName]) { context.report({ diff --git a/tests/lib/rules/require-default-props.js b/tests/lib/rules/require-default-props.js index 3b3e248446..91d88ab448 100644 --- a/tests/lib/rules/require-default-props.js +++ b/tests/lib/rules/require-default-props.js @@ -30,7 +30,7 @@ const ruleTester = new RuleTester({parserOptions}); ruleTester.run('require-default-props', rule, { - valid: [ + valid: [].concat( // // stateless components as function declarations { @@ -1089,8 +1089,39 @@ ruleTester.run('require-default-props', rule, { }; `, parser: parsers.BABEL_ESLINT - } - ], + }, + parsers.TS([{ + code: ` + import React from "react"; + + interface Props { + name: string; + } + + const MyComponent: React.FC = ({ name }) => { + return
{name}
; + }; + + export default MyComponent; + `, + parser: parsers.TYPESCRIPT_ESLINT + }, { + code: ` + import React from "react"; + + interface Props { + name: string; + } + + const MyComponent: React.FC = ({ name }) => { + return
{name}
; + }; + + export default MyComponent; + `, + parser: parsers['@TYPESCRIPT_ESLINT'] + }]) + ), invalid: [ // From 577cb64fb651f979427834086a9b8d447ca81154 Mon Sep 17 00:00:00 2001 From: Jordan Harband Date: Sun, 5 Sep 2021 10:07:02 -0700 Subject: [PATCH 24/29] [actions] remove unnecessary setup-node --- .github/workflows/smoke-test.yml | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/.github/workflows/smoke-test.yml b/.github/workflows/smoke-test.yml index 0720122928..58ded28215 100644 --- a/.github/workflows/smoke-test.yml +++ b/.github/workflows/smoke-test.yml @@ -6,14 +6,11 @@ on: workflow_dispatch: jobs: - lint: + smoke-test: if: ${{ github.repository == 'yannickcr/eslint-plugin-react' || github.event_name == 'workflow_dispatch' }} runs-on: ubuntu-latest steps: - uses: actions/checkout@v2 - - uses: actions/setup-node@v1 - with: - node-version: 14 - uses: ljharb/actions/node/install@main name: 'nvm install lts/* && npm install' with: From 5340e9dfc6b6473d36fb6e5ea80f073f6085d195 Mon Sep 17 00:00:00 2001 From: Alan Orozco Date: Wed, 8 Sep 2021 16:02:36 -0700 Subject: [PATCH 25/29] [Fix] `jsx-uses-vars`: ignore lowercase tag names Fixes https://github.com/eslint/eslint/issues/15040 --- CHANGELOG.md | 2 ++ lib/rules/jsx-uses-vars.js | 7 +++++++ tests/lib/rules/jsx-uses-vars.js | 19 +++++++++++++++++++ 3 files changed, 28 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index cbd453efd6..243f656964 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,7 +10,9 @@ This change log adheres to standards from [Keep a CHANGELOG](http://keepachangel * [`prop-types`], `propTypes`: handle implicit `children` prop in react's generic types ([#3064][] @vedadeepta) * [`display-name`]: fix arrow function returning result of function call with JSX arguments being interpreted as component ([#3065][] @danielfinke) * [`jsx-no-target-blank`]: avoid crash on attr-only href ([#3066][] @ljharb @gaz77a) +* [`jsx-uses-vars`]: ignore lowercase tag names ([#3070][] @alanorozco) +[#3070]: https://github.com/yannickcr/eslint-plugin-react/pull/3070 [#3066]: https://github.com/yannickcr/eslint-plugin-react/issue/3066 [#3065]: https://github.com/yannickcr/eslint-plugin-react/pull/3065 [#3064]: https://github.com/yannickcr/eslint-plugin-react/pull/3064 diff --git a/lib/rules/jsx-uses-vars.js b/lib/rules/jsx-uses-vars.js index 2ccdc5d360..ac94789a38 100644 --- a/lib/rules/jsx-uses-vars.js +++ b/lib/rules/jsx-uses-vars.js @@ -11,6 +11,9 @@ const docsUrl = require('../util/docsUrl'); // Rule Definition // ------------------------------------------------------------------------------ +const isTagNameRe = /^[a-z]/; +const isTagName = (name) => isTagNameRe.test(name); + module.exports = { meta: { docs: { @@ -33,6 +36,10 @@ module.exports = { if (node.name.name) { // name = node.name.name; + // Exclude lowercase tag names like
+ if (isTagName(name)) { + return; + } } else if (node.name.object) { // let parent = node.name.object; diff --git a/tests/lib/rules/jsx-uses-vars.js b/tests/lib/rules/jsx-uses-vars.js index ce02d7fc29..1b2e0d0561 100644 --- a/tests/lib/rules/jsx-uses-vars.js +++ b/tests/lib/rules/jsx-uses-vars.js @@ -116,6 +116,18 @@ ruleTester.run('no-unused-vars', ruleNoUnusedVars, { }; foo() ` + }, { + code: ` + /* eslint jsx-uses-vars: 1 */ + var object; + React.render(); + ` + }, { + code: ` + /* eslint jsx-uses-vars: 1 */ + var object; + React.render(); + ` } ], invalid: [ @@ -196,6 +208,13 @@ ruleTester.run('no-unused-vars', ruleNoUnusedVars, { line: 3 }], parser: parsers.BABEL_ESLINT + }, { + code: ` + /* eslint jsx-uses-vars: 1 */ + var lowercase; + React.render(); + `, + errors: [{message: '\'lowercase\' is defined but never used.'}] } ] }); From 9c1aee5eab8776b94d9d46cbcfa4bb53a8b4e175 Mon Sep 17 00:00:00 2001 From: Jordan Harband Date: Thu, 9 Sep 2021 13:24:40 -0700 Subject: [PATCH 26/29] [Dev Deps] update `eslint-remote-tester-repositories` --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 7553a6d168..5708846ee6 100644 --- a/package.json +++ b/package.json @@ -54,7 +54,7 @@ "eslint-plugin-eslint-plugin": "^2.3.0 || ^3.3.1", "eslint-plugin-import": "^2.24.2", "eslint-remote-tester": "^1.3.0", - "eslint-remote-tester-repositories": "^0.0.2", + "eslint-remote-tester-repositories": "^0.0.3", "espree": "^3.5.4", "istanbul": "^0.4.5", "markdown-magic": "^2.5.2", From f4854ea731a570f54c5c1287fe87baa7f819cafc Mon Sep 17 00:00:00 2001 From: Jordan Harband Date: Thu, 16 Sep 2021 16:25:39 -0700 Subject: [PATCH 27/29] Update CHANGELOG and bump version --- CHANGELOG.md | 3 +++ package.json | 2 +- 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 243f656964..403fcaeb08 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,8 @@ This change log adheres to standards from [Keep a CHANGELOG](http://keepachangel ## Unreleased +## [7.25.2] - 2021.09.16 + ### Fixed * [`jsx-no-useless-fragments`]: Handle insignificant whitespace correctly when `allowExpressions` is `true` ([#3061][] @benj-dobs) * [`prop-types`], `propTypes`: handle implicit `children` prop in react's generic types ([#3064][] @vedadeepta) @@ -12,6 +14,7 @@ This change log adheres to standards from [Keep a CHANGELOG](http://keepachangel * [`jsx-no-target-blank`]: avoid crash on attr-only href ([#3066][] @ljharb @gaz77a) * [`jsx-uses-vars`]: ignore lowercase tag names ([#3070][] @alanorozco) +[7.25.2]: https://github.com/yannickcr/eslint-plugin-react/compare/v7.25.1...v7.25.2 [#3070]: https://github.com/yannickcr/eslint-plugin-react/pull/3070 [#3066]: https://github.com/yannickcr/eslint-plugin-react/issue/3066 [#3065]: https://github.com/yannickcr/eslint-plugin-react/pull/3065 diff --git a/package.json b/package.json index 5708846ee6..e9bbfb01d7 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "eslint-plugin-react", - "version": "7.25.1", + "version": "7.25.2", "author": "Yannick Croissant ", "description": "React specific linting rules for ESLint", "main": "index.js", From bd270fc700da09a327af36c0da2b9a9437bca2a1 Mon Sep 17 00:00:00 2001 From: Prateek Surana Date: Fri, 10 Sep 2021 22:22:56 +0530 Subject: [PATCH 28/29] [readme] Update broken link for configuration files --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index baee650534..193920c819 100644 --- a/README.md +++ b/README.md @@ -241,7 +241,7 @@ To enable this configuration use the `extends` property in your `.eslintrc` conf } ``` -See [ESLint documentation](http://eslint.org/docs/user-guide/configuring#extending-configuration-files) for more information about extending configuration files. +See [ESLint documentation](https://eslint.org/docs/user-guide/configuring/configuration-files#extending-configuration-files) for more information about extending configuration files. ## All From 227e9678e9b1e29137027837d6cd2e7eb3e4aaf0 Mon Sep 17 00:00:00 2001 From: vedadeepta Date: Sat, 18 Sep 2021 18:30:32 +0530 Subject: [PATCH 29/29] [Fix] `prop-types`, `propTypes`: bail out unknown generic types inside func params --- CHANGELOG.md | 5 +++++ lib/util/propTypes.js | 10 ---------- tests/lib/rules/prop-types.js | 30 ++++++++++++++++++++++++++++++ 3 files changed, 35 insertions(+), 10 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 403fcaeb08..963d7a0663 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,11 @@ This change log adheres to standards from [Keep a CHANGELOG](http://keepachangel ## Unreleased +### Fixed +* [`prop-types`], `propTypes`: bail out unknown generic types inside func params ([#3076] @vedadeepta) + +[#3076]: https://github.com/yannickcr/eslint-plugin-react/pull/3076 + ## [7.25.2] - 2021.09.16 ### Fixed diff --git a/lib/util/propTypes.js b/lib/util/propTypes.js index 24b83a579e..b9e397deb7 100644 --- a/lib/util/propTypes.js +++ b/lib/util/propTypes.js @@ -970,16 +970,6 @@ module.exports = function propTypesInstructions(context, components, utils) { } }); } else { - // check if its a valid generic type when `X<{...}>` - if ( - param.typeAnnotation - && param.typeAnnotation.typeAnnotation - && param.typeAnnotation.typeAnnotation.type === 'TSTypeReference' - && param.typeAnnotation.typeAnnotation.typeParameters != null - && !isValidReactGenericTypeAnnotation(param.typeAnnotation.typeAnnotation) - ) { - return; - } markPropTypesAsDeclared(node, resolveTypeAnnotation(param)); } } else { diff --git a/tests/lib/rules/prop-types.js b/tests/lib/rules/prop-types.js index d02c5f94a1..55edbbbff7 100644 --- a/tests/lib/rules/prop-types.js +++ b/tests/lib/rules/prop-types.js @@ -3303,6 +3303,19 @@ ruleTester.run('prop-types', rule, { `, parser: parsers['@TYPESCRIPT_ESLINT'] + }, + { + code: ` + type Props = { + value: ValueType; + onClick: (value: ValueType) => void; + }; + + const Button = ({ onClick, value }: Props) => { + return ; + }; + `, + parser: parsers['@TYPESCRIPT_ESLINT'] } ]), { @@ -6865,6 +6878,23 @@ ruleTester.run('prop-types', rule, { } ], parser: parsers['@TYPESCRIPT_ESLINT'] + }, + { + code: ` + type PersonProps = { + x: T; + } + const Person = (props: PersonProps): React.ReactElement => ( +
{props.username}
+ ); + `, + errors: [ + { + messageId: 'missingPropType', + data: {name: 'username'} + } + ], + parser: parsers['@TYPESCRIPT_ESLINT'] } ]) )