|
| 1 | +import { DefinitionType } from '@typescript-eslint/scope-manager'; |
| 2 | +import { |
| 3 | + AST_NODE_TYPES, |
| 4 | + ASTUtils, |
| 5 | + TSESLint, |
| 6 | + TSESTree, |
| 7 | +} from '@typescript-eslint/utils'; |
| 8 | + |
| 9 | +import { |
| 10 | + isImportDefaultSpecifier, |
| 11 | + isImportExpression, |
| 12 | + isProperty, |
| 13 | + isImportSpecifier, |
| 14 | + isTSImportEqualsDeclaration, |
| 15 | + isCallExpression, |
| 16 | +} from '../node-utils'; |
| 17 | +import { |
| 18 | + AccessorNode, |
| 19 | + getAccessorValue, |
| 20 | + getStringValue, |
| 21 | + isIdentifier, |
| 22 | + isStringNode, |
| 23 | + isSupportedAccessor, |
| 24 | +} from '../node-utils/accessors'; |
| 25 | + |
| 26 | +import { LIBRARY_MODULES } from '.'; |
| 27 | + |
| 28 | +interface ImportDetails { |
| 29 | + source: string; |
| 30 | + local: string; |
| 31 | + imported: string | null; |
| 32 | +} |
| 33 | + |
| 34 | +const describeImportDefAsImport = ( |
| 35 | + def: TSESLint.Scope.Definitions.ImportBindingDefinition |
| 36 | +): ImportDetails | null => { |
| 37 | + if (isTSImportEqualsDeclaration(def.parent)) { |
| 38 | + return null; |
| 39 | + } |
| 40 | + |
| 41 | + if (isImportDefaultSpecifier(def.node)) { |
| 42 | + return { |
| 43 | + source: def.parent.source.value, |
| 44 | + imported: null, |
| 45 | + local: def.node.local.name, |
| 46 | + }; |
| 47 | + } |
| 48 | + |
| 49 | + if (!isImportSpecifier(def.node)) { |
| 50 | + return null; |
| 51 | + } |
| 52 | + |
| 53 | + // we only care about value imports |
| 54 | + if (def.parent.importKind === 'type') { |
| 55 | + return null; |
| 56 | + } |
| 57 | + |
| 58 | + return { |
| 59 | + source: def.parent.source.value, |
| 60 | + imported: |
| 61 | + 'name' in def.node.imported |
| 62 | + ? def.node.imported.name |
| 63 | + : def.node.imported.value, |
| 64 | + local: def.node.local.name, |
| 65 | + }; |
| 66 | +}; |
| 67 | + |
| 68 | +const describeVariableDefAsImport = ( |
| 69 | + def: TSESLint.Scope.Definitions.VariableDefinition |
| 70 | +): ImportDetails | null => { |
| 71 | + if (!def.node.init) return null; |
| 72 | + |
| 73 | + const sourceNode = |
| 74 | + isCallExpression(def.node.init) && |
| 75 | + isIdentifier(def.node.init.callee, 'require') |
| 76 | + ? def.node.init.arguments[0] |
| 77 | + : ASTUtils.isAwaitExpression(def.node.init) && |
| 78 | + isImportExpression(def.node.init.argument) |
| 79 | + ? def.node.init.argument.source |
| 80 | + : null; |
| 81 | + |
| 82 | + if (!sourceNode || !isStringNode(sourceNode)) return null; |
| 83 | + if (!isProperty(def.name.parent)) return null; |
| 84 | + if (!isSupportedAccessor(def.name.parent.key)) return null; |
| 85 | + |
| 86 | + return { |
| 87 | + source: getStringValue(sourceNode), |
| 88 | + imported: getAccessorValue(def.name.parent.key), |
| 89 | + local: def.name.name, |
| 90 | + }; |
| 91 | +}; |
| 92 | + |
| 93 | +const describePossibleImportDef = ( |
| 94 | + def: TSESLint.Scope.Definition |
| 95 | +): ImportDetails | null => { |
| 96 | + if (def.type === DefinitionType.Variable) { |
| 97 | + return describeVariableDefAsImport(def); |
| 98 | + } |
| 99 | + if (def.type === DefinitionType.ImportBinding) { |
| 100 | + return describeImportDefAsImport(def); |
| 101 | + } |
| 102 | + return null; |
| 103 | +}; |
| 104 | + |
| 105 | +const resolveScope = ( |
| 106 | + scope: TSESLint.Scope.Scope, |
| 107 | + identifier: string |
| 108 | +): ImportDetails | 'local' | null => { |
| 109 | + let currentScope: TSESLint.Scope.Scope | null = scope; |
| 110 | + while (currentScope !== null) { |
| 111 | + const ref = currentScope.set.get(identifier); |
| 112 | + if (ref && ref.defs.length > 0) { |
| 113 | + const def = ref.defs[ref.defs.length - 1]; |
| 114 | + const importDetails = describePossibleImportDef(def); |
| 115 | + |
| 116 | + if (importDetails?.local === identifier) { |
| 117 | + return importDetails; |
| 118 | + } |
| 119 | + |
| 120 | + return 'local'; |
| 121 | + } |
| 122 | + |
| 123 | + currentScope = currentScope.upper; |
| 124 | + } |
| 125 | + |
| 126 | + return null; |
| 127 | +}; |
| 128 | + |
| 129 | +const joinChains = ( |
| 130 | + a: AccessorNode[] | null, |
| 131 | + b: AccessorNode[] | null |
| 132 | +): AccessorNode[] | null => (a && b ? [...a, ...b] : null); |
| 133 | + |
| 134 | +export const getNodeChain = (node: TSESTree.Node): AccessorNode[] | null => { |
| 135 | + if (isSupportedAccessor(node)) { |
| 136 | + return [node]; |
| 137 | + } |
| 138 | + |
| 139 | + switch (node.type) { |
| 140 | + case AST_NODE_TYPES.TaggedTemplateExpression: |
| 141 | + return getNodeChain(node.tag); |
| 142 | + case AST_NODE_TYPES.MemberExpression: |
| 143 | + return joinChains(getNodeChain(node.object), getNodeChain(node.property)); |
| 144 | + case AST_NODE_TYPES.CallExpression: |
| 145 | + return getNodeChain(node.callee); |
| 146 | + } |
| 147 | + |
| 148 | + return null; |
| 149 | +}; |
| 150 | + |
| 151 | +interface ResolvedTestingLibraryUserEventFn { |
| 152 | + original: string | null; |
| 153 | + local: string; |
| 154 | +} |
| 155 | + |
| 156 | +const USER_EVENT_PACKAGE = '@testing-library/user-event'; |
| 157 | + |
| 158 | +export const resolveToTestingLibraryFn = ( |
| 159 | + node: TSESTree.CallExpression, |
| 160 | + context: TSESLint.RuleContext<string, unknown[]> |
| 161 | +): ResolvedTestingLibraryUserEventFn | null => { |
| 162 | + const chain = getNodeChain(node); |
| 163 | + if (!chain?.length) return null; |
| 164 | + |
| 165 | + const identifier = chain[0]; |
| 166 | + const scope = context.sourceCode.getScope(identifier); |
| 167 | + const maybeImport = resolveScope(scope, getAccessorValue(identifier)); |
| 168 | + |
| 169 | + if (maybeImport === 'local' || maybeImport === null) { |
| 170 | + return null; |
| 171 | + } |
| 172 | + |
| 173 | + if ( |
| 174 | + [...LIBRARY_MODULES, USER_EVENT_PACKAGE].some( |
| 175 | + (module) => module === maybeImport.source |
| 176 | + ) |
| 177 | + ) { |
| 178 | + return { |
| 179 | + original: maybeImport.imported, |
| 180 | + local: maybeImport.local, |
| 181 | + }; |
| 182 | + } |
| 183 | + |
| 184 | + return null; |
| 185 | +}; |
0 commit comments