|
| 1 | +import { GetConfig } from '../types' |
| 2 | + |
| 3 | +const reactTypesPath = 'node_modules/@types/react/index.d.ts' |
| 4 | + |
| 5 | +const symbolCache = new Map<string, boolean>() |
| 6 | + |
| 7 | +export default (entries: ts.CompletionEntry[], node: ts.Node, position: number, languageService: ts.LanguageService, c: GetConfig) => { |
| 8 | + if (node.getSourceFile().languageVariant !== ts.LanguageVariant.JSX) return |
| 9 | + if (ts.isIdentifier(node)) node = node.parent |
| 10 | + if ( |
| 11 | + ![ts.SyntaxKind.JsxOpeningFragment, ts.SyntaxKind.JsxOpeningElement, ts.SyntaxKind.JsxSelfClosingElement].includes(node.kind) && |
| 12 | + !isJsxOpeningElem(position, node) |
| 13 | + ) { |
| 14 | + return |
| 15 | + } |
| 16 | + |
| 17 | + const nodeText = node.getText().slice(0, position - (node.pos + node.getLeadingTriviaWidth())) |
| 18 | + // workaround for <div test |></div> |
| 19 | + if (nodeText.includes(' ')) return |
| 20 | + if (c('jsxImproveElementsSuggestions.enabled')) { |
| 21 | + let lastPart = nodeText.split('.').at(-1)! |
| 22 | + if (lastPart.startsWith('<')) lastPart = lastPart.slice(1) |
| 23 | + const isStartingWithUpperCase = (str: string) => str[0] && str[0] === str[0].toUpperCase() |
| 24 | + // check if starts with lowercase |
| 25 | + if (isStartingWithUpperCase(lastPart)) { |
| 26 | + entries = entries.filter(entry => isStartingWithUpperCase(entry.name) && ![ts.ScriptElementKind.enumElement].includes(entry.kind)) |
| 27 | + } |
| 28 | + } |
| 29 | + |
| 30 | + const fileName = node.getSourceFile().fileName |
| 31 | + const interestedKinds: ts.ScriptElementKind[] = [ |
| 32 | + ts.ScriptElementKind.variableElement, |
| 33 | + ts.ScriptElementKind.functionElement, |
| 34 | + ts.ScriptElementKind.constElement, |
| 35 | + ts.ScriptElementKind.letElement, |
| 36 | + ts.ScriptElementKind.alias, |
| 37 | + ts.ScriptElementKind.parameterElement, |
| 38 | + ts.ScriptElementKind.memberVariableElement, |
| 39 | + ts.ScriptElementKind.memberFunctionElement, |
| 40 | + ] |
| 41 | + const timings = {} |
| 42 | + const typeAtLocLog = {} |
| 43 | + const program = languageService.getProgram()! |
| 44 | + const typeChecker = program.getTypeChecker()! |
| 45 | + const nowGetter = tsFull.tryGetNativePerformanceHooks()!.performance |
| 46 | + let mark = nowGetter.now() |
| 47 | + const startMark = () => { |
| 48 | + mark = nowGetter.now() |
| 49 | + } |
| 50 | + const addMark = (name: string) => { |
| 51 | + timings[name] ??= 0 |
| 52 | + timings[name] += nowGetter.now() - mark |
| 53 | + timings[name + 'Count'] ??= 0 |
| 54 | + timings[name + 'Count']++ |
| 55 | + } |
| 56 | + const getIsJsxComponentSignature = (signature: ts.Signature) => { |
| 57 | + let returnType: ts.Type | undefined = signature.getReturnType() |
| 58 | + if (!returnType) return |
| 59 | + // todo setting to allow any! |
| 60 | + if (returnType.flags & ts.TypeFlags.Any) return false |
| 61 | + returnType = getPossiblyJsxType(returnType) |
| 62 | + if (!returnType) return false |
| 63 | + startMark() |
| 64 | + // todo(perf) this seems to be taking a lot of time (mui test 180ms) |
| 65 | + const typeString = typeChecker.typeToString(returnType) |
| 66 | + addMark('stringType') |
| 67 | + // todo-low resolve indentifier instead |
| 68 | + // or compare node name from decl (invest perf) |
| 69 | + if (['Element', 'ReactElement'].every(s => !typeString.startsWith(s))) return |
| 70 | + const declFile = returnType.getSymbol()?.declarations?.[0]?.getSourceFile().fileName |
| 71 | + if (!declFile?.endsWith(reactTypesPath)) return |
| 72 | + return true |
| 73 | + } |
| 74 | + const getIsEntryReactComponent = (entry: ts.CompletionEntry) => { |
| 75 | + // todo add more checks from ref https://github.com/microsoft/TypeScript/blob/e4816ed44cf9bcfe7cebb997b1f44cdb5564dac4/src/compiler/checker.ts#L30030 |
| 76 | + // todo support classes |
| 77 | + const symbol = entry['symbol'] as ts.Symbol |
| 78 | + // tsFull.isCheckJsEnabledForFile(sourceFile, compilerOptions) |
| 79 | + // symbol.declarations |
| 80 | + if (!symbol) return true |
| 81 | + // performance: symbol coming from lib cannot be JSX element, so let's skip checking them |
| 82 | + // todo other decl |
| 83 | + const firstDeclaration = symbol.declarations?.[0] |
| 84 | + if (!firstDeclaration) return |
| 85 | + // todo-low |
| 86 | + const isIntrisicElem = ts.isInterfaceDeclaration(firstDeclaration.parent) && firstDeclaration.parent.name.text === 'IntrinsicElements' |
| 87 | + if (isIntrisicElem) return true |
| 88 | + // todo check |
| 89 | + // todo allow property access |
| 90 | + // only intrinsic elements can have lowercase starting and getting type of local variables might be really slow for some reason |
| 91 | + if (entry.name[0]?.toLowerCase() === entry.name[0]) return false |
| 92 | + const firstDeclarationFileName = firstDeclaration.getSourceFile().fileName |
| 93 | + if (firstDeclarationFileName.includes('/node_modules/typescript/lib/lib')) return false |
| 94 | + let shouldBeCached = firstDeclarationFileName.includes('node_modules') |
| 95 | + if (!shouldBeCached && firstDeclaration.getSourceFile().fileName === fileName) { |
| 96 | + // startMark() |
| 97 | + const definitionAtPosition = languageService.getDefinitionAtPosition(fileName, firstDeclaration.pos + 1)?.[0] |
| 98 | + // addMark('getDefinitionAtPosition') |
| 99 | + if (!definitionAtPosition) return |
| 100 | + shouldBeCached = definitionAtPosition.fileName.includes('node_modules') |
| 101 | + } |
| 102 | + const symbolSerialized = `${firstDeclarationFileName}#${symbol.name}` |
| 103 | + if (shouldBeCached && symbolCache.has(symbolSerialized)) { |
| 104 | + // that caching, not ideal |
| 105 | + return symbolCache.get(symbolSerialized) |
| 106 | + } |
| 107 | + startMark() |
| 108 | + const entryType = typeChecker.getTypeOfSymbolAtLocation(symbol, node) |
| 109 | + typeAtLocLog[entry.name] = nowGetter.now() - mark |
| 110 | + addMark('getTypeAtLocation') |
| 111 | + // todo setting to allow any! |
| 112 | + if (entryType.flags & ts.TypeFlags.Any) return false |
| 113 | + // startMark() |
| 114 | + const signatures = typeChecker.getSignaturesOfType(entryType, ts.SignatureKind.Call) |
| 115 | + // addMark('signatures') |
| 116 | + const result = signatures.length > 0 && signatures.every(signature => getIsJsxComponentSignature(signature)) |
| 117 | + if (shouldBeCached) symbolCache.set(symbolSerialized, result) |
| 118 | + return result |
| 119 | + } |
| 120 | + |
| 121 | + // todo inspect div suggestion |
| 122 | + console.time('filterJsxComponents') |
| 123 | + const newEntries = entries.filter(entry => { |
| 124 | + // if (!entry.name[0] || entry.name[0].toLowerCase() === entry.name[0]) return false |
| 125 | + if (entry.kind === ts.ScriptElementKind.keyword) return false |
| 126 | + // todo |
| 127 | + if (c('jsxImproveElementsSuggestions.filterNamespaces') && entry.kind === ts.ScriptElementKind.moduleElement) return false |
| 128 | + if (!c('experiments.excludeNonJsxCompletions')) return true |
| 129 | + // todo!!! |
| 130 | + if (entry.kind === ts.ScriptElementKind.classElement) return false |
| 131 | + if (entry.kind === ts.ScriptElementKind.localClassElement) return false |
| 132 | + if (!interestedKinds.includes(entry.kind)) return true |
| 133 | + const isEntryReactComponent = getIsEntryReactComponent(entry) |
| 134 | + return isEntryReactComponent |
| 135 | + }) |
| 136 | + console.timeEnd('filterJsxComponents') |
| 137 | + console.log('filterJsxComponentsTimings', JSON.stringify(Object.fromEntries(Object.entries(timings).map(([k, v]) => [k, Math.round(v)])))) |
| 138 | + return newEntries |
| 139 | +} |
| 140 | + |
| 141 | +const isJsxOpeningElem = (position: number, node: ts.Node) => { |
| 142 | + // handle cases like: |
| 143 | + // const a = () => <| |
| 144 | + // function a() { return <| } |
| 145 | + const fullText = node |
| 146 | + .getSourceFile() |
| 147 | + .getFullText() |
| 148 | + .slice(0, position) |
| 149 | + // not needed as well |
| 150 | + .replace(/[\w\d]+$/, '') |
| 151 | + if (!fullText.endsWith('<')) return |
| 152 | + // todo remove this |
| 153 | + while (ts.isParenthesizedExpression(node)) { |
| 154 | + node = node.parent |
| 155 | + } |
| 156 | + if (node.kind === ts.SyntaxKind.FirstBinaryOperator && node.parent.getText() === '<') { |
| 157 | + return true |
| 158 | + // const parent = node.parent.parent; |
| 159 | + // if (!parent) return false |
| 160 | + // if (parent.kind === ts.SyntaxKind.ReturnStatement) return true |
| 161 | + // if (ts.is(parent)) |
| 162 | + } |
| 163 | + return false |
| 164 | +} |
| 165 | + |
| 166 | +const getReactElementType = (program: ts.Program) => { |
| 167 | + const reactDeclSource = program.getSourceFiles().find(name => name.fileName.endsWith(reactTypesPath)) |
| 168 | + const namespace = reactDeclSource && ts.forEachChild(reactDeclSource, s => ts.isModuleDeclaration(s) && s.name.text === 'React' && s) |
| 169 | + if (!namespace || !namespace.body) return |
| 170 | + return ts.forEachChild(namespace.body, node => { |
| 171 | + if (ts.isInterfaceDeclaration(node) && node.name.text === 'ReactElement') { |
| 172 | + return node |
| 173 | + } |
| 174 | + return |
| 175 | + }) |
| 176 | +} |
| 177 | + |
| 178 | +const getPossiblyJsxType = (type: ts.Type) => { |
| 179 | + if (type.isUnion()) { |
| 180 | + for (const t of type.types) { |
| 181 | + if (t.flags & ts.TypeFlags.Null) continue |
| 182 | + if (t.flags & ts.TypeFlags.Object) { |
| 183 | + type = t |
| 184 | + break |
| 185 | + } else { |
| 186 | + return |
| 187 | + } |
| 188 | + } |
| 189 | + } |
| 190 | + return type.flags & ts.TypeFlags.Object ? type : undefined |
| 191 | +} |
0 commit comments