diff --git a/rules/no-default-props.js b/rules/no-default-props.js index 427a872..d2a97ed 100644 --- a/rules/no-default-props.js +++ b/rules/no-default-props.js @@ -31,7 +31,8 @@ module.exports = { fix(fixer) { const sourceCode = context.sourceCode ?? context.getSourceCode(); const defaultProps = node.right.properties.reduce((acc, prop) => { - acc[prop.key.name] = sourceCode.getText(prop.value); + const keyName = prop.key.name || prop.key.value; + acc[keyName] = sourceCode.getText(prop.value); return acc; }, {}); @@ -53,47 +54,160 @@ module.exports = { const fixes = []; if ( - componentNode && - componentNode.params && - componentNode.params[0] && - componentNode.params[0].type === "ObjectPattern" + componentNode.type === "FunctionDeclaration" || + componentNode.type === "FunctionExpression" || + componentNode.type === "ArrowFunctionExpression" ) { - const params = componentNode.params[0]; - const firstProp = params.properties[0]; - const lastProp = - params.properties[params.properties.length - 1]; + // Handle functional component + if ( + componentNode.params && + componentNode.params[0] && + componentNode.params[0].type === "ObjectPattern" + ) { + const params = componentNode.params[0]; + const properties = params.properties; - // Check if props are on multiple lines - const isMultiline = - firstProp.loc.start.line !== lastProp.loc.end.line; + const existingProps = properties.reduce((acc, prop) => { + acc[prop.key.name || prop.key.value] = prop; + return acc; + }, {}); - const newParams = params.properties - .map((prop) => { - const propName = prop.key.name; - const defaultValue = defaultProps[propName] - ? ` = ${defaultProps[propName]}` - : ""; - return `${propName}${defaultValue}`; - }) - .join(isMultiline ? ",\n" : ", "); + for (const [key, value] of Object.entries(defaultProps)) { + if (existingProps[key]) { + // Update existing property + const prop = existingProps[key]; + const propText = sourceCode.getText(prop.key); + const newPropText = `${propText} = ${value}`; + fixes.push(fixer.replaceText(prop, newPropText)); + } else { + // Add new property + const newPropText = `${key} = ${value}`; + const lastProp = properties[properties.length - 1]; + fixes.push( + fixer.insertTextAfter(lastProp, `, ${newPropText}`), + ); + } + } + } else { + // Insert new parameter + const newParamsText = `{ ${Object.entries(defaultProps) + .map(([key, value]) => `${key} = ${value}`) + .join(", ")} }`; + if (componentNode.params.length > 0) { + fixes.push( + fixer.replaceText(componentNode.params[0], newParamsText), + ); + } else { + fixes.push( + fixer.insertTextBefore( + componentNode.body, + `(${newParamsText}) => `, + ), + ); + } + } + } else if ( + componentNode.type === "ClassDeclaration" || + componentNode.type === "ClassExpression" + ) { + // Handle class component + const body = componentNode.body.body; + const renderMethod = body.find( + (method) => + method.type === "MethodDefinition" && + method.kind === "method" && + method.key.name === "render", + ); + + if ( + !renderMethod || + !renderMethod.value || + !renderMethod.value.body + ) { + return null; + } + + const renderBody = renderMethod.value.body.body; + + // Find existing destructuring of this.props + let destructuringDeclarator = null; + for (const statement of renderBody) { + if (statement.type === "VariableDeclaration") { + for (const declarator of statement.declarations) { + if ( + declarator.init && + declarator.init.type === "MemberExpression" && + declarator.init.object.type === "ThisExpression" && + declarator.init.property.name === "props" && + declarator.id.type === "ObjectPattern" + ) { + destructuringDeclarator = declarator; + break; + } + } + if (destructuringDeclarator) { + break; + } + } + } + + if (destructuringDeclarator) { + // Modify existing destructuring + const properties = destructuringDeclarator.id.properties; + const existingProps = properties.reduce((acc, prop) => { + acc[prop.key.name || prop.key.value] = prop; + return acc; + }, {}); - const newParamsText = isMultiline - ? `{\n${newParams}\n}` - : `{ ${newParams} }`; - fixes.push(fixer.replaceText(params, newParamsText)); + for (const [key, value] of Object.entries(defaultProps)) { + if (existingProps[key]) { + // Update existing property + const prop = existingProps[key]; + const propText = sourceCode.getText(prop.key); + const newPropText = `${propText} = ${value}`; + fixes.push(fixer.replaceText(prop, newPropText)); + } else { + // Add new property + const newPropText = `${key} = ${value}`; + const lastProp = properties[properties.length - 1]; + fixes.push( + fixer.insertTextAfter(lastProp, `, ${newPropText}`), + ); + } + } + } else { + // Insert new destructuring + const newDestructuringText = `const { ${Object.entries( + defaultProps, + ) + .map(([key, value]) => `${key} = ${value}`) + .join(", ")} } = this.props;`; + const firstStatement = renderBody[0]; + if (firstStatement) { + fixes.push( + fixer.insertTextBefore( + firstStatement, + `${newDestructuringText}\n`, + ), + ); + } else { + fixes.push( + fixer.insertTextAfter( + renderMethod.value.body.openingBrace, + `\n${newDestructuringText}\n`, + ), + ); + } + } } else { - const newParams = Object.entries(defaultProps) - .map(([key, value]) => `${key} = ${value}`) - .join(", "); - const newParamsText = `{ ${newParams} }`; - fixes.push( - fixer.insertTextBefore( - componentNode.body, - `(${newParamsText}) => `, - ), - ); + // Component type not recognized + return null; } + // Remove the defaultProps assignment + fixes.push(fixer.remove(node)); + + // Remove semicolon if necessary const semicolonToken = sourceCode.getTokenAfter(node); if ( semicolonToken && @@ -103,7 +217,6 @@ module.exports = { fixes.push(fixer.remove(semicolonToken)); } - fixes.push(fixer.remove(node)); return fixes; }, }); diff --git a/tests/tests.js b/tests/tests.js index 808f6c6..a806d08 100644 --- a/tests/tests.js +++ b/tests/tests.js @@ -29,7 +29,22 @@ const ruleTester = new RuleTester({ try { // Test for "no-default-props" ruleTester.run("no-default-props", ruleNoDefaultProps, { - valid: [`const Component = ({ name }) =>