Skip to content

Commit eaca948

Browse files
authored
Merge pull request #309 from testing-library/306_null-identifiers
fix: guard against null identifier nodes Closes #305 Closes #306
2 parents 7e9d5c5 + cc040a5 commit eaca948

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

45 files changed

+2289
-1761
lines changed

jest.config.js

-6
Original file line numberDiff line numberDiff line change
@@ -6,12 +6,6 @@ module.exports = {
66
},
77
coverageThreshold: {
88
global: {
9-
branches: 100,
10-
functions: 100,
11-
lines: 100,
12-
statements: 100,
13-
},
14-
'./lib/node-utils.ts': {
159
branches: 90,
1610
functions: 90,
1711
lines: 90,

lib/detect-testing-library-utils.ts

+39-13
Original file line numberDiff line numberDiff line change
@@ -135,7 +135,8 @@ export function detectTestingLibraryUtils<
135135

136136
// Init options based on shared ESLint settings
137137
const customModule = context.settings['testing-library/utils-module'];
138-
const customRenders = context.settings['testing-library/custom-renders'];
138+
const customRenders =
139+
context.settings['testing-library/custom-renders'] ?? [];
139140

140141
/**
141142
* Small method to extract common checks to determine whether a node is
@@ -160,6 +161,11 @@ export function detectTestingLibraryUtils<
160161

161162
const referenceNode = getReferenceNode(node);
162163
const referenceNodeIdentifier = getPropertyIdentifierNode(referenceNode);
164+
165+
if (!referenceNodeIdentifier) {
166+
return false;
167+
}
168+
163169
const importedUtilSpecifier = getImportedUtilSpecifier(
164170
referenceNodeIdentifier
165171
);
@@ -309,7 +315,8 @@ export function detectTestingLibraryUtils<
309315
(identifierNodeName, originalNodeName) => {
310316
return (
311317
(validNames as string[]).includes(identifierNodeName) ||
312-
(validNames as string[]).includes(originalNodeName)
318+
(!!originalNodeName &&
319+
(validNames as string[]).includes(originalNodeName))
313320
);
314321
}
315322
);
@@ -370,9 +377,10 @@ export function detectTestingLibraryUtils<
370377
return false;
371378
}
372379

373-
const parentMemberExpression:
374-
| TSESTree.MemberExpression
375-
| undefined = isMemberExpression(node.parent) ? node.parent : undefined;
380+
const parentMemberExpression: TSESTree.MemberExpression | undefined =
381+
node.parent && isMemberExpression(node.parent)
382+
? node.parent
383+
: undefined;
376384

377385
if (!parentMemberExpression) {
378386
return false;
@@ -417,9 +425,10 @@ export function detectTestingLibraryUtils<
417425
return false;
418426
}
419427

420-
const parentMemberExpression:
421-
| TSESTree.MemberExpression
422-
| undefined = isMemberExpression(node.parent) ? node.parent : undefined;
428+
const parentMemberExpression: TSESTree.MemberExpression | undefined =
429+
node.parent && isMemberExpression(node.parent)
430+
? node.parent
431+
: undefined;
423432

424433
if (!parentMemberExpression) {
425434
return false;
@@ -483,8 +492,15 @@ export function detectTestingLibraryUtils<
483492
};
484493

485494
const isRenderVariableDeclarator: IsRenderVariableDeclaratorFn = (node) => {
495+
if (!node.init) {
496+
return false;
497+
}
486498
const initIdentifierNode = getDeepestIdentifierNode(node.init);
487499

500+
if (!initIdentifierNode) {
501+
return false;
502+
}
503+
488504
return isRenderUtil(initIdentifierNode);
489505
};
490506

@@ -547,7 +563,7 @@ export function detectTestingLibraryUtils<
547563
const node = getCustomModuleImportNode() ?? getTestingLibraryImportNode();
548564

549565
if (!node) {
550-
return null;
566+
return undefined;
551567
}
552568

553569
if (isImportDeclaration(node)) {
@@ -613,7 +629,11 @@ export function detectTestingLibraryUtils<
613629
node: TSESTree.MemberExpression | TSESTree.Identifier
614630
): TSESTree.ImportClause | TSESTree.Identifier | undefined => {
615631
const identifierName: string | undefined = getPropertyIdentifierNode(node)
616-
.name;
632+
?.name;
633+
634+
if (!identifierName) {
635+
return undefined;
636+
}
617637

618638
return findImportedUtilSpecifier(identifierName);
619639
};
@@ -641,7 +661,11 @@ export function detectTestingLibraryUtils<
641661
}
642662

643663
const identifierName: string | undefined = getPropertyIdentifierNode(node)
644-
.name;
664+
?.name;
665+
666+
if (!identifierName) {
667+
return false;
668+
}
645669

646670
return hasImportMatch(importNode, identifierName);
647671
};
@@ -696,6 +720,7 @@ export function detectTestingLibraryUtils<
696720
// check only if custom module import not found yet so we avoid
697721
// to override importedCustomModuleNode after it's found
698722
if (
723+
customModule &&
699724
!importedCustomModuleNode &&
700725
String(node.source.value).endsWith(customModule)
701726
) {
@@ -735,6 +760,7 @@ export function detectTestingLibraryUtils<
735760
!importedCustomModuleNode &&
736761
args.some(
737762
(arg) =>
763+
customModule &&
738764
isLiteral(arg) &&
739765
typeof arg.value === 'string' &&
740766
arg.value.endsWith(customModule)
@@ -770,11 +796,11 @@ export function detectTestingLibraryUtils<
770796
allKeys.forEach((instruction) => {
771797
enhancedRuleInstructions[instruction] = (node) => {
772798
if (instruction in detectionInstructions) {
773-
detectionInstructions[instruction](node);
799+
detectionInstructions[instruction]?.(node);
774800
}
775801

776802
if (canReportErrors() && ruleInstructions[instruction]) {
777-
return ruleInstructions[instruction](node);
803+
return ruleInstructions[instruction]?.(node);
778804
}
779805
};
780806
});

lib/node-utils.ts

+25-16
Original file line numberDiff line numberDiff line change
@@ -42,13 +42,13 @@ export function isCallExpression(
4242
}
4343

4444
export function isNewExpression(
45-
node: TSESTree.Node
45+
node: TSESTree.Node | null | undefined
4646
): node is TSESTree.NewExpression {
4747
return node?.type === 'NewExpression';
4848
}
4949

5050
export function isMemberExpression(
51-
node: TSESTree.Node
51+
node: TSESTree.Node | null | undefined
5252
): node is TSESTree.MemberExpression {
5353
return node?.type === AST_NODE_TYPES.MemberExpression;
5454
}
@@ -60,31 +60,31 @@ export function isLiteral(
6060
}
6161

6262
export function isImportSpecifier(
63-
node: TSESTree.Node
63+
node: TSESTree.Node | null | undefined
6464
): node is TSESTree.ImportSpecifier {
6565
return node?.type === AST_NODE_TYPES.ImportSpecifier;
6666
}
6767

6868
export function isImportNamespaceSpecifier(
69-
node: TSESTree.Node
69+
node: TSESTree.Node | null | undefined
7070
): node is TSESTree.ImportNamespaceSpecifier {
7171
return node?.type === AST_NODE_TYPES.ImportNamespaceSpecifier;
7272
}
7373

7474
export function isImportDefaultSpecifier(
75-
node: TSESTree.Node
75+
node: TSESTree.Node | null | undefined
7676
): node is TSESTree.ImportDefaultSpecifier {
7777
return node?.type === AST_NODE_TYPES.ImportDefaultSpecifier;
7878
}
7979

8080
export function isBlockStatement(
81-
node: TSESTree.Node
81+
node: TSESTree.Node | null | undefined
8282
): node is TSESTree.BlockStatement {
8383
return node?.type === AST_NODE_TYPES.BlockStatement;
8484
}
8585

8686
export function isObjectPattern(
87-
node: TSESTree.Node
87+
node: TSESTree.Node | null | undefined
8888
): node is TSESTree.ObjectPattern {
8989
return node?.type === AST_NODE_TYPES.ObjectPattern;
9090
}
@@ -96,13 +96,13 @@ export function isProperty(
9696
}
9797

9898
export function isJSXAttribute(
99-
node: TSESTree.Node
99+
node: TSESTree.Node | null | undefined
100100
): node is TSESTree.JSXAttribute {
101101
return node?.type === AST_NODE_TYPES.JSXAttribute;
102102
}
103103

104104
export function isExpressionStatement(
105-
node: TSESTree.Node
105+
node: TSESTree.Node | null | undefined
106106
): node is TSESTree.ExpressionStatement {
107107
return node?.type === AST_NODE_TYPES.ExpressionStatement;
108108
}
@@ -137,7 +137,7 @@ export function findClosestCallExpressionNode(
137137
export function findClosestCallNode(
138138
node: TSESTree.Node,
139139
name: string
140-
): TSESTree.CallExpression {
140+
): TSESTree.CallExpression | null {
141141
if (!node.parent) {
142142
return null;
143143
}
@@ -195,12 +195,12 @@ export function hasChainedThen(node: TSESTree.Node): boolean {
195195
const parent = node.parent;
196196

197197
// wait(...).then(...)
198-
if (isCallExpression(parent)) {
198+
if (isCallExpression(parent) && parent.parent) {
199199
return hasThenProperty(parent.parent);
200200
}
201201

202202
// promise.then(...)
203-
return hasThenProperty(parent);
203+
return !!parent && hasThenProperty(parent);
204204
}
205205

206206
export function isPromiseIdentifier(
@@ -239,6 +239,7 @@ export function isPromisesArrayResolved(node: TSESTree.Node): boolean {
239239
}
240240

241241
return (
242+
!!closestCallExpression.parent &&
242243
isArrayExpression(closestCallExpression.parent) &&
243244
isCallExpression(closestCallExpression.parent.parent) &&
244245
(isPromiseAll(closestCallExpression.parent.parent) ||
@@ -268,6 +269,9 @@ export function isPromiseHandled(nodeIdentifier: TSESTree.Identifier): boolean {
268269
);
269270

270271
for (const node of suspiciousNodes) {
272+
if (!node || !node.parent) {
273+
continue;
274+
}
271275
if (ASTUtils.isAwaitExpression(node.parent)) {
272276
return true;
273277
}
@@ -436,7 +440,10 @@ export function getReferenceNode(
436440
| TSESTree.MemberExpression
437441
| TSESTree.Identifier
438442
): TSESTree.CallExpression | TSESTree.MemberExpression | TSESTree.Identifier {
439-
if (isMemberExpression(node.parent) || isCallExpression(node.parent)) {
443+
if (
444+
node.parent &&
445+
(isMemberExpression(node.parent) || isCallExpression(node.parent))
446+
) {
440447
return getReferenceNode(node.parent);
441448
}
442449

@@ -505,9 +512,10 @@ export function getAssertNodeInfo(
505512
let matcher = ASTUtils.getPropertyName(node);
506513
const isNegated = matcher === 'not';
507514
if (isNegated) {
508-
matcher = isMemberExpression(node.parent)
509-
? ASTUtils.getPropertyName(node.parent)
510-
: null;
515+
matcher =
516+
node.parent && isMemberExpression(node.parent)
517+
? ASTUtils.getPropertyName(node.parent)
518+
: null;
511519
}
512520

513521
if (!matcher) {
@@ -526,6 +534,7 @@ export function hasClosestExpectResolvesRejects(node: TSESTree.Node): boolean {
526534
if (
527535
isCallExpression(node) &&
528536
ASTUtils.isIdentifier(node.callee) &&
537+
node.parent &&
529538
isMemberExpression(node.parent) &&
530539
node.callee.name === 'expect'
531540
) {

lib/rules/await-async-query.ts

+1-2
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,6 @@ export default createTestingLibraryRule<Options, MessageIds>({
2626
asyncQueryWrapper:
2727
'promise returned from {{ name }} wrapper over async query must be handled',
2828
},
29-
fixable: null,
3029
schema: [],
3130
},
3231
defaultOptions: [],
@@ -52,7 +51,7 @@ export default createTestingLibraryRule<Options, MessageIds>({
5251
true
5352
);
5453

55-
if (!closestCallExpressionNode) {
54+
if (!closestCallExpressionNode || !closestCallExpressionNode.parent) {
5655
return;
5756
}
5857

lib/rules/await-async-utils.ts

+1-2
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,6 @@ export default createTestingLibraryRule<Options, MessageIds>({
2626
asyncUtilWrapper:
2727
'Promise returned from {{ name }} wrapper over async util must be handled',
2828
},
29-
fixable: null,
3029
schema: [],
3130
},
3231
defaultOptions: [],
@@ -53,7 +52,7 @@ export default createTestingLibraryRule<Options, MessageIds>({
5352
true
5453
);
5554

56-
if (!closestCallExpression) {
55+
if (!closestCallExpression || !closestCallExpression.parent) {
5756
return;
5857
}
5958

lib/rules/await-fire-event.ts

+1-2
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,6 @@ export default createTestingLibraryRule<Options, MessageIds>({
2727
fireEventWrapper:
2828
'Promise returned from `fireEvent.{{ wrapperName }}` wrapper over fire event method must be handled',
2929
},
30-
fixable: null,
3130
schema: [],
3231
},
3332
defaultOptions: [],
@@ -67,7 +66,7 @@ export default createTestingLibraryRule<Options, MessageIds>({
6766
true
6867
);
6968

70-
if (!closestCallExpression) {
69+
if (!closestCallExpression || !closestCallExpression.parent) {
7170
return;
7271
}
7372

lib/rules/consistent-data-testid.ts

+5-5
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,6 @@ export default ESLintUtils.RuleCreator(getDocsUrl)<Options, MessageIds>({
3030
messages: {
3131
consistentDataTestId: '`{{attr}}` "{{value}}" should match `{{regex}}`',
3232
},
33-
fixable: null,
3433
schema: [
3534
{
3635
type: 'object',
@@ -72,7 +71,7 @@ export default ESLintUtils.RuleCreator(getDocsUrl)<Options, MessageIds>({
7271

7372
function getFileNameData() {
7473
const splitPath = getFilename().split('/');
75-
const fileNameWithExtension = splitPath.pop();
74+
const fileNameWithExtension = splitPath.pop() ?? '';
7675
const parent = splitPath.pop();
7776
const fileName = fileNameWithExtension.split('.').shift();
7877

@@ -85,17 +84,18 @@ export default ESLintUtils.RuleCreator(getDocsUrl)<Options, MessageIds>({
8584
return new RegExp(testIdPattern.replace(FILENAME_PLACEHOLDER, fileName));
8685
}
8786

88-
function isTestIdAttribute(name: string) {
87+
function isTestIdAttribute(name: string): boolean {
8988
if (typeof attr === 'string') {
9089
return attr === name;
9190
} else {
92-
return attr.includes(name);
91+
return attr?.includes(name) ?? false;
9392
}
9493
}
9594

9695
return {
9796
JSXIdentifier: (node) => {
9897
if (
98+
!node.parent ||
9999
!isJSXAttribute(node.parent) ||
100100
!isLiteral(node.parent.value) ||
101101
!isTestIdAttribute(node.name)
@@ -105,7 +105,7 @@ export default ESLintUtils.RuleCreator(getDocsUrl)<Options, MessageIds>({
105105

106106
const value = node.parent.value.value;
107107
const { fileName } = getFileNameData();
108-
const regex = getTestIdValidator(fileName);
108+
const regex = getTestIdValidator(fileName ?? '');
109109

110110
if (value && typeof value === 'string' && !regex.test(value)) {
111111
context.report({

0 commit comments

Comments
 (0)