/
requireReadonlyReactProps.js
135 lines (110 loc) · 3.84 KB
/
requireReadonlyReactProps.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
const schema = [];
const reComponentName = /^(Pure)?Component$/;
const reReadOnly = /^\$(ReadOnly|FlowFixMe)$/;
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) => {
const id = node.superTypeParameters.params[0].id;
return id && !reReadOnly.test(id.name) && !readOnlyTypes.includes(id.name);
};
const isReadOnlyObjectType = (node) => {
if (!node || node.type !== 'ObjectTypeAnnotation') {
return false;
}
// we consider `{||}` to be ReadOnly since it's exact AND has no props
if (node.exact && node.properties.length === 0) {
return true;
}
// { +foo: ..., +bar: ..., ... }
return node.properties.length > 0 &&
node.properties.every((prop) => {
return prop.variance && prop.variance.kind === 'plus';
});
};
const isReadOnlyType = (node) => {
return node.type === 'TypeAlias' && node.right.id && reReadOnly.test(node.right.id.name) || isReadOnlyObjectType(node.right);
};
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' &&
!isReadOnlyObjectType(node.superTypeParameters.params[0])) {
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) &&
!reReadOnly.test(identifier.name)) {
if (reportedFunctionalComponents.includes(identifier)) {
return;
}
context.report({
message: identifier.name + ' must be $ReadOnly',
node
});
reportedFunctionalComponents.push(identifier);
return;
}
if (typeAnnotation.typeAnnotation.type === 'ObjectTypeAnnotation' &&
!isReadOnlyObjectType(typeAnnotation.typeAnnotation)) {
context.report({
message: currentNode.id.name + ' component props must be $ReadOnly',
node
});
}
}
}
};
};
export default {
create,
schema
};