forked from gajus/eslint-plugin-flowtype
-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat: add require-readonly-react-props rule (gajus#400)
* Add requireReadOnlyReactProps rule * Update messages * Change naming * Fix imports * Fix linter * Fix some edge cases * Fix few more edge cases * Make covariant notation work * Update docs
- Loading branch information
Showing
7 changed files
with
406 additions
and
1 deletion.
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
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,82 @@ | ||
### `require-readonly-react-props` | ||
|
||
This rule validates that React props are marked as $ReadOnly. React props are immutable and modifying them could lead to unexpected results. Marking prop shapes as $ReadOnly avoids these issues. | ||
|
||
The rule tries its best to work with both class and functional components. For class components, it does a fuzzy check for one of "Component", "PureComponent", "React.Component" and "React.PureComponent". It doesn't actually infer that those identifiers resolve to a proper `React.Component` object. | ||
|
||
For example, this will NOT be checked: | ||
|
||
```js | ||
import MyReact from 'react'; | ||
class Foo extends MyReact.Component { } | ||
``` | ||
|
||
As a result, you can safely use other classes without getting warnings from this rule: | ||
|
||
```js | ||
class MyClass extends MySuperClass { } | ||
``` | ||
|
||
React's functional components are hard to detect statically. The way it's done here is by searching for JSX within a function. When present, a function is considered a React component: | ||
|
||
```js | ||
// this gets checked | ||
type Props = { }; | ||
function MyComponent(props: Props) { | ||
return <p />; | ||
} | ||
|
||
// this doesn't get checked since no JSX is present in a function | ||
type Options = { }; | ||
function SomeHelper(options: Options) { | ||
// ... | ||
} | ||
|
||
// this doesn't get checked since no JSX is present directly in a function | ||
function helper() { return <p /> } | ||
function MyComponent(props: Props) { | ||
return helper(); | ||
} | ||
``` | ||
|
||
The rule only works for locally defined props that are marked with a `$ReadOnly` or using covariant notation. It doesn't work with imported props: | ||
|
||
```js | ||
// the rule has no way of knowing whether ImportedProps are read-only | ||
import { type ImportedProps } from './somewhere'; | ||
class Foo extends React.Component<ImportedProps> { } | ||
|
||
|
||
// the rule also checks for covariant properties | ||
type Props = {| | ||
+foo: string | ||
|}; | ||
class Bar extends React.Component<Props> { } | ||
|
||
// this fails because the object is not fully read-only | ||
type Props = {| | ||
+foo: string, | ||
bar: number, | ||
|}; | ||
class Bar extends React.Component<Props> { } | ||
|
||
// this fails because spreading makes object mutable (as of Flow 0.98) | ||
// https://github.com/gajus/eslint-plugin-flowtype/pull/400#issuecomment-489813899 | ||
type Props = {| | ||
+foo: string, | ||
...bar, | ||
|}; | ||
class Bar extends React.Component<Props> { } | ||
``` | ||
|
||
|
||
```js | ||
{ | ||
"rules": { | ||
"flowtype/require-readonly-react-props": 2 | ||
} | ||
} | ||
``` | ||
|
||
|
||
<!-- assertions requireReadOnlyReactProps --> |
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
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,124 @@ | ||
const schema = []; | ||
|
||
const reComponentName = /^(Pure)?Component$/; | ||
|
||
const isReactComponent = (node) => { | ||
if (!node.superClass) { | ||
return false; | ||
} | ||
|
||
return ( | ||
|
||
// class Foo extends Component { } | ||
// class Foo extends PureComponent { } | ||
node.superClass.type === 'Identifier' && reComponentName.test(node.superClass.name) || | ||
|
||
// class Foo extends React.Component { } | ||
// class Foo extends React.PureComponent { } | ||
node.superClass.type === 'MemberExpression' && | ||
(node.superClass.object.name === 'React' && reComponentName.test(node.superClass.property.name)) | ||
); | ||
}; | ||
|
||
const create = (context) => { | ||
const readOnlyTypes = []; | ||
const reportedFunctionalComponents = []; | ||
|
||
const isReadOnlyClassProp = (node) => { | ||
return node.superTypeParameters.params[0].id && | ||
node.superTypeParameters.params[0].id.name !== '$ReadOnly' && | ||
!readOnlyTypes.includes(node.superTypeParameters.params[0].id.name); | ||
}; | ||
|
||
const isReadOnlyObjectType = (node) => { | ||
return node.type === 'TypeAlias' && | ||
node.right && | ||
node.right.type === 'ObjectTypeAnnotation' && | ||
node.right.properties.length > 0 && | ||
node.right.properties.every((prop) => { | ||
return prop.variance && prop.variance.kind === 'plus'; | ||
}); | ||
}; | ||
|
||
const isReadOnlyType = (node) => { | ||
return node.type === 'TypeAlias' && node.right.id && node.right.id.name === '$ReadOnly' || isReadOnlyObjectType(node); | ||
}; | ||
|
||
for (const node of context.getSourceCode().ast.body) { | ||
// type Props = $ReadOnly<{}> | ||
if (isReadOnlyType(node) || | ||
|
||
// export type Props = $ReadOnly<{}> | ||
node.type === 'ExportNamedDeclaration' && | ||
node.declaration && | ||
isReadOnlyType(node.declaration)) { | ||
readOnlyTypes.push(node.id ? node.id.name : node.declaration.id.name); | ||
} | ||
} | ||
|
||
return { | ||
|
||
// class components | ||
ClassDeclaration (node) { | ||
if (isReactComponent(node) && isReadOnlyClassProp(node)) { | ||
context.report({ | ||
message: node.superTypeParameters.params[0].id.name + ' must be $ReadOnly', | ||
node | ||
}); | ||
} else if (node.superTypeParameters && node.superTypeParameters.params[0].type === 'ObjectTypeAnnotation') { | ||
context.report({ | ||
message: node.id.name + ' class props must be $ReadOnly', | ||
node | ||
}); | ||
} | ||
}, | ||
|
||
// functional components | ||
JSXElement (node) { | ||
let currentNode = node; | ||
let identifier; | ||
let typeAnnotation; | ||
|
||
while (currentNode && currentNode.type !== 'FunctionDeclaration') { | ||
currentNode = currentNode.parent; | ||
} | ||
|
||
// functional components can only have 1 param | ||
if (!currentNode || currentNode.params.length !== 1) { | ||
return; | ||
} | ||
|
||
if (currentNode.params[0].type === 'Identifier' && | ||
(typeAnnotation = currentNode.params[0].typeAnnotation)) { | ||
if ((identifier = typeAnnotation.typeAnnotation.id) && | ||
!readOnlyTypes.includes(identifier.name) && | ||
identifier.name !== '$ReadOnly') { | ||
if (reportedFunctionalComponents.includes(identifier)) { | ||
return; | ||
} | ||
|
||
context.report({ | ||
message: identifier.name + ' must be $ReadOnly', | ||
node | ||
}); | ||
|
||
reportedFunctionalComponents.push(identifier); | ||
|
||
return; | ||
} | ||
|
||
if (typeAnnotation.typeAnnotation.type === 'ObjectTypeAnnotation') { | ||
context.report({ | ||
message: currentNode.id.name + ' component props must be $ReadOnly', | ||
node | ||
}); | ||
} | ||
} | ||
} | ||
}; | ||
}; | ||
|
||
export default { | ||
create, | ||
schema | ||
}; |
Oops, something went wrong.