diff --git a/.github/workflows/node-18+.yml b/.github/workflows/node-18+.yml index 7dcb4daff5..ee7d126939 100644 --- a/.github/workflows/node-18+.yml +++ b/.github/workflows/node-18+.yml @@ -38,6 +38,64 @@ jobs: - 10 - 9 - 8 + typescript-eslint: + - 5 + - 6 + - 7 + - 8 + exclude: + - eslint: 3 + babel-eslint: 10 + - eslint: 3 + typescript-eslint: 5 + - eslint: 4 + typescript-eslint: 5 + - eslint: 4.14 + typescript-eslint: 5 + - eslint: 5 + typescript-eslint: 5 + - eslint: 3 + typescript-eslint: 6 + - eslint: 4 + typescript-eslint: 6 + - eslint: 4.14 + typescript-eslint: 6 + - eslint: 5 + typescript-eslint: 6 + - eslint: 6 + typescript-eslint: 6 + - eslint: 9 + typescript-eslint: 6 + - eslint: 3 + typescript-eslint: 7 + - eslint: 4 + typescript-eslint: 7 + - eslint: 4.14 + typescript-eslint: 7 + - eslint: 5 + typescript-eslint: 7 + - eslint: 6 + typescript-eslint: 7 + - eslint: 7 + typescript-eslint: 7 + - eslint: 9 + typescript-eslint: 7 + - eslint: 3 + typescript-eslint: 8 + - eslint: 4 + typescript-eslint: 8 + - eslint: 4.14 + typescript-eslint: 8 + - eslint: 5 + typescript-eslint: 8 + - eslint: 6 + typescript-eslint: 8 + - eslint: 7 + typescript-eslint: 8 + - eslint: 9 + typescript-eslint: 5 + - node-version: 19 + typescript-eslint: 7 steps: - uses: actions/checkout@v4 @@ -46,9 +104,9 @@ jobs: with: node-version: ${{ matrix.node-version }} after_install: | - npm install --no-save "eslint@${{ matrix.eslint }}" "@typescript-eslint/parser@5" "babel-eslint@${{ matrix.babel-eslint }}" + npm install --no-save "eslint@${{ matrix.eslint }}" "@typescript-eslint/parser@${{ matrix.typescript-eslint }}" "babel-eslint@${{ matrix.babel-eslint }}" env: - NPM_CONFIG_LEGACY_PEER_DEPS: true + NPM_CONFIG_LEGACY_PEER_DEPS: "${{ matrix.typescript-eslint >= 6 && 'false' || 'true' }}" - run: npx ls-engines - run: npm run unit-test - uses: codecov/codecov-action@v3.1.5 diff --git a/.github/workflows/node-minors.yml b/.github/workflows/node-minors.yml index 548b571dd2..69f92684ea 100644 --- a/.github/workflows/node-minors.yml +++ b/.github/workflows/node-minors.yml @@ -100,10 +100,10 @@ jobs: with: node-version: ${{ matrix.node-version }} after_install: | - npm install --no-save "eslint@${{ matrix.eslint }}" "@typescript-eslint/parser@${{ matrix.node-version >= 14 && '5' || (matrix.node-version >= 12 && '4' || (matrix.node-version >= 10 && '4.0' || (matrix.node-version >= 8 && '3' || '2'))) }}" "babel-eslint@${{ matrix.babel-eslint }}" + npm install --no-save "eslint@${{ matrix.eslint }}" "@typescript-eslint/parser@${{ matrix.node-version >= 18 && matrix.eslint >= 8 && '8' || (matrix.node-version >= 16 && matrix.eslint >= 7 && '6' || (matrix.node-version >= 14 && '5' || (matrix.node-version >= 12 && '4' || (matrix.node-version >= 10 && '4.0' || (matrix.node-version >= 8 && '3' || '2'))))) }}" "babel-eslint@${{ matrix.babel-eslint }}" skip-ls-check: ${{ matrix.node-version < 10 && true || false }} env: - NPM_CONFIG_LEGACY_PEER_DEPS: true + NPM_CONFIG_LEGACY_PEER_DEPS: "${{ matrix.node-version >= 16 && matrix.eslint >= 7 && 'false' || 'true' }}" - run: npx ls-engines if: ${{ matrix.node-version >= 12 }} - run: npm run unit-test diff --git a/CHANGELOG.md b/CHANGELOG.md index 63bc11a1f8..12c9e1f850 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,4 +1,5 @@ # Change Log + All notable changes to this project will be documented in this file. This project adheres to [Semantic Versioning](https://semver.org/). This change log adheres to standards from [Keep a CHANGELOG](https://keepachangelog.com). @@ -8,6 +9,14 @@ This change log adheres to standards from [Keep a CHANGELOG](https://keepachange ### Added * [`no-string-refs`]: allow this.refs in > 18.3.0 ([#3807][] @henryqdineen) +### Fixed +* [`function-component-definition`], [`boolean-prop-naming`], [`jsx-first-prop-new-line`], [`jsx-props-no-multi-spaces`], `propTypes`: use type args ([#3629][] @HenryBrown0) + +### Changed +* [Tests] add @typescript-eslint/parser v6 ([#3629][] @HenryBrown0) +* [Tests] add @typescript-eslint/parser v7 and v8 ([#3629][] @hampustagerud) + +[#3629]: https://github.com/jsx-eslint/eslint-plugin-react/pull/3629 [#3807]: https://github.com/jsx-eslint/eslint-plugin-react/pull/3807 ## [7.35.2] - 2024.09.03 diff --git a/lib/rules/boolean-prop-naming.js b/lib/rules/boolean-prop-naming.js index 27becc4525..afa844a903 100644 --- a/lib/rules/boolean-prop-naming.js +++ b/lib/rules/boolean-prop-naming.js @@ -259,14 +259,16 @@ module.exports = { return; } - const annotationTypeParams = component.node.parent.id.typeAnnotation.typeAnnotation.typeParameters; + const annotationTypeArguments = propsUtil.getTypeArguments( + component.node.parent.id.typeAnnotation.typeAnnotation + ); if ( - annotationTypeParams && ( - annotationTypeParams.type === 'TSTypeParameterInstantiation' - || annotationTypeParams.type === 'TypeParameterInstantiation' + annotationTypeArguments && ( + annotationTypeArguments.type === 'TSTypeParameterInstantiation' + || annotationTypeArguments.type === 'TypeParameterInstantiation' ) ) { - return annotationTypeParams.params.find( + return annotationTypeArguments.params.find( (param) => param.type === 'TSTypeReference' || param.type === 'GenericTypeAnnotation' ); } diff --git a/lib/rules/function-component-definition.js b/lib/rules/function-component-definition.js index 24ad3f48d7..cca665abe2 100644 --- a/lib/rules/function-component-definition.js +++ b/lib/rules/function-component-definition.js @@ -10,6 +10,7 @@ const Components = require('../util/Components'); const docsUrl = require('../util/docsUrl'); const reportC = require('../util/report'); const getText = require('../util/eslint').getText; +const propsUtil = require('../util/props'); // ------------------------------------------------------------------------------ // Rule Definition @@ -34,12 +35,12 @@ const UNNAMED_FUNCTION_TEMPLATES = { }; function hasOneUnconstrainedTypeParam(node) { - const nodeTypeParams = node.typeParameters; + const nodeTypeArguments = propsUtil.getTypeArguments(node); - return nodeTypeParams - && nodeTypeParams.params - && nodeTypeParams.params.length === 1 - && !nodeTypeParams.params[0].constraint; + return nodeTypeArguments + && nodeTypeArguments.params + && nodeTypeArguments.params.length === 1 + && !nodeTypeArguments.params[0].constraint; } function hasName(node) { @@ -202,11 +203,12 @@ module.exports = { varType = node.parent.parent.kind; } + const nodeTypeArguments = propsUtil.getTypeArguments(node); return (fixer) => fixer.replaceTextRange( options.range, buildFunction(options.template, { typeAnnotation, - typeParams: getNodeText(node.typeParameters, source), + typeParams: getNodeText(nodeTypeArguments, source), params: getParams(node, source), returnType: getNodeText(node.returnType, source), body: getBody(node, source), diff --git a/lib/rules/jsx-first-prop-new-line.js b/lib/rules/jsx-first-prop-new-line.js index 7c9d969b51..3014d24f1e 100644 --- a/lib/rules/jsx-first-prop-new-line.js +++ b/lib/rules/jsx-first-prop-new-line.js @@ -7,6 +7,7 @@ const docsUrl = require('../util/docsUrl'); const report = require('../util/report'); +const propsUtil = require('../util/props'); // ------------------------------------------------------------------------------ // Rule Definition @@ -55,7 +56,8 @@ module.exports = { report(context, messages.propOnNewLine, 'propOnNewLine', { node: decl, fix(fixer) { - return fixer.replaceTextRange([(node.typeParameters || node.name).range[1], decl.range[0]], '\n'); + const nodeTypeArguments = propsUtil.getTypeArguments(node); + return fixer.replaceTextRange([(nodeTypeArguments || node.name).range[1], decl.range[0]], '\n'); }, }); } diff --git a/lib/rules/jsx-max-depth.js b/lib/rules/jsx-max-depth.js index 12206a4081..12625fa704 100644 --- a/lib/rules/jsx-max-depth.js +++ b/lib/rules/jsx-max-depth.js @@ -91,7 +91,7 @@ module.exports = { function findJSXElementOrFragment(startNode, name, previousReferences) { function find(refs, prevRefs) { for (let i = refs.length - 1; i >= 0; i--) { - if (has(refs[i], 'writeExpr')) { + if (typeof refs[i].writeExpr !== 'undefined') { const writeExpr = refs[i].writeExpr; return (jsxUtil.isJSX(writeExpr) diff --git a/lib/rules/jsx-props-no-multi-spaces.js b/lib/rules/jsx-props-no-multi-spaces.js index ce80338cd8..8402c6d4ff 100644 --- a/lib/rules/jsx-props-no-multi-spaces.js +++ b/lib/rules/jsx-props-no-multi-spaces.js @@ -8,6 +8,7 @@ const docsUrl = require('../util/docsUrl'); const eslintUtil = require('../util/eslint'); const report = require('../util/report'); +const propsUtil = require('../util/props'); const getSourceCode = eslintUtil.getSourceCode; const getText = eslintUtil.getText; @@ -103,18 +104,18 @@ module.exports = { } function containsGenericType(node) { - const nodeTypeParams = node.typeParameters; - if (typeof nodeTypeParams === 'undefined') { + const nodeTypeArguments = propsUtil.getTypeArguments(node); + if (typeof nodeTypeArguments === 'undefined') { return false; } - return nodeTypeParams.type === 'TSTypeParameterInstantiation'; + return nodeTypeArguments.type === 'TSTypeParameterInstantiation'; } function getGenericNode(node) { const name = node.name; if (containsGenericType(node)) { - const type = node.typeParameters; + const nodeTypeArguments = propsUtil.getTypeArguments(node); return Object.assign( {}, @@ -122,7 +123,7 @@ module.exports = { { range: [ name.range[0], - type.range[1], + nodeTypeArguments.range[1], ], } ); diff --git a/lib/util/propTypes.js b/lib/util/propTypes.js index a1dce37d8b..f26a6510c9 100644 --- a/lib/util/propTypes.js +++ b/lib/util/propTypes.js @@ -41,7 +41,8 @@ function isFunctionType(node) { */ function isSuperTypeParameterPropsDeclaration(node) { if (node && (node.type === 'ClassDeclaration' || node.type === 'ClassExpression')) { - if (node.superTypeParameters && node.superTypeParameters.params.length > 0) { + const parameters = propsUtil.getSuperTypeArguments(node); + if (parameters && parameters.params.length > 0) { return true; } } @@ -639,8 +640,8 @@ module.exports = function propTypesInstructions(context, components, utils) { typeName = node.typeName.name; const leftMostName = getLeftMostTypeName(node.typeName); const shouldTraverseTypeParams = genericReactTypesImport.has(leftMostName); - const nodeTypeParams = node.typeParameters; - if (shouldTraverseTypeParams && nodeTypeParams && nodeTypeParams.length !== 0) { + const nodeTypeArguments = propsUtil.getTypeArguments(node); + if (shouldTraverseTypeParams && nodeTypeArguments && nodeTypeArguments.length !== 0) { // All react Generic types are derived from: // type PropsWithChildren

