diff --git a/.github/workflows/node-4+.yml b/.github/workflows/node-4+.yml index cbc44fe913..732f2273c6 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 && '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/.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: diff --git a/CHANGELOG.md b/CHANGELOG.md index 91344f511c..963d7a0663 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,12 +5,44 @@ 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 +* [`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) +* [`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 +[#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 + +### 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 + ### Added * [`jsx-no-useless-fragments`]: add option to allow single expressions in fragments ([#3006][] @mattdarveniza) * add [`prefer-exact-props`] rule ([#1547][] @jomasti) * [`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` ([bb64df65][] @ljharb) ### Fixed * component detection: use `estraverse` to improve component detection ([#2992][] @Wesitos) @@ -19,6 +51,11 @@ 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) +* [`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) @@ -26,7 +63,16 @@ 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) + +[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 +[#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 [#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/README.md b/README.md index ad95c7e922..193920c819 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)) @@ -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 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/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/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 diff --git a/index.js b/index.js index b44670d074..ec1e15a92e 100644 --- a/index.js +++ b/index.js @@ -169,7 +169,8 @@ module.exports = { parserOptions: { ecmaFeatures: { jsx: true - } + }, + jsxPragma: null // for @typescript/eslint-parser }, rules: { 'react/react-in-jsx-scope': 0, 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/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/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/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/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/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/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/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/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/lib/util/propTypes.js b/lib/util/propTypes.js index 95671b9a2f..b9e397deb7 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,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 = new Set(['PropsWithChildren', 'SFC', 'StatelessComponent', 'FunctionComponent', 'FC']); + const genericReactTypesImport = new Set(); /** * Returns the full scope. @@ -494,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; @@ -547,6 +579,17 @@ module.exports = function propTypesInstructions(context, components, utils) { let typeName; if (astUtil.isTSTypeReference(node)) { typeName = node.typeName.name; + 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; + } } else if (astUtil.isTSInterfaceHeritage(node)) { if (!node.expression && node.id) { typeName = node.id.name; @@ -717,6 +760,13 @@ module.exports = function propTypesInstructions(context, components, utils) { } endAndStructDeclaredPropTypes() { + if (this.shouldSpecifyOptionalChildrenProps) { + this.declaredPropTypes.children = { + fullName: 'children', + name: 'children', + isRequired: false + }; + } this.foundDeclaredPropertiesList.forEach((tsInterfaceBody) => { if (tsInterfaceBody && (tsInterfaceBody.type === 'TSPropertySignature' || tsInterfaceBody.type === 'TSMethodSignature')) { let accessor = 'name'; @@ -887,7 +937,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 +959,34 @@ 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; + } + + if (!isValidReactGenericTypeAnnotation(annotation)) return; + + markPropTypesAsDeclared(node, resolveTypeAnnotation(siblingIdentifier)); } } @@ -1001,6 +1076,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/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/package.json b/package.json index bdfb9f7cd1..e9bbfb01d7 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "eslint-plugin-react", - "version": "7.24.0", + "version": "7.25.2", "author": "Yannick Croissant ", "description": "React specific linting rules for ESLint", "main": "index.js", @@ -46,18 +46,18 @@ "@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.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", - "eslint-plugin-import": "^2.23.4", + "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.0.0", + "markdown-magic": "^2.5.2", "mocha": "^5.2.0", "semver": "^6.3.0", "sinon": "^7.5.0", 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: [{ 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-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: '', 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: [ 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: [ 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/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.'}] } ] }); 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) { 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: ` diff --git a/tests/lib/rules/no-unused-prop-types.js b/tests/lib/rules/no-unused-prop-types.js index 5b41d0e777..589de12fad 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'); @@ -3905,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'] } ]) ), @@ -4452,7 +4471,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 +4482,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 +4497,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/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: [ { diff --git a/tests/lib/rules/prop-types.js b/tests/lib/rules/prop-types.js index 2475d85257..55edbbbff7 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'); @@ -3144,6 +3147,175 @@ ruleTester.run('prop-types', rule, { } `, parser: parsers['@TYPESCRIPT_ESLINT'] + }, + { + code: ` + import React from 'react'; + + interface PersonProps { + username: string; + } + const Person: React.FunctionComponent = (props): React.ReactElement => ( +
{props.username}
+ ); + `, + parser: parsers['@TYPESCRIPT_ESLINT'] + }, + { + code: ` + import React from 'react'; + + const Person: React.FunctionComponent = (props): React.ReactElement => ( +
{props.username}
+ ); + `, + parser: parsers['@TYPESCRIPT_ESLINT'] + }, + { + code: ` + import React from 'react'; + + interface PersonProps { + username: string; + } + const Person: React.FC = (props): React.ReactElement => ( +
{props.username}
+ ); + `, + parser: parsers['@TYPESCRIPT_ESLINT'] + }, + { + code: ` + import React from 'react'; + const Person: React.FunctionComponent<{ username: string }> = (props): React.ReactElement => ( +
{props.username}
+ ); + `, + parser: parsers['@TYPESCRIPT_ESLINT'] + }, + { + code: ` + import React from 'react'; + type PersonProps = { + username: string; + } + const Person: React.FunctionComponent = (props): React.ReactElement => ( +
{props.username}
+ ); + `, + 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'] + }, + { + code: ` + export const EuiSuperSelectControl: ( + props: EuiSuperSelectControlProps + ) => ReturnType>> = ({ + ...rest + }) => { + return null; + } + `, + 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'] + }, + { + code: ` + type Props = { + value: ValueType; + onClick: (value: ValueType) => void; + }; + + const Button = ({ onClick, value }: Props) => { + return ; + }; + `, + parser: parsers['@TYPESCRIPT_ESLINT'] } ]), { @@ -3508,7 +3680,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 +3696,7 @@ ruleTester.run('prop-types', rule, { messageId: 'missingPropType', data: {name: 'firstname'} }] - }, { + } : [], { code: [ 'class Hello extends React.Component {', ' render() {', @@ -6618,6 +6790,111 @@ 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'] + }, + { + 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'] + }, + { + code: ` + type PersonProps = { + x: T; + } + const Person = (props: PersonProps): React.ReactElement => ( +
{props.username}
+ ); + `, + errors: [ + { + messageId: 'missingPropType', + data: {name: 'username'} + } + ], + parser: parsers['@TYPESCRIPT_ESLINT'] } ]) ) 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: [ // 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: [ 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)); + }); }); });