import type { EnumDeclaration, GetAccessorDeclaration, Identifier, LiteralType, ParameterDeclaration, PropertyDeclaration, PropertySignature, Type, TypeChecker, } from 'typescript'; import ts from 'typescript'; import { PropMeta, PropTypeInfo } from '../../plugins/ComponentMeta.js'; import { getDeclarationParameters, isDecoratorNamed, } from '../../utils/decorators.js'; import { LogLevel, reportDiagnosticByNode } from '../../utils/log.js'; import { isMemberPrivate } from '../../utils/members.js'; import { camelCaseToDashCase } from '../../utils/string.js'; import { isUndefined } from '../../utils/unit.js'; import { getDocumentation } from './meta-doc.js'; import { findDocTag, getDocTags, hasDocTag } from './meta-doc-tag.js'; import { getTypeReferences, resolveType, typeTextFromTSType, typeToString, } from './meta-type.js'; export interface DefaultPropOptions { attribute?: string; reflect?: boolean; } export function buildPropMetaFromDeclarationOrSignature( checker: TypeChecker, declaration: PropertyDeclaration | GetAccessorDeclaration | PropertySignature, propDecoratorName = '', internalPropDecoratorName = '', transformPropOptions?: (propOptions: T) => DefaultPropOptions, ): PropMeta { const prop: Partial = {}; const identifier = declaration.name as Identifier; const symbol = checker.getSymbolAtLocation(identifier)!; const type = checker.getTypeAtLocation(declaration); const typeDeclaration = type.aliasSymbol?.declarations?.[0]; const name = symbol.escapedName as string; const isProperty = ts.isPropertyDeclaration(declaration) || ts.isPropertySignature(declaration); const decorator = declaration.decorators?.find( isDecoratorNamed(propDecoratorName), ); const decoratorParams = decorator ? getDeclarationParameters(decorator) : undefined; const propOptions = decoratorParams?.[0] as T | undefined; const hasSetter = !isProperty ? (symbol.declarations?.length ?? 0) > 1 : undefined; const isStatic = declaration.modifiers?.some(m => m.kind === ts.SyntaxKind.StaticKeyword) ?? false; if (isProperty && isMemberPrivate(declaration)) { reportDiagnosticByNode( [ `Property \`${name}\` cannot be \`private\` or \`protected\`. Use the`, `\`@${internalPropDecoratorName}()\` decorator instead.`, ].join('\n'), declaration, LogLevel.Warn, ); } const typeText = typeTextFromTSType(type); // @ts-expect-error - not sure how to cast declaration node to union typed node. const unionTypesText = type?.types?.map(type => type.value).filter(Boolean); if (unionTypesText && unionTypesText.length > 0) prop.unionTypesText = unionTypesText; // Prop can have an attribute if type is NOT "unknown". if ( (typeText !== 'unknown' || (!isProperty && hasSetter)) && !isUndefined(propOptions) && !isUndefined(transformPropOptions) ) { const { attribute, reflect } = transformPropOptions(propOptions); prop.attribute = attribute ? attribute.trim().toLowerCase() : undefined; prop.reflect = reflect ?? false; } prop.node = declaration; prop.name = name; prop.typeText = typeText; prop.static = isStatic; prop.typeInfo = getPropTypeInfo(checker, declaration, type); prop.documentation = getDocumentation(checker, identifier); prop.docTags = getDocTags(declaration); prop.readonly = (!isProperty && !hasSetter) || (!hasSetter && hasDocTag(prop.docTags, 'readonly')); prop.attribute = !prop.readonly ? prop.attribute ?? camelCaseToDashCase(name) : findDocTag(prop.docTags, 'attribute')?.text; prop.internal = hasDocTag(prop.docTags, 'internal'); prop.deprecated = hasDocTag(prop.docTags, 'deprecated'); prop.required = !isUndefined((declaration as PropertyDeclaration).exclamationToken) || hasDocTag(prop.docTags, 'required'); prop.optional = !isUndefined(declaration.questionToken) || hasDocTag(prop.docTags, 'optional'); prop.defaultValue = ts.isPropertyDeclaration(declaration) ? declaration.initializer?.getText() : findDocTag(prop.docTags, 'default')?.text; prop.defaultValue = prop.defaultValue ?? (prop.optional ? 'undefined' : ''); prop.enum = !isUndefined(typeDeclaration) && ts.isEnumDeclaration(typeDeclaration); prop.enumDeclaration = prop.enum ? (typeDeclaration as EnumDeclaration) : undefined; prop.enumDefaultValue = prop.enum && ts.isPropertyDeclaration(declaration) && !isUndefined(declaration.initializer) && ts.isPropertyAccessExpression(declaration.initializer) ? String( (checker.getTypeAtLocation(declaration.initializer) as LiteralType) .value, ) : undefined; return prop as PropMeta; } export function getPropTypeInfo( typeChecker: TypeChecker, node: | PropertyDeclaration | ParameterDeclaration | PropertySignature | GetAccessorDeclaration | Identifier, type: Type, ): PropTypeInfo { const nodeType = (node as PropertyDeclaration).type; return { original: nodeType ? nodeType.getText() : typeToString(typeChecker, type), resolved: resolveType(typeChecker, type), references: getTypeReferences(node), }; }