Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
185 changes: 149 additions & 36 deletions rules/no-default-props.js
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}, {});

Expand All @@ -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 &&
Expand All @@ -103,7 +217,6 @@ module.exports = {
fixes.push(fixer.remove(semicolonToken));
}

fixes.push(fixer.remove(node));
return fixes;
},
});
Expand Down
58 changes: 57 additions & 1 deletion tests/tests.js
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,22 @@ const ruleTester = new RuleTester({
try {
// Test for "no-default-props"
ruleTester.run("no-default-props", ruleNoDefaultProps, {
valid: [`const Component = ({ name }) => <div>{name}</div>;`],
valid: [
`const Component = ({ name }) => <div>{name}</div>;`,
// Class component without defaultProps
`
import { PureComponent } from 'react';

class ClassComponent extends PureComponent {
render() {
const { propKey } = this.props;
return propKey;
}
}

export default ClassComponent;
`,
],
invalid: [
{
code: `const Component = ({ name }) => <div>{name}</div>; Component.defaultProps = { name: 'Test' };`,
Expand All @@ -41,6 +56,46 @@ try {
],
output: `const Component = ({ name = 'Test' }) => <div>{name}</div>; `,
},
// Test case from the GitHub issue
{
code: `
import { PureComponent } from 'react';

class ClassComponent extends PureComponent {
render() {
const { propKey } = this.props;
return propKey;
}
}

ClassComponent.defaultProps = {
propKey: 'propValue',
};

export default ClassComponent;
`,
errors: [
{
message:
"'defaultProps' should not be used in 'ClassComponent' as they are no longer supported in React 19. Use default parameters instead.",
type: "AssignmentExpression",
},
],
output: `
import { PureComponent } from 'react';

class ClassComponent extends PureComponent {
render() {
const { propKey = 'propValue' } = this.props;
return propKey;
}
}



export default ClassComponent;
`,
},
],
});

Expand Down Expand Up @@ -135,6 +190,7 @@ try {
{
message:
"'getChildContext' uses a legacy context API that is no longer supported in React 19. Use 'React.createContext()' instead.",
type: "MethodDefinition",
},
],
},
Expand Down