Skip to content

Commit 3c76ab7

Browse files
Optimize import handling for files with multiple components and simplify visitors logic
1 parent 29953ed commit 3c76ab7

File tree

2 files changed

+122
-136
lines changed

2 files changed

+122
-136
lines changed

Diff for: index.js

+120-135
Original file line numberDiff line numberDiff line change
@@ -7,14 +7,18 @@ const pluginName = 'check-prop-types';
77

88
const reactClassComponentExtends = ['Component', 'PureComponent'];
99

10-
const importIdentifier = '_checkPropTypes';
11-
const arrowPropertiesIdentifier = '_props';
10+
const importIdentifierName = '_checkPropTypes';
11+
const importSourceName = 'prop-types/checkPropTypes';
12+
const arrowPropertiesIdentifierName = '_props';
1213

1314
// implementation
1415

1516
export default ({ types }) => {
1617
let fileName;
17-
let programBody;
18+
19+
let importIdentifierNode;
20+
let importExists = false;
21+
let importUsed = false;
1822

1923
// options
2024

@@ -33,93 +37,105 @@ export default ({ types }) => {
3337
const getSourceFile = () => fileName && fileName !== 'unknown' ? ` "${fileName}" file` : '';
3438
const getSource = ({ loc: { start: { line, column } } }) => `at${getSourceFile()} ${line} line ${column} column`;
3539

36-
const warnClass = ({ identifier }, superClassName, superClassObject) => warn(optionLogIgnoredClassComponentExtends,
37-
`Ignored propTypes ${getSource(identifier)} for class "${identifier.name}" with "${superClassObject ? `${superClassObject}.` : ''}${superClassName}" super class`);
40+
const warnClass = ({ identifier }, superClassName) => warn(optionLogIgnoredClassComponentExtends,
41+
`Ignored propTypes ${getSource(identifier)} for class "${identifier.name}" with "${superClassName}" super class`);
3842

3943
const warnBinding = (bindingType, { identifier }, type) => warn(optionLogIgnoredBinding,
4044
`Ignored propTypes ${getSource(identifier)} for ${bindingType} "${identifier.name}" with "${type}" type`);
4145

42-
// updaters
46+
// getters
4347

44-
const updateImport = (binding) => {
45-
if (binding.path.scope.hasBinding(importIdentifier)) return;
48+
const getImportIdentifier = (path) => {
49+
const importDeclaration = path.node.body.find(item => item.type === 'ImportDeclaration'
50+
&& item.source.value === importSourceName
51+
&& item.specifiers.some(specifier => specifier.type === 'ImportDefaultSpecifier'));
4652

47-
if (programBody.some(item => item.type === 'ImportDeclaration'
48-
&& item.specifiers.find(specifier => specifier.local.name === importIdentifier))) return;
53+
if (importDeclaration) importExists = true;
4954

50-
const importDeclaration = types.importDeclaration(
51-
[types.importDefaultSpecifier(types.identifier(importIdentifier))],
52-
types.stringLiteral('prop-types/checkPropTypes'),
53-
);
54-
programBody.unshift(importDeclaration);
55+
importIdentifierNode = importDeclaration
56+
? importDeclaration.specifiers[0].local
57+
: path.scope.generateUidIdentifier(importIdentifierName);
5558
};
5659

57-
const getMethodArgument = (methodNode) => { // eslint-disable-line unicorn/consistent-function-scoping
60+
const getMethodArgumentIdentifier = (methodNode) => {
5861
const [firstArgument] = methodNode.params;
5962

6063
if (firstArgument) {
61-
if (firstArgument.type === 'Identifier') return firstArgument.name;
64+
if (firstArgument.type === 'Identifier') return firstArgument;
65+
6266
if (firstArgument.type === 'AssignmentPattern'
63-
&& firstArgument.left.type === 'Identifier') return firstArgument.left.name;
67+
&& firstArgument.left.type === 'Identifier') return firstArgument.left;
6468
}
6569

66-
return arrowPropertiesIdentifier;
70+
return types.identifier(arrowPropertiesIdentifierName);
6771
};
6872

69-
const updateMethodArgument = (methodNode) => {
70-
const [firstArgument] = methodNode.params;
71-
72-
if (!firstArgument) {
73-
methodNode.params.push(types.identifier(arrowPropertiesIdentifier));
74-
}
75-
76-
else if (firstArgument.type === 'ObjectPattern') {
77-
methodNode.body.body.unshift(types.variableDeclaration('const', [
78-
types.variableDeclarator(
79-
firstArgument,
80-
types.identifier(arrowPropertiesIdentifier),
81-
),
82-
]));
73+
// updaters
8374

84-
methodNode.params[0] = types.identifier(arrowPropertiesIdentifier);
85-
}
75+
const updateImports = (path) => {
76+
if (importExists || !importUsed) return;
8677

87-
else if (firstArgument.type === 'AssignmentPattern' && firstArgument.left.type === 'ObjectPattern') {
88-
methodNode.body.body.unshift(types.variableDeclaration('const', [
89-
types.variableDeclarator(
90-
firstArgument.left,
91-
types.identifier(arrowPropertiesIdentifier),
92-
),
93-
]));
78+
const importDeclaration = types.importDeclaration(
79+
[types.importDefaultSpecifier(importIdentifierNode)],
80+
types.stringLiteral(importSourceName),
81+
);
82+
path.node.body.unshift(importDeclaration);
83+
};
9484

95-
firstArgument.left = types.identifier(arrowPropertiesIdentifier);
96-
}
85+
const updateMethodBodyWithVariableDeclaration = (methodNode, identifier) => {
86+
methodNode.body.body.unshift(types.variableDeclaration('const', [
87+
types.variableDeclarator(
88+
identifier,
89+
types.identifier(arrowPropertiesIdentifierName),
90+
),
91+
]));
9792
};
9893

99-
const updateMethodBody = (binding, methodNode, statements) => {
94+
const updateMethodBodyWithValidationExpression = (binding, methodNode, statements) => {
10095
const [firstNode] = methodNode.body.body;
10196
if (firstNode && firstNode.type === 'ExpressionStatement'
102-
&& firstNode.expression.callee.name === importIdentifier) return;
97+
&& firstNode.expression.callee.name === importIdentifierNode.name) return;
10398

10499
const expression = types.expressionStatement(types.callExpression(
105-
types.identifier(importIdentifier), statements,
100+
importIdentifierNode, [
101+
...statements,
102+
types.stringLiteral('prop'),
103+
types.logicalExpression('||',
104+
types.identifier(`${binding.identifier.name}.displayName`),
105+
types.stringLiteral(binding.identifier.name),
106+
),
107+
],
106108
));
107109
methodNode.body.body.unshift(expression);
108110

109-
updateImport(binding);
111+
importUsed = true;
112+
};
113+
114+
const updateMethodArgument = (methodNode) => {
115+
const [firstArgument] = methodNode.params;
116+
117+
const arrowPropertiesIdentifier = types.identifier(arrowPropertiesIdentifierName);
118+
if (!firstArgument) {
119+
methodNode.params.push(arrowPropertiesIdentifier);
120+
}
121+
122+
else if (firstArgument.type === 'ObjectPattern') {
123+
updateMethodBodyWithVariableDeclaration(methodNode, firstArgument);
124+
methodNode.params[0] = arrowPropertiesIdentifier;
125+
}
126+
127+
else if (firstArgument.type === 'AssignmentPattern' && firstArgument.left.type === 'ObjectPattern') {
128+
updateMethodBodyWithVariableDeclaration(methodNode, firstArgument.left);
129+
firstArgument.left = arrowPropertiesIdentifier;
130+
}
110131
};
111132

112133
// visitors
113134

114135
const visitFunctionDeclaration = (binding, functionNode) => {
115-
updateMethodBody(binding, functionNode, [
136+
updateMethodBodyWithValidationExpression(binding, functionNode, [
116137
types.identifier(`${binding.identifier.name}.propTypes`),
117138
types.identifier('arguments[0]'),
118-
types.stringLiteral('prop'),
119-
types.logicalExpression('||',
120-
types.identifier(`${binding.identifier.name}.displayName`),
121-
types.stringLiteral(binding.identifier.name),
122-
),
123139
]);
124140
};
125141

@@ -129,97 +145,65 @@ export default ({ types }) => {
129145

130146
if (currentSuperClass.type === 'AssignmentExpression') currentSuperClass = currentSuperClass.right;
131147

132-
if (currentSuperClass.name) {
133-
if (!optionClassComponentExtends.includes(currentSuperClass.name)) {
134-
warnClass(binding, currentSuperClass.name);
148+
const { name, object, property } = currentSuperClass;
149+
if (name) {
150+
if (!optionClassComponentExtends.includes(name)) {
151+
warnClass(binding, name);
135152
return;
136153
}
137154
}
138155

139-
else {
140-
const { object, property } = currentSuperClass;
141-
142-
if (object.name === 'React') {
143-
if (!reactClassComponentExtends.includes(property.name)) {
144-
warnClass(binding, property.name, object.name);
145-
return;
146-
}
147-
}
148-
149-
else if (optionClassComponentExtendsObject.includes(object.name)) {
150-
if (!optionClassComponentExtends.includes(property.name)) {
151-
warnClass(binding, property.name, object.name);
152-
return;
153-
}
154-
}
155-
156-
else {
157-
warnClass(binding, property.name, object.name);
158-
return;
159-
}
156+
else if (object.name === 'React'
157+
? !reactClassComponentExtends.includes(property.name)
158+
: (optionClassComponentExtendsObject.includes(object.name)
159+
? !optionClassComponentExtends.includes(property.name)
160+
: true
161+
)) {
162+
warnClass(binding, `${property.name}.${object.name}`);
163+
return;
160164
}
161165

162-
// ignore class if render is not regular method
163166
const renderNode = classNode.body.body.find(item => item.kind === 'method'
164167
&& item.key.name === 'render' && !item.static);
165168
if (!renderNode) return;
166169

167-
// update class render method with validation call
168-
updateMethodBody(binding, renderNode, [
169-
// always use final propTypes even for super class render
170+
updateMethodBodyWithValidationExpression(binding, renderNode, [
170171
types.identifier('this.constructor.propTypes'),
171172
types.identifier('this.props'),
172-
types.stringLiteral('prop'),
173-
types.logicalExpression('||',
174-
types.identifier(`${binding.identifier.name}.displayName`),
175-
types.stringLiteral(binding.identifier.name),
176-
),
177173
]);
178174
};
179175

180176
const visitVariableDeclaration = (binding) => {
181177
const declarationNode = binding.path.node.init;
182178

183-
// follow function visitor logic for function assignment
184179
if (declarationNode.type === 'FunctionExpression') {
185180
visitFunctionDeclaration(binding, declarationNode);
186181
return;
187182
}
188183

189-
// follow class visitor logic for function assignment
190184
if (declarationNode.type === 'ClassExpression') {
191185
visitClassDeclaration(binding, declarationNode);
192186
return;
193187
}
194188

195-
// ignore non arrow function assignment
196189
if (declarationNode.type !== 'ArrowFunctionExpression') {
197190
warnBinding('assignment', binding, declarationNode.type);
198191
return;
199192
}
200193

201-
// update parenthesis arrow function with block statement
202194
if (declarationNode.body.type !== 'BlockStatement') {
203195
declarationNode.body = types.blockStatement([
204196
types.returnStatement(declarationNode.body),
205197
]);
206198
}
207199

208-
// get arrow function argument name
209-
const methodArgument = getMethodArgument(declarationNode);
200+
const methodArgumentIdentifier = getMethodArgumentIdentifier(declarationNode);
210201

211-
// update arrow function with validation call
212-
updateMethodBody(binding, declarationNode, [
202+
updateMethodBodyWithValidationExpression(binding, declarationNode, [
213203
types.identifier(`${binding.identifier.name}.propTypes`),
214-
types.identifier(methodArgument),
215-
types.stringLiteral('prop'),
216-
types.logicalExpression('||',
217-
types.identifier(`${binding.identifier.name}.displayName`),
218-
types.stringLiteral(binding.identifier.name),
219-
),
204+
methodArgumentIdentifier,
220205
]);
221206

222-
// update arrow function argument if needed
223207
updateMethodArgument(declarationNode);
224208
};
225209

@@ -250,60 +234,61 @@ export default ({ types }) => {
250234

251235
const visitAssignmentExpression = (path) => {
252236
const { left } = path.node;
253-
254-
// ignore propTypes assignment without property name
255237
if (!left.property || left.property.name !== 'propTypes') return;
256238

257-
// find propTypes binding
258239
const binding = path.scope.getBinding(left.object.name);
259240
if (!binding) return;
260241

261242
visitBinding(binding);
262243
};
263244

264-
// plugin api
245+
// options
265246

266-
return {
267-
name: pluginName,
247+
const parseOptions = ({
248+
classComponentExtendsObject,
249+
classComponentExtends,
268250

269-
visitor: {
251+
logIgnoredBinding,
252+
logIgnoredClassComponentExtends,
270253

271-
AssignmentExpression: visitAssignmentExpression,
254+
...unknownOptions
255+
}) => {
256+
if (classComponentExtendsObject !== undefined) {
257+
if (Array.isArray(classComponentExtendsObject)) optionClassComponentExtendsObject.push(...classComponentExtendsObject);
258+
else warnOptions({ classComponentExtendsObject });
259+
}
272260

273-
Program: {
274-
enter(path, { file, opts: {
275-
classComponentExtendsObject,
276-
classComponentExtends,
261+
if (classComponentExtends !== undefined) {
262+
if (Array.isArray(classComponentExtends)) optionClassComponentExtends.push(...classComponentExtends);
263+
else warnOptions({ classComponentExtends });
264+
}
277265

278-
logIgnoredBinding,
279-
logIgnoredClassComponentExtends,
266+
if (logIgnoredBinding !== undefined) optionLogIgnoredBinding = Boolean(logIgnoredBinding);
267+
if (logIgnoredClassComponentExtends !== undefined) optionLogIgnoredClassComponentExtends = Boolean(logIgnoredClassComponentExtends);
280268

281-
...unknownOptions
282-
} }) {
283-
fileName = file.opts.filename.slice(file.opts.cwd.length + 1);
284-
programBody = path.node.body;
269+
if (Object.keys(unknownOptions).length > 0) {
270+
warnOptions(unknownOptions);
271+
}
272+
};
285273

286-
// options parsing
274+
// plugin api
287275

288-
if (classComponentExtendsObject !== undefined) {
289-
if (Array.isArray(classComponentExtendsObject)) optionClassComponentExtendsObject.push(...classComponentExtendsObject);
290-
else warnOptions({ classComponentExtendsObject });
291-
}
276+
return {
277+
name: pluginName,
292278

293-
if (classComponentExtends !== undefined) {
294-
if (Array.isArray(classComponentExtends)) optionClassComponentExtends.push(...classComponentExtends);
295-
else warnOptions({ classComponentExtends });
296-
}
279+
visitor: {
280+
AssignmentExpression: visitAssignmentExpression,
297281

298-
if (logIgnoredBinding !== undefined) optionLogIgnoredBinding = Boolean(logIgnoredBinding);
299-
if (logIgnoredClassComponentExtends !== undefined) optionLogIgnoredClassComponentExtends = Boolean(logIgnoredClassComponentExtends);
282+
Program: {
283+
enter(path, { file, opts }) {
284+
fileName = file.opts.filename.slice(file.opts.cwd.length + 1);
300285

301-
if (Object.keys(unknownOptions).length > 0) {
302-
warnOptions(unknownOptions);
303-
}
286+
getImportIdentifier(path);
287+
parseOptions(opts);
304288
},
305-
},
306289

290+
exit: updateImports,
291+
},
307292
},
308293
};
309294
};

Diff for: test.js

+2-1
Original file line numberDiff line numberDiff line change
@@ -255,9 +255,10 @@ describe('import', () => {
255255
function MyComponent() {}
256256
${TEMPLATE_TYPES}
257257
`, `
258+
${TEMPLATE_IMPORT.replace('_checkPropTypes', '_checkPropTypes2')}
258259
const ${TEMPLATE_IMPORT_NAME} = () => {};
259260
function MyComponent() {
260-
${TEMPLATE_CHECK_FUNCTION}
261+
${TEMPLATE_CHECK_FUNCTION.replace('_checkPropTypes', '_checkPropTypes2')}
261262
}
262263
${TEMPLATE_TYPES}
263264
`));

0 commit comments

Comments
 (0)