import type { __String, ArrowFunction, CallExpression, ClassDeclaration, ClassExpression, Declaration, ExpressionWithTypeArguments, FunctionDeclaration, HeritageClause, Identifier, InterfaceDeclaration, Node, TypeAliasDeclaration, TypeChecker, VariableDeclaration, } from 'typescript'; import ts from 'typescript'; import { buildComponentMetaWithHeritage } from '../../cli/commands/analyze/discover.js'; import { HeritageKind, HeritageMeta, MethodMeta, PropMeta, } from '../../plugins/ComponentMeta.js'; import { Plugin } from '../../plugins/Plugin.js'; import { isUndefined } from '../../utils/unit.js'; import { getDocumentation } from './meta-doc.js'; import { getDocTags } from './meta-doc-tag.js'; import { buildMethodMetaFromDeclarationOrSignature } from './meta-method.js'; import { buildPropMetaFromDeclarationOrSignature, getPropTypeInfo, } from './meta-prop.js'; import { buildSourceMetaFromNode } from './meta-source.js'; import { getTypeSourceFile } from './meta-type.js'; export const IGNORE_HERITAGE = new Set(['HTMLElement', 'LitElement']); export async function buildHeritageMetaTree( checker: TypeChecker, plugin: Plugin, heritages: HeritageMeta[], parent?: HeritageMeta, ): Promise { if (isUndefined(plugin.build)) return []; const chain: HeritageMeta[] = []; await Promise.all( heritages .filter(heritage => !IGNORE_HERITAGE.has(heritage.name)) .map(async heritage => { const declaration = getHeritageDeclaration(heritage, checker); if (isUndefined(declaration)) return; // Class. if (ts.isClassDeclaration(declaration)) { chain.push( await buildClassHeritageMeta( checker, plugin, heritage, declaration, ), ); } // Interface. else if ( ts.isTypeAliasDeclaration(declaration) || ts.isInterfaceDeclaration(declaration) ) { chain.push( await buildInterfaceHeritageMeta( checker, plugin, heritage, declaration, parent, ), ); } // Mixin. else if ( ts.isVariableDeclaration(declaration) || ts.isFunctionDeclaration(declaration) ) { chain.push( await buildMixinHeritageMeta( checker, plugin, heritage, declaration, ), ); } }), ); return chain; } export async function buildClassHeritageMeta( checker: TypeChecker, plugin: Plugin, heritage: HeritageMeta, declaration: ClassDeclaration | ClassExpression, ): Promise { const component = await buildComponentMetaWithHeritage( checker, plugin, declaration, heritage, ); return { ...heritage, component, }; } export async function buildMixinHeritageMeta( checker: TypeChecker, plugin: Plugin, heritage: HeritageMeta, declaration: VariableDeclaration | FunctionDeclaration, ): Promise { const body = ts.isVariableDeclaration(declaration) ? (declaration.initializer as ArrowFunction).body : declaration.body!; const mixin = ts.forEachChild(body, node => { if (ts.isClassDeclaration(node)) return node; if (ts.isReturnStatement(node) && ts.isClassExpression(node.expression!)) { return node.expression; } return undefined; }); const mixinMeta = mixin ? await buildComponentMetaWithHeritage(checker, plugin, mixin, heritage) : undefined; return { ...heritage, mixin: mixinMeta, }; } export async function buildInterfaceHeritageMeta( checker: TypeChecker, plugin: Plugin, heritage: HeritageMeta, declaration: TypeAliasDeclaration | InterfaceDeclaration, parent?: HeritageMeta, ): Promise { const props: PropMeta[] = []; const methods: MethodMeta[] = []; const heritages = ts.isInterfaceDeclaration(declaration) ? buildHeritageMeta(checker, declaration, parent) : []; // Follow interface heritages all the way up. heritages .map(h => getHeritageDeclaration(h, checker)) .filter(d => !isUndefined(d) && ts.isInterfaceDeclaration(d)) .map(d => buildHeritageMetaTree( checker, plugin, buildHeritageMeta(checker, d! as InterfaceDeclaration, heritage), heritage, ), ); // TODO: handle this case by finding and unpacking type references. if (ts.isTypeAliasDeclaration(declaration)) { // ... } function findSignatures(node: Node) { if (ts.isPropertySignature(node)) { props.push(buildPropMetaFromDeclarationOrSignature(checker, node)); return; } if (ts.isMethodSignature(node)) { methods.push(buildMethodMetaFromDeclarationOrSignature(checker, node)); return; } ts.forEachChild(node, findSignatures); } ts.forEachChild(declaration, findSignatures); return { ...heritage, interface: { node: declaration, source: buildSourceMetaFromNode(declaration), props, methods, heritage: heritages, docTags: getDocTags(declaration), documentation: getDocumentation(checker, declaration.name as Identifier), }, }; } export function getHeritageDeclaration( heritage: HeritageMeta, checker: TypeChecker, ): Declaration | undefined { const type = checker.getTypeAtLocation(heritage.node); const sourceFile = getTypeSourceFile(type); const fileSymbol = checker.getSymbolAtLocation(sourceFile)!; const symbol = fileSymbol?.exports?.get(heritage.name as __String); return symbol?.getDeclarations()?.[0]; } export function buildHeritageMeta( checker: TypeChecker, declaration: T, parent?: HeritageMeta, ): HeritageMeta[] { const heritage: HeritageMeta[] = []; const clauseToMeta = ( node: ExpressionWithTypeArguments | Identifier | CallExpression, ) => { if (ts.isIdentifier(node)) { const isParentHeritageClause = ts.isHeritageClause(node.parent.parent); const isParentCallExpression = ts.isCallExpression(node.parent); const isBaseMixinClass = isParentCallExpression && (node.parent as CallExpression).expression !== node; const symbol = checker.getSymbolAtLocation(node)!; const clauseDeclaration = symbol.getDeclarations?.()?.[0]; if (ts.isParameter(clauseDeclaration!)) { return; } const kind = isParentCallExpression ? isBaseMixinClass ? HeritageKind.Subclass : HeritageKind.Mixin : isParentHeritageClause && (node.parent.parent as HeritageClause).token === ts.SyntaxKind.ImplementsKeyword ? HeritageKind.Interface : HeritageKind.Subclass; const name = node.escapedText.toString(); const type = checker.getTypeAtLocation(node); heritage.push({ name, node, kind, parent, documentation: getDocumentation(checker, node), typeInfo: getPropTypeInfo(checker, node, type), }); } else if (ts.isExpressionWithTypeArguments(node)) { clauseToMeta(node.expression as Identifier | CallExpression); } else if (ts.isCallExpression(node)) { clauseToMeta(node.expression as Identifier); if (node.arguments[0]) clauseToMeta(node.arguments[0] as CallExpression); } }; const heritageClauses = ( declaration as unknown as | ClassDeclaration | ClassExpression | InterfaceDeclaration ).heritageClauses ?? ts.factory.createNodeArray(); heritageClauses.forEach(clause => { clause.types.forEach(node => { clauseToMeta(node); }); }); return heritage; }