From 3a035798d510feb92735af5d452e96a9c01873c3 Mon Sep 17 00:00:00 2001 From: Jordan Harband Date: Wed, 28 Aug 2024 22:18:55 -0700 Subject: [PATCH 1/4] [Refactor] `usedPropTypes`: avoid switch statements --- lib/util/ast.js | 21 ++++++ lib/util/usedPropTypes.js | 151 +++++++++++++++++--------------------- 2 files changed, 87 insertions(+), 85 deletions(-) diff --git a/lib/util/ast.js b/lib/util/ast.js index 452b1a1ef2..4cc140254c 100644 --- a/lib/util/ast.js +++ b/lib/util/ast.js @@ -448,6 +448,24 @@ function isTSTypeParameterInstantiation(node) { return node.type === 'TSTypeParameterInstantiation'; } +function isMemberExpression(node) { + if (!node) { return false; } + + return node.type === 'MemberExpression' || node.type === 'OptionalMemberExpression'; +} + +function isObjectPattern(node) { + if (!node) { return false; } + + return node.type === 'ObjectPattern'; +} + +function isVariableDeclarator(node) { + if (!node) { return false; } + + return node.type === 'VariableDeclarator'; +} + module.exports = { findReturnStatement, getComponentProperties, @@ -462,7 +480,9 @@ module.exports = { isFunction, isFunctionLike, isFunctionLikeExpression, + isMemberExpression, isNodeFirstInLine, + isObjectPattern, isParenthesized, isTSAsExpression, isTSFunctionType, @@ -477,6 +497,7 @@ module.exports = { isTSTypeParameterInstantiation, isTSTypeQuery, isTSTypeReference, + isVariableDeclarator, traverse, traverseReturns, unwrapTSAsExpression, diff --git a/lib/util/usedPropTypes.js b/lib/util/usedPropTypes.js index 41eb307d6b..f0dae34ca4 100644 --- a/lib/util/usedPropTypes.js +++ b/lib/util/usedPropTypes.js @@ -9,7 +9,6 @@ const values = require('object.values'); const astUtil = require('./ast'); const componentUtil = require('./componentUtil'); const testReactVersion = require('./version').testReactVersion; -const ast = require('./ast'); const eslintUtil = require('./eslint'); const getScope = eslintUtil.getScope; @@ -164,7 +163,7 @@ function isInLifeCycleMethod(node, checkAsyncSafeLifeCycles) { */ function isSetStateUpdater(node) { const unwrappedParentCalleeNode = astUtil.isCallExpression(node.parent) - && ast.unwrapTSAsExpression(node.parent.callee); + && astUtil.unwrapTSAsExpression(node.parent.callee); return unwrappedParentCalleeNode && unwrappedParentCalleeNode.property @@ -181,7 +180,7 @@ function isPropArgumentInSetStateUpdater(context, node, name) { while (scope) { const unwrappedParentCalleeNode = scope.block && astUtil.isCallExpression(scope.block.parent) - && ast.unwrapTSAsExpression(scope.block.parent.callee); + && astUtil.unwrapTSAsExpression(scope.block.parent.callee); if ( unwrappedParentCalleeNode && unwrappedParentCalleeNode.property @@ -215,7 +214,7 @@ function isInClassComponent(context, node) { function isThisDotProps(node) { return !!node && node.type === 'MemberExpression' - && ast.unwrapTSAsExpression(node.object).type === 'ThisExpression' + && astUtil.unwrapTSAsExpression(node.object).type === 'ThisExpression' && node.property.name === 'props'; } @@ -239,7 +238,7 @@ function hasSpreadOperator(context, node) { * @returns {boolean} */ function isPropTypesUsageByMemberExpression(context, node, utils, checkAsyncSafeLifeCycles) { - const unwrappedObjectNode = ast.unwrapTSAsExpression(node.object); + const unwrappedObjectNode = astUtil.unwrapTSAsExpression(node.object); if (isInClassComponent(context, node)) { // this.props.* @@ -260,7 +259,7 @@ function isPropTypesUsageByMemberExpression(context, node, utils, checkAsyncSafe return false; } // props.* in function component - return unwrappedObjectNode.name === 'props' && !ast.isAssignmentLHS(node); + return unwrappedObjectNode.name === 'props' && !astUtil.isAssignmentLHS(node); } /** @@ -321,109 +320,91 @@ module.exports = function usedPropTypesInstructions(context, components, utils) let name; let allNames; let properties; - switch (node.type) { - case 'OptionalMemberExpression': - case 'MemberExpression': - name = getPropertyName(context, node, utils, checkAsyncSafeLifeCycles); - if (name) { - allNames = parentNames.concat(name); - if ( - // Match props.foo.bar, don't match bar[props.foo] - node.parent.type === 'MemberExpression' - && node.parent.object === node - ) { - markPropTypesAsUsed(node.parent, allNames); - } - // Handle the destructuring part of `const {foo} = props.a.b` - if ( - node.parent.type === 'VariableDeclarator' - && node.parent.id.type === 'ObjectPattern' - ) { - node.parent.id.parent = node.parent; // patch for bug in eslint@4 in which ObjectPattern has no parent - markPropTypesAsUsed(node.parent.id, allNames); - } - - // const a = props.a - if ( - node.parent.type === 'VariableDeclarator' - && node.parent.id.type === 'Identifier' - ) { - propVariables.set(node.parent.id.name, allNames); - } - // Do not mark computed props as used. - type = name !== '__COMPUTED_PROP__' ? 'direct' : null; + if (astUtil.isMemberExpression(node)) { + name = getPropertyName(context, node, utils, checkAsyncSafeLifeCycles); + if (name) { + allNames = parentNames.concat(name); + const parent = node.parent; + if ( + // Match props.foo.bar, don't match bar[props.foo] + parent.type === 'MemberExpression' + && 'object' in parent + && parent.object === node + ) { + markPropTypesAsUsed(parent, allNames); } - break; - case 'ArrowFunctionExpression': - case 'FunctionDeclaration': - case 'FunctionExpression': { - if (node.params.length === 0) { - break; + // Handle the destructuring part of `const {foo} = props.a.b` + if ( + astUtil.isVariableDeclarator(parent) + && parent.id.type === 'ObjectPattern' + ) { + parent.id.parent = parent; // patch for bug in eslint@4 in which ObjectPattern has no parent + markPropTypesAsUsed(parent.id, allNames); + } + + // const a = props.a + if ( + astUtil.isVariableDeclarator(parent) + && parent.id.type === 'Identifier' + ) { + propVariables.set(parent.id.name, allNames); } + // Do not mark computed props as used. + type = name !== '__COMPUTED_PROP__' ? 'direct' : null; + } + } else if (astUtil.isFunctionLike(node)) { + if (node.params.length > 0) { type = 'destructuring'; const propParam = isSetStateUpdater(node) ? node.params[1] : node.params[0]; properties = propParam.type === 'AssignmentPattern' ? propParam.left.properties : propParam.properties; - break; } - case 'ObjectPattern': - type = 'destructuring'; - properties = node.properties; - break; - case 'TSEmptyBodyFunctionExpression': - break; - default: - throw new Error(`${node.type} ASTNodes are not handled by markPropTypesAsUsed`); + } else if (astUtil.isObjectPattern(node)) { + type = 'destructuring'; + properties = node.properties; + } else if (node.type !== 'TSEmptyBodyFunctionExpression') { + throw new Error(`${node.type} ASTNodes are not handled by markPropTypesAsUsed`); } const component = components.get(utils.getParentComponent(node)); const usedPropTypes = (component && component.usedPropTypes) || []; let ignoreUnusedPropTypesValidation = (component && component.ignoreUnusedPropTypesValidation) || false; - switch (type) { - case 'direct': { - // Ignore Object methods - if (name in Object.prototype) { - break; - } - + if (type === 'direct') { + // Ignore Object methods + if (!(name in Object.prototype)) { const reportedNode = node.property; usedPropTypes.push({ name, allNames, node: reportedNode, }); - break; } - case 'destructuring': { - for (let k = 0, l = (properties || []).length; k < l; k++) { - if (hasSpreadOperator(context, properties[k]) || properties[k].computed) { - ignoreUnusedPropTypesValidation = true; - break; - } - const propName = ast.getKeyValue(context, properties[k]); + } else if (type === 'destructuring') { + for (let k = 0, l = (properties || []).length; k < l; k++) { + if (hasSpreadOperator(context, properties[k]) || properties[k].computed) { + ignoreUnusedPropTypesValidation = true; + break; + } + const propName = astUtil.getKeyValue(context, properties[k]); - if (!propName || properties[k].type !== 'Property') { - break; - } + if (!propName || properties[k].type !== 'Property') { + break; + } - usedPropTypes.push({ - allNames: parentNames.concat([propName]), - name: propName, - node: properties[k], - }); + usedPropTypes.push({ + allNames: parentNames.concat([propName]), + name: propName, + node: properties[k], + }); - if (properties[k].value.type === 'ObjectPattern') { - markPropTypesAsUsed(properties[k].value, parentNames.concat([propName])); - } else if (properties[k].value.type === 'Identifier') { - propVariables.set(properties[k].value.name, parentNames.concat(propName)); - } + if (properties[k].value.type === 'ObjectPattern') { + markPropTypesAsUsed(properties[k].value, parentNames.concat([propName])); + } else if (properties[k].value.type === 'Identifier') { + propVariables.set(properties[k].value.name, parentNames.concat(propName)); } - break; } - default: - break; } components.set(component ? component.node : node, { @@ -484,7 +465,7 @@ module.exports = function usedPropTypesInstructions(context, components, utils) return { VariableDeclarator(node) { - const unwrappedInitNode = ast.unwrapTSAsExpression(node.init); + const unwrappedInitNode = astUtil.unwrapTSAsExpression(node.init); // let props = this.props if (isThisDotProps(unwrappedInitNode) && isInClassComponent(context, node) && node.id.type === 'Identifier') { @@ -559,7 +540,7 @@ module.exports = function usedPropTypesInstructions(context, components, utils) return; } - const propVariable = propVariables.get(ast.unwrapTSAsExpression(node.object).name); + const propVariable = propVariables.get(astUtil.unwrapTSAsExpression(node.object).name); if (propVariable) { markPropTypesAsUsed(node, propVariable); } From fd08120ddbe15a4d2fb510c6ed705314d38201f0 Mon Sep 17 00:00:00 2001 From: derek stapleton Date: Mon, 5 May 2025 14:44:25 -0500 Subject: [PATCH 2/4] [Fix] `no-unknown-property`: allow `onLoad` on `body` They body tag supports an onload event https://developer.mozilla.org/en-US/docs/Web/API/HTMLElement/load_event --- CHANGELOG.md | 5 +++++ lib/rules/no-unknown-property.js | 2 +- tests/lib/rules/no-unknown-property.js | 3 ++- 3 files changed, 8 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 002a1ed942..ddbbfef5c8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,11 @@ This change log adheres to standards from [Keep a CHANGELOG](https://keepachange ## Unreleased +### Fixed +* [`no-unknown-property`]: allow `onLoad` on `body` ([#3923][] @DerekStapleton) + +[#3923]: https://github.com/jsx-eslint/eslint-plugin-react/pull/3923 + ## [7.37.5] - 2025.04.03 ### Fixed diff --git a/lib/rules/no-unknown-property.js b/lib/rules/no-unknown-property.js index d702cc4303..ff87d2f0ff 100644 --- a/lib/rules/no-unknown-property.js +++ b/lib/rules/no-unknown-property.js @@ -85,7 +85,7 @@ const ATTRIBUTE_TAGS_MAP = { onEncrypted: ['audio', 'video'], onEnded: ['audio', 'video'], onError: ['audio', 'video', 'img', 'link', 'source', 'script', 'picture', 'iframe'], - onLoad: ['script', 'img', 'link', 'picture', 'iframe', 'object', 'source'], + onLoad: ['script', 'img', 'link', 'picture', 'iframe', 'object', 'source', 'body'], onLoadedData: ['audio', 'video'], onLoadedMetadata: ['audio', 'video'], onLoadStart: ['audio', 'video'], diff --git a/tests/lib/rules/no-unknown-property.js b/tests/lib/rules/no-unknown-property.js index f24257a04d..ab0d5fb6d0 100644 --- a/tests/lib/rules/no-unknown-property.js +++ b/tests/lib/rules/no-unknown-property.js @@ -74,6 +74,7 @@ ruleTester.run('no-unknown-property', rule, { { code: '' }, { code: '' }, { code: '' }, + { code: '' }, { code: '