= P & { children?: ReactNode | undefined } // So we should construct an optional children prop @@ -662,7 +663,7 @@ module.exports = function propTypesInstructions(context, components, utils) { const idx = genericTypeParamIndexWherePropsArePresent[ leftMostName !== rightMostName ? rightMostName : importedName ]; - const nextNode = nodeTypeParams.params[idx]; + const nextNode = nodeTypeArguments.params[idx]; this.visitTSNode(nextNode); return; } @@ -759,10 +760,10 @@ module.exports = function propTypesInstructions(context, components, utils) { convertReturnTypeToPropTypes(node, rootNode) { // ReturnType should always have one parameter - const nodeTypeParams = node.typeParameters; - if (nodeTypeParams) { - if (nodeTypeParams.params.length === 1) { - let returnType = nodeTypeParams.params[0]; + const nodeTypeArguments = propsUtil.getTypeArguments(node); + if (nodeTypeArguments) { + if (nodeTypeArguments.params.length === 1) { + let returnType = nodeTypeArguments.params[0]; // This line is trying to handle typescript-eslint-parser // typescript-eslint-parser TSTypeQuery is wrapped by TSTypeReference if (astUtil.isTSTypeReference(returnType)) { @@ -794,9 +795,9 @@ module.exports = function propTypesInstructions(context, components, utils) { case 'ObjectExpression': iterateProperties(context, res.properties, (key, value, propNode) => { if (propNode && astUtil.isCallExpression(propNode.argument)) { - const propNodeTypeParams = propNode.argument.typeParameters; - if (propNodeTypeParams) { - this.visitTSNode(propNodeTypeParams); + const propNodeTypeArguments = propsUtil.getTypeArguments(propNode.argument); + if (propNodeTypeArguments) { + this.visitTSNode(propNodeTypeArguments); } else { // Ignore this CallExpression return value since it doesn't have any typeParameters to let us know it's types. this.shouldIgnorePropTypes = true; @@ -816,8 +817,8 @@ module.exports = function propTypesInstructions(context, components, utils) { }); break; case 'CallExpression': - if (res.typeParameters) { - this.visitTSNode(res.typeParameters); + if (propsUtil.getTypeArguments(res)) { + this.visitTSNode(propsUtil.getTypeArguments(res)); } else { // Ignore this CallExpression return value since it doesn't have any typeParameters to let us know it's types. this.shouldIgnorePropTypes = true; @@ -1002,9 +1003,9 @@ module.exports = function propTypesInstructions(context, components, utils) { break; case 'GenericTypeAnnotation': if (propTypes.id.name === '$ReadOnly') { - const propTypeParams = propTypes.typeParameters; + const propTypeArguments = propsUtil.getTypeArguments(propTypes); ignorePropsValidation = declarePropTypesForObjectTypeAnnotation( - propTypeParams.params[0], + propTypeArguments.params[0], declaredPropTypes ); } else { @@ -1041,11 +1042,16 @@ module.exports = function propTypesInstructions(context, components, utils) { return; } + let propTypesArguments = null; + if (node.parent) { + propTypesArguments = propsUtil.getTypeArguments(node.parent); + } + if ( node.parent && node.parent.callee - && node.parent.typeParameters - && node.parent.typeParameters.params + && propTypesArguments + && propTypesArguments.params && ( node.parent.callee.name === 'forwardRef' || ( node.parent.callee.object @@ -1055,9 +1061,8 @@ module.exports = function propTypesInstructions(context, components, utils) { ) ) ) { - const propTypesParams = node.parent.typeParameters; const declaredPropTypes = {}; - const obj = new DeclarePropTypesForTSTypeAnnotation(propTypesParams.params[1], declaredPropTypes, rootNode); + const obj = new DeclarePropTypesForTSTypeAnnotation(propTypesArguments.params[1], declaredPropTypes, rootNode); components.set(node, { declaredPropTypes: obj.declaredPropTypes, ignorePropsValidation: obj.shouldIgnorePropTypes, @@ -1103,7 +1108,7 @@ module.exports = function propTypesInstructions(context, components, utils) { if ( annotation && annotation.type !== 'TSTypeReference' - && annotation.typeParameters == null + && propsUtil.getTypeArguments(annotation) == null ) { return; } @@ -1115,23 +1120,25 @@ module.exports = function propTypesInstructions(context, components, utils) { } /** - * Resolve the type annotation for a given class declaration node with superTypeParameters. + * Resolve the type annotation for a given class declaration node. * * @param {ASTNode} node The annotation or a node containing the type annotation. * @returns {ASTNode} The resolved type annotation for the node. */ function resolveSuperParameterPropsType(node) { let propsParameterPosition; + const parameters = propsUtil.getSuperTypeArguments(node); + try { // Flow <=0.52 had 3 required TypedParameters of which the second one is the Props. // Flow >=0.53 has 2 optional TypedParameters of which the first one is the Props. propsParameterPosition = testFlowVersion(context, '>= 0.53.0') ? 0 : 1; } catch (e) { // In case there is no flow version defined, we can safely assume that when there are 3 Props we are dealing with version <= 0.52 - propsParameterPosition = node.superTypeParameters.params.length <= 2 ? 0 : 1; + propsParameterPosition = parameters.params.length <= 2 ? 0 : 1; } - let annotation = node.superTypeParameters.params[propsParameterPosition]; + let annotation = parameters.params[propsParameterPosition]; while (annotation && (annotation.type === 'TypeAnnotation' || annotation.type === 'NullableTypeAnnotation')) { annotation = annotation.typeAnnotation; } diff --git a/lib/util/props.js b/lib/util/props.js index f8a114ce6a..ac4ed8703b 100644 --- a/lib/util/props.js +++ b/lib/util/props.js @@ -93,6 +93,30 @@ function isRequiredPropType(propTypeExpression) { && propTypeExpression.property.name === 'isRequired'; } +/** + * Returns the type arguments of a node or type parameters if type arguments are not available. + * @param {ASTNode} node The node to get the type arguments from. + * @returns {ASTNode} The type arguments or type parameters of the node. + */ +function getTypeArguments(node) { + if ('typeArguments' in node) { + return node.typeArguments; + } + return node.typeParameters; +} + +/** + * Returns the super type arguments of a node or super type parameters if type arguments are not available. + * @param {ASTNode} node The node to get the super type arguments from. + * @returns {ASTNode} The super type arguments or parameters of the node. + */ +function getSuperTypeArguments(node) { + if ('superTypeArguments' in node) { + return node.superTypeArguments; + } + return node.superTypeParameters; +} + module.exports = { isPropTypesDeclaration, isContextTypesDeclaration, @@ -101,4 +125,6 @@ module.exports = { isDefaultPropsDeclaration, isDisplayNameDeclaration, isRequiredPropType, + getTypeArguments, + getSuperTypeArguments, }; diff --git a/package.json b/package.json index 3b157996cc..d0ca2ada83 100644 --- a/package.json +++ b/package.json @@ -54,7 +54,7 @@ "@types/eslint": "=7.2.10", "@types/estree": "0.0.52", "@types/node": "^4.9.5", - "@typescript-eslint/parser": "^2.34.0 || ^3.10.1 || ^4.0.0 || ^5.0.0", + "@typescript-eslint/parser": "^2.34.0 || ^3.10.1 || ^4 || ^5 || ^6.20 || ^7.14.1 || ^8.4", "babel-eslint": "^8 || ^9 || ^10.1.0", "eslint": "^3 || ^4 || ^5 || ^6 || ^7 || ^8 || ^9.7", "eslint-config-airbnb-base": "^15.0.0", diff --git a/tests/helpers/parsers.js b/tests/helpers/parsers.js index 24cc12bdb9..cbd6568d01 100644 --- a/tests/helpers/parsers.js +++ b/tests/helpers/parsers.js @@ -1,6 +1,5 @@ 'use strict'; -const path = require('path'); const semver = require('semver'); const entries = require('object.entries'); const version = require('eslint/package.json').version; @@ -31,13 +30,11 @@ function minEcmaVersion(features, parserOptions) { return Number.isFinite(result) ? result : undefined; } -const NODE_MODULES = '../../node_modules'; - const parsers = { - BABEL_ESLINT: path.join(__dirname, NODE_MODULES, 'babel-eslint'), - '@BABEL_ESLINT': path.join(__dirname, NODE_MODULES, '@babel/eslint-parser'), - TYPESCRIPT_ESLINT: path.join(__dirname, NODE_MODULES, 'typescript-eslint-parser'), - '@TYPESCRIPT_ESLINT': path.join(__dirname, NODE_MODULES, '@typescript-eslint/parser'), + BABEL_ESLINT: require.resolve('babel-eslint'), + '@BABEL_ESLINT': require.resolve('@babel/eslint-parser'), + TYPESCRIPT_ESLINT: require.resolve('typescript-eslint-parser'), + '@TYPESCRIPT_ESLINT': require.resolve('@typescript-eslint/parser'), disableNewTS, skipDueToMultiErrorSorting: semver.satisfies(process.versions.node, '^8 || ^9'), babelParserOptions: function parserOptions(test, features) {