-
-
Notifications
You must be signed in to change notification settings - Fork 32.5k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
[test] Lint useThemeVariants with custom rules plugin (#21963)
- Loading branch information
Showing
5 changed files
with
238 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
108 changes: 108 additions & 0 deletions
108
packages/eslint-plugin-material-ui/src/rules/rules-of-use-theme-variants.js
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,108 @@ | ||
module.exports = { | ||
meta: { | ||
type: 'problem', | ||
}, | ||
create(context) { | ||
function getComponentProps(componentBlockNode) { | ||
// finds the declarator in `const {...} = props;` | ||
let componentPropsDeclarator = null; | ||
componentBlockNode.body.forEach((node) => { | ||
if (node.type === 'VariableDeclaration') { | ||
const propsDeclarator = node.declarations.find((declarator) => { | ||
return declarator.init.name === 'props'; | ||
}); | ||
if (propsDeclarator !== undefined) { | ||
componentPropsDeclarator = propsDeclarator; | ||
} | ||
} | ||
}); | ||
|
||
return componentPropsDeclarator !== null ? componentPropsDeclarator.id : undefined; | ||
} | ||
|
||
function getComponentBlockNode(hookCallNode) { | ||
let node = hookCallNode.parent; | ||
while (node !== undefined) { | ||
if (node.type === 'BlockStatement') { | ||
return node; | ||
} | ||
node = node.parent; | ||
} | ||
return null; | ||
} | ||
|
||
return { | ||
CallExpression(node) { | ||
if (node.callee.name === 'useThemeVariants') { | ||
const componentBlockNode = getComponentBlockNode(node); | ||
|
||
const componentProps = getComponentProps(componentBlockNode); | ||
const defaultProps = | ||
componentProps === undefined | ||
? [] | ||
: componentProps.properties.filter((objectProperty) => { | ||
return ( | ||
objectProperty.type === 'Property' && | ||
objectProperty.value.type === 'AssignmentPattern' | ||
); | ||
}); | ||
|
||
const [variantProps] = node.arguments; | ||
|
||
const unsupportedComponentPropsNode = | ||
componentProps !== undefined && componentProps.type !== 'ObjectPattern'; | ||
|
||
if (unsupportedComponentPropsNode) { | ||
context.report({ | ||
node: componentProps, | ||
message: `Can only analyze object patterns but found '${componentProps.type}'. Prefer \`const {...} = props;\``, | ||
}); | ||
} | ||
|
||
if (defaultProps.length === 0) { | ||
return; | ||
} | ||
|
||
if (variantProps.type !== 'ObjectExpression') { | ||
context.report({ | ||
node: variantProps, | ||
message: `Can only analyze object patterns but found '${variantProps.type}'. Prefer \`{...props}\`.`, | ||
}); | ||
return; | ||
} | ||
|
||
const variantPropsRestNode = variantProps.properties.find((objectProperty) => { | ||
return objectProperty.type === 'SpreadElement'; | ||
}); | ||
|
||
if ( | ||
variantPropsRestNode !== undefined && | ||
variantProps.properties.indexOf(variantPropsRestNode) !== 0 && | ||
defaultProps.length > 0 | ||
) { | ||
context.report({ | ||
node: variantPropsRestNode, | ||
message: | ||
'The props spread must come first in the `useThemeVariants` props. Otherwise destructured props with default values could be overridden.', | ||
}); | ||
} | ||
|
||
defaultProps.forEach((componentProp) => { | ||
const isPassedToVariantProps = | ||
variantProps.properties.find((variantProp) => { | ||
return ( | ||
variantProp.type === 'Property' && componentProp.key.name === variantProp.key.name | ||
); | ||
}) !== undefined; | ||
if (!isPassedToVariantProps) { | ||
context.report({ | ||
node: variantProps, | ||
message: `Prop \`${componentProp.key.name}\` is not passed to \`useThemeVariants\` props.`, | ||
}); | ||
} | ||
}); | ||
} | ||
}, | ||
}; | ||
}, | ||
}; |
123 changes: 123 additions & 0 deletions
123
packages/eslint-plugin-material-ui/src/rules/rules-of-use-theme-variants.test.js
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,123 @@ | ||
const eslint = require('eslint'); | ||
const rule = require('./rules-of-use-theme-variants'); | ||
|
||
const ruleTester = new eslint.RuleTester({ | ||
parser: require.resolve('@typescript-eslint/parser'), | ||
parserOptions: { | ||
ecmaFeatures: { jsx: true }, | ||
}, | ||
}); | ||
ruleTester.run('rules-of-use-theme-variants', rule, { | ||
valid: [ | ||
// allowed but dangerous | ||
` | ||
{ | ||
const useCustomThemeVariants = props => useThemeVariants(props); | ||
}`, | ||
` | ||
{ | ||
useThemeVariants(props); | ||
} | ||
`, | ||
` | ||
{ | ||
const { className, value: valueProp, ...other } = props; | ||
useThemeVariants(props); | ||
} | ||
`, | ||
` | ||
{ | ||
const { className, disabled = false, value: valueProp, ...other } = props; | ||
useThemeVariants({ ...props, disabled }); | ||
} | ||
`, | ||
` | ||
{ | ||
const { className, value: valueProp, ...other } = props; | ||
const [stateA, setStateA] = React.useState(0); | ||
const [stateB, setStateB] = React.useState(0); | ||
useThemeVariants({ stateA, ...props, stateB }); | ||
} | ||
`, | ||
// unneccessary spread but it's not the responsibility of this rule to catch "unnecessary" spread | ||
` | ||
{ | ||
const { className, value: valueProp, ...other } = props; | ||
useThemeVariants({ ...props}); | ||
} | ||
`, | ||
], | ||
invalid: [ | ||
{ | ||
code: ` | ||
{ | ||
const { disabled = false, ...other } = props; | ||
useThemeVariants({ ...props}); | ||
} | ||
`, | ||
errors: [ | ||
{ | ||
message: 'Prop `disabled` is not passed to `useThemeVariants` props.', | ||
line: 4, | ||
column: 20, | ||
endLine: 4, | ||
endColumn: 31, | ||
}, | ||
], | ||
}, | ||
{ | ||
code: ` | ||
{ | ||
const { disabled = false, variant = 'text', ...other } = props; | ||
useThemeVariants({ ...props, disabled }); | ||
} | ||
`, | ||
errors: [ | ||
{ | ||
message: 'Prop `variant` is not passed to `useThemeVariants` props.', | ||
line: 4, | ||
column: 20, | ||
endLine: 4, | ||
endColumn: 42, | ||
}, | ||
], | ||
}, | ||
{ | ||
code: ` | ||
{ | ||
const { disabled = false, ...other } = props; | ||
useThemeVariants({ disabled, ...props }); | ||
} | ||
`, | ||
errors: [ | ||
{ | ||
message: | ||
'The props spread must come first in the `useThemeVariants` props. Otherwise destructured props with default values could be overridden.', | ||
line: 4, | ||
column: 32, | ||
endLine: 4, | ||
endColumn: 40, | ||
}, | ||
], | ||
}, | ||
// this is valid code but not analyzeable by this rule | ||
{ | ||
code: ` | ||
{ | ||
const { disabled = false, ...other } = props; | ||
const themeVariantProps = { ...props, disabled }; | ||
useThemeVariants(themeVariantProps); | ||
} | ||
`, | ||
errors: [ | ||
{ | ||
message: "Can only analyze object patterns but found 'Identifier'. Prefer `{...props}`.", | ||
line: 5, | ||
column: 20, | ||
endLine: 5, | ||
endColumn: 37, | ||
}, | ||
], | ||
}, | ||
], | ||
}); |