/** * Shared utilities for the per-declaration extractors in `typescript-extract-*.ts`. * * Holds helpers used across function, type, and class extraction: signature * parameter extraction, overload detection, generic parsing, modifier * extraction, location reporting, intersection-property filtering, and runes * detection. * * @see `typescript-extract-function.ts`, `typescript-extract-type.ts`, * `typescript-extract-class.ts` for the per-declaration extractors that * build on these helpers * * @module */ import ts from 'typescript'; import type { GenericParamJson, DeclarationKind, DeclarationModifier, MemberKind, ParameterJson, OverloadJsonInput, Reactivity, } from './types.js'; import type {DeclarationJsonBuild, MemberJsonBuild} from './declaration-build.js'; import {type Diagnostic, type MisplacedTagDiagnostic} from './diagnostics.js'; import {applyToDeclaration, parseComment, type TsdocParsedComment} from './tsdoc.js'; import {type IsExternalFile} from './typescript-program.js'; /** * Infer declaration kind from symbol and node. * * Maps TypeScript constructs to `DeclarationKind`: * - Classes → `'class'` * - Functions (declarations, expressions, arrows) → `'function'` * - Interfaces → `'interface'` * - Type aliases → `'type'` * - Enums (regular and const) → `'enum'` * - Variables → `'variable'` (unless function-valued → `'function'`) * * Note: namespace re-exports (`export * as ns from './x'`) have no inline * declaration form in TypeScript and are caught upstream in `analyzeExports` * via `classifyNamespaceReExport`. They never reach this function. A direct * call here on a `ValueModule` symbol would fall through to `'variable'` and * leak `typeof import("/abs/path")` into the output — keep the namespace * dispatch in `analyzeExports`. */ export const inferDeclarationKind = (symbol: ts.Symbol, node: ts.Node): DeclarationKind => { // Check symbol flags if (symbol.flags & ts.SymbolFlags.Class) return 'class'; if (symbol.flags & ts.SymbolFlags.Function) return 'function'; if (symbol.flags & ts.SymbolFlags.Interface) return 'interface'; if (symbol.flags & ts.SymbolFlags.TypeAlias) return 'type'; if (symbol.flags & ts.SymbolFlags.Enum) return 'enum'; if (symbol.flags & ts.SymbolFlags.ConstEnum) return 'enum'; // Check node kind if (ts.isFunctionDeclaration(node) || ts.isArrowFunction(node) || ts.isFunctionExpression(node)) return 'function'; if (ts.isClassDeclaration(node)) return 'class'; if (ts.isInterfaceDeclaration(node)) return 'interface'; if (ts.isTypeAliasDeclaration(node)) return 'type'; if (ts.isEnumDeclaration(node)) return 'enum'; if (ts.isVariableDeclaration(node)) { // Check if it's a function-valued variable const init = node.initializer; if (init && (ts.isArrowFunction(init) || ts.isFunctionExpression(init))) { return 'function'; } return 'variable'; } return 'variable'; }; /** * Extract parameters from a TypeScript signature with TSDoc descriptions and default values. * * Shared helper for extracting parameter information from both standalone functions * and class methods/constructors. * * @param sig - the TypeScript signature to extract parameters from * @param checker - TypeScript type checker for type resolution * @param tsdocParams - record of parameter names to TSDoc descriptions (from `TsdocParsedComment.params`) * @returns array of `ParameterJson` objects */ export const extractSignatureParameters = ( sig: ts.Signature, checker: ts.TypeChecker, tsdocParams: Record<string, string> | undefined, ): Array<ParameterJson> => { return sig.parameters.map((param) => { const paramDecl = param.valueDeclaration; // Get type - use declaration location if available, otherwise get declared type let typeString = 'unknown'; if (paramDecl) { const paramType = checker.getTypeOfSymbolAtLocation(param, paramDecl); typeString = checker.typeToString(paramType); } else { const paramType = checker.getDeclaredTypeOfSymbol(param); typeString = checker.typeToString(paramType); } // Get TSDoc description for this parameter const description = tsdocParams?.[param.name]; // Collect dotted `@param obj.prop` descriptions for object/destructured // parameters, keyed by the sub-path relative to this parameter // (`obj.prop` → `prop`, `obj.a.b` → `a.b`). let propertyDescriptions: Record<string, string> | undefined; if (tsdocParams) { const prefix = param.name + '.'; for (const [key, value] of Object.entries(tsdocParams)) { if (key.startsWith(prefix)) { // Null-prototype map: the sliced sub-path is source-derived and is // emitted as `propertyDescriptions`; a `@param obj.__proto__` key on a // plain object would pollute the prototype on write. (propertyDescriptions ??= Object.create(null))[key.slice(prefix.length)] = value; } } } // Extract default value from AST let defaultValue: string | undefined; if (paramDecl && ts.isParameter(paramDecl) && paramDecl.initializer) { defaultValue = paramDecl.initializer.getText(); } const optional = !!(paramDecl && ts.isParameter(paramDecl) && paramDecl.questionToken); const rest = !!(paramDecl && ts.isParameter(paramDecl) && paramDecl.dotDotDotToken); return { name: param.name, type: typeString, optional, rest, description, defaultValue, propertyDescriptions, }; }); }; /** * Emit `unknown_param` warnings for `@param` keys that don't reference a real * parameter. Catches typos (`@param argz` for `args`) and stale doc after a * rename. The description is dropped silently by `extractSignatureParameters`; * this surfaces the drop without halting. Dotted keys (`@param obj.prop`) that * document a property of an object parameter are accepted when `obj` is a real * parameter. * * @internal Helper for `extractOverloads` and other `@param`-extracting sites. */ const validateParamKeys = ( tsdocParams: Record<string, string> | undefined, parameters: ReadonlyArray<{name: string}>, declNode: ts.Node, functionName: string, diagnostics: Array<Diagnostic>, ): void => { if (!tsdocParams) return; const known = new Set(parameters.map((p) => p.name)); for (const key of Object.keys(tsdocParams)) { // Dotted keys (`@param obj.prop`) document a property of an object/destructured // parameter — valid JSDoc/TSDoc. Treat as known when the root segment is a real param. const root = key.includes('.') ? key.slice(0, key.indexOf('.')) : key; if (!known.has(key) && !known.has(root)) { const loc = getNodeLocation(declNode); diagnostics.push({ kind: 'unknown_param', file: loc.file, line: loc.line, column: loc.column, message: `@param "${key}" on "${functionName}" doesn't match any parameter (typo or stale doc?)`, severity: 'warning', paramName: key, functionName, }); } } }; /** * Collect symbol-scope JSDoc tags present on a parsed comment. * * Symbol-scope tags describe the function as a whole and belong on the * primary signature's JSDoc (which feeds the parent declaration). Used by * `extractOverloads` to detect tags misplaced on non-primary overloads. * * @internal */ const collectSymbolScopeTags = ( tsdoc: TsdocParsedComment, ): Array<MisplacedTagDiagnostic['tagName']> => { const found: Array<MisplacedTagDiagnostic['tagName']> = []; if (tsdoc.examples?.length) found.push('example'); if (tsdoc.deprecatedMessage !== undefined) found.push('deprecated'); if (tsdoc.since) found.push('since'); if (tsdoc.seeAlso?.length) found.push('see'); if (tsdoc.throws?.length) found.push('throws'); if (tsdoc.mutates && Object.keys(tsdoc.mutates).length > 0) found.push('mutates'); if (tsdoc.defaultValue !== undefined) found.push('default'); if (tsdoc.nodocs) found.push('nodocs'); return found; }; /** * Extract all public overload signatures for a function. * * Each overload gets its own typeSignature, parameters, returnType, and * per-overload JSDoc if available. The implementation signature is excluded * (TypeScript's `getCallSignatures()` already omits it). * * Per-overload `@param` descriptions flow through to that overload's * `parameters[i].description`. Per-overload `@returns` populates * `returnDescription`. These are signature-scope: each overload may * describe its own parameters and return value distinctly. * * Symbol-scope tags (`@example`, `@deprecated`, `@since`, `@see`, `@throws`, * `@mutates`) describe the function as a whole and belong on the parent * declaration. The primary overload — the one whose JSDoc text matches the * parent's `docComment` — already feeds the parent's symbol-level extraction, * so its symbol-scope tags reach the parent through that path. On non-primary * overloads, symbol-scope tags would otherwise be silently dropped from * output; this function emits a `misplaced_tag` warning instead, pointing the * author at the primary signature. * * @param signatures - all call signatures from the type checker * @param checker - TypeScript type checker * @param parentTsdoc - parsed JSDoc of the parent declaration (for primary-signature detection) * @param parentName - parent function/method name (for diagnostic messages) * @param diagnostics - diagnostics collector for non-fatal issues * @returns array of overload info objects */ const extractOverloads = ( signatures: ReadonlyArray<ts.Signature>, checker: ts.TypeChecker, parentTsdoc: TsdocParsedComment | undefined, parentName: string, diagnostics: Array<Diagnostic>, ): Array<OverloadJsonInput> => { return signatures.map((sig) => { const decl = sig.getDeclaration(); const sourceFile = decl.getSourceFile(); const tsdoc = parseComment(decl, sourceFile); const typeSignature = checker.signatureToString(sig); const returnType = checker.typeToString(checker.getReturnTypeOfSignature(sig)); const parameters = extractSignatureParameters(sig, checker, tsdoc?.params); validateParamKeys(tsdoc?.params, parameters, decl, parentName, diagnostics); const overload: OverloadJsonInput = {typeSignature, parameters, returnType}; if (tsdoc?.text) { overload.docComment = tsdoc.text; } if (tsdoc?.returns) { overload.returnDescription = tsdoc.returns; } // Extract per-overload generic type parameters if (ts.isFunctionLike(decl) && decl.typeParameters?.length) { overload.genericParams = decl.typeParameters.map(parseGenericParam); } // Detect primary overload by matching JSDoc text against the parent's. // The TS API resolves the parent declaration's JSDoc by walking from the // implementation node to the first overload signature with JSDoc; that // signature is the "primary" — its symbol-scope tags already reach the // parent through symbol-level extraction. Non-primary overloads with // symbol-scope tags would silently lose them; surface as warnings instead. const isPrimary = tsdoc?.text !== undefined && parentTsdoc?.text !== undefined && tsdoc.text === parentTsdoc.text; if (!isPrimary && tsdoc) { const misplaced = collectSymbolScopeTags(tsdoc); if (misplaced.length > 0) { const loc = getNodeLocation(decl); for (const tagName of misplaced) { diagnostics.push({ kind: 'misplaced_tag', file: loc.file, line: loc.line, column: loc.column, message: `@${tagName} on non-primary overload of "${parentName}" — place it on the primary signature's JSDoc instead (symbol-scope tags describe the function as a whole)`, severity: 'warning', tagName, functionName: parentName, }); } } } return overload; }); }; /** * Populate the callable fields of a declaration or member from its call/construct * signatures: `typeSignature`, `parameters`, `overloads`, and (unless * `includeReturn` is false) `returnType` / `returnDescription`. * * The shared core of every named-callable extractor — standalone functions, * interface methods, class methods/constructors, and type-alias function * properties. Callers differ in how they obtain `signatures` (symbol type, * constructor declarations, property call signatures) and in their own * try/catch + diagnostic kind, so those stay at the callsite; this captures * only the identical projection from a resolved signature list onto the build * target. No-op when `signatures` is empty. * * @param target - declaration or member build object (mutated) * @param signatures - public call/construct signatures (`signatures[0]` is primary) * @param tsdoc - parsed TSDoc for the target (supplies `@param`/`@returns`) * @param paramValidationNode - node `validateParamKeys` reports `unknown_param` against * @param name - target name, for diagnostic messages * @param includeReturn - set `false` for constructors (no return type/description) * @mutates target - sets typeSignature, parameters, overloads, returnType, returnDescription * @mutates diagnostics - via `validateParamKeys` / `extractOverloads` */ export const populateCallableMember = ( target: DeclarationJsonBuild | MemberJsonBuild, signatures: ReadonlyArray<ts.Signature>, checker: ts.TypeChecker, tsdoc: TsdocParsedComment | undefined, paramValidationNode: ts.Node, name: string, diagnostics: Array<Diagnostic>, includeReturn = true, ): void => { if (signatures.length === 0) return; const sig = signatures[0]!; target.typeSignature = checker.signatureToString(sig); if (includeReturn) { target.returnType = checker.typeToString(checker.getReturnTypeOfSignature(sig)); if (tsdoc?.returns) target.returnDescription = tsdoc.returns; } target.parameters = extractSignatureParameters(sig, checker, tsdoc?.params); validateParamKeys(tsdoc?.params, target.parameters, paramValidationNode, name, diagnostics); if (signatures.length > 1) { target.overloads = extractOverloads(signatures, checker, tsdoc, name, diagnostics); } }; /** * Check whether all declarations of a property symbol are in external source files. * Properties with no declarations (synthesized) are considered non-external. */ const isExternalProperty = (prop: ts.Symbol, isExternalFile: IsExternalFile): boolean => { const decls = prop.getDeclarations(); if (!decls?.length) return false; return decls.every((d) => isExternalFile(d.getSourceFile())); }; /** * Determine whether an intersection branch refers to a type whose declarations * all live in external files (e.g., `HTMLAttributes<HTMLDivElement>` from svelte's * d.ts). Inline object-literal branches and unrecognized node shapes return false * (treated as local). */ export const isExternalIntersectionBranch = ( branchNode: ts.TypeNode, checker: ts.TypeChecker, isExternalFile: IsExternalFile, ): boolean => { if (!ts.isTypeReferenceNode(branchNode) && !ts.isIndexedAccessTypeNode(branchNode)) { return false; } const branchType = checker.getTypeAtLocation(branchNode); const sym = branchType.aliasSymbol ?? branchType.symbol; const decls = sym.getDeclarations(); if (!decls?.length) return false; return decls.every((d) => isExternalFile(d.getSourceFile())); }; /** * Resolve the `IntersectionTypeNode` for a type node, walking through type * alias indirection. Returns `undefined` when the underlying type is an * intersection but no AST node is recoverable (rare — synthesized intersections). */ export const resolveIntersectionTypeNode = ( type: ts.Type, typeNode: ts.Node, ): ts.IntersectionTypeNode | undefined => { if (ts.isIntersectionTypeNode(typeNode)) return typeNode; const aliasDecl = type.aliasSymbol?.declarations?.[0]; if ( aliasDecl && ts.isTypeAliasDeclaration(aliasDecl) && ts.isIntersectionTypeNode(aliasDecl.type) ) { return aliasDecl.type; } return undefined; }; /** * Determine whether a type-reference / indexed-access node names a type whose * properties all come from external files (e.g. `SvelteHTMLElements['li']`, * `HTMLAttributes<HTMLDivElement>`). Such a node is an external attribute "bag" * that should be summarized in `intersects` rather than enumerated as members. * * Mirrors the per-property origin test used for membership: external only when * the node has at least one property and every property is external. A * zero-property branch (e.g. a pure index signature) is not surfaced — there is * no named member to attribute to it. */ const isExternalTypeRefNode = ( node: ts.TypeNode, checker: ts.TypeChecker, isExternalFile: IsExternalFile, ): boolean => { const props = checker.getTypeAtLocation(node).getProperties(); return props.length > 0 && props.every((p) => isExternalProperty(p, isExternalFile)); }; /** * Walk a written type node and collect, in source order, the verbatim text of * every external type reference it composes. * * Structure is read from the AST rather than the inferred type because * inference erases it: `(A | B) & C` normalizes to a union and `X['k']` * flattens to a property bag, both losing the `&`/`|`/index-access shape the * author wrote. Composition nodes (intersection, union, parenthesized) recurse; * leaf references (`TypeReference`, `IndexedAccessType`) are tested with * `isExternalTypeRefNode` and, when external, emitted via `getText()`. Inline * object literals and other local shapes contribute no entry. * * @mutates out - appends each external reference's source text */ const collectExternalTypeRefs = ( node: ts.TypeNode, checker: ts.TypeChecker, isExternalFile: IsExternalFile, out: Array<string>, ): void => { if (ts.isParenthesizedTypeNode(node)) { collectExternalTypeRefs(node.type, checker, isExternalFile, out); } else if (ts.isIntersectionTypeNode(node) || ts.isUnionTypeNode(node)) { for (const branch of node.types) collectExternalTypeRefs(branch, checker, isExternalFile, out); } else if (ts.isTypeReferenceNode(node) || ts.isIndexedAccessTypeNode(node)) { if (isExternalTypeRefNode(node, checker, isExternalFile)) out.push(node.getText()); } }; /** * Resolve a props/type-alias annotation node to the written type node whose * structure drives `intersects` extraction. * * The svelte2tsx props annotation is a reference to a generated `$$ComponentProps` * alias; unwrap one level of *local* type-alias reference so the underlying * composition (`SvelteHTMLElements['li']`, `(A | B) & C`, …) is visible. External * alias references are left intact so they read as a single named bag rather than * leaking their node_modules-internal definition. Type-alias callers pass the * written node directly, so for them this is a no-op. */ const resolveAnnotationTypeNode = ( typeNode: ts.Node, checker: ts.TypeChecker, isExternalFile: IsExternalFile, ): ts.TypeNode | undefined => { if (ts.isTypeReferenceNode(typeNode)) { const aliasDecl = checker .getSymbolAtLocation(typeNode.typeName) ?.getDeclarations() ?.find(ts.isTypeAliasDeclaration); if (aliasDecl && !isExternalFile(aliasDecl.getSourceFile())) return aliasDecl.type; } return ts.isTypeNode(typeNode) ? typeNode : undefined; }; /** * Partition a type's properties into local (kept) and external (dropped), and * collect the external type references that contributed the dropped ones. * * Applies to any composition shape — intersection, union, bare reference, * indexed-access — not only intersections. Membership is decided per property * by declaration origin (`isExternalProperty`), which is structure-agnostic: * TypeScript preserves original declaration sources on derived properties, so * the test gives the right answer through utility-type wrappers (Partial, Pick, * `OmitStrict`) too. A property with no declarations (synthesized) is treated as * local and kept. The external-type labels for `intersects` come from an AST * walk (`collectExternalTypeRefs`) — the authoritative source for the * `&`/`|`/index-access shape inference would otherwise erase. */ export const filterExternalProperties = ( type: ts.Type, typeNode: ts.Node, checker: ts.TypeChecker, isExternalFile: IsExternalFile, ): {properties: Array<ts.Symbol>; externalTypes: Array<string>} => { const properties = type .getProperties() .filter((prop) => !isExternalProperty(prop, isExternalFile)); const externalTypes: Array<string> = []; const annotation = resolveAnnotationTypeNode(typeNode, checker, isExternalFile); if (annotation) collectExternalTypeRefs(annotation, checker, isExternalFile, externalTypes); return {properties, externalTypes}; }; /** * Detect a Svelte 5 reactivity rune from a variable or property initializer. * * Inspects the AST since runes erase to their inner type after type-checking. * Returns `undefined` for any non-rune expression. See the `Reactivity` enum * in `types.ts` for the rationale on running this on every file regardless of * extension. */ export const detectReactivity = ( initializer: ts.Expression | undefined, ): Reactivity | undefined => { // Unwrap type-only wrappers so e.g. `$state(0) as Foo` and `($state(0))` are // still detected. Runtime semantics are unchanged by these wrappers. let expr: ts.Expression | undefined = initializer; while ( expr && (ts.isParenthesizedExpression(expr) || ts.isAsExpression(expr) || ts.isSatisfiesExpression(expr) || ts.isNonNullExpression(expr) || ts.isTypeAssertionExpression(expr)) ) { expr = expr.expression; } if (!expr || !ts.isCallExpression(expr)) return undefined; const callee = expr.expression; if (ts.isIdentifier(callee)) { if (callee.text === '$state') return '$state'; if (callee.text === '$derived') return '$derived'; return undefined; } if (ts.isPropertyAccessExpression(callee) && ts.isIdentifier(callee.expression)) { const base = callee.expression.text; const prop = callee.name.text; if (base === '$state' && prop === 'raw') return '$state.raw'; if (base === '$derived' && prop === 'by') return '$derived.by'; } return undefined; }; /** * Extract line and column from a TypeScript node. * Returns 1-based line and column numbers. */ export const getNodeLocation = (node: ts.Node): {file: string; line: number; column: number} => { const sourceFile = node.getSourceFile(); const {line, character} = sourceFile.getLineAndCharacterOfPosition(node.getStart()); return { file: sourceFile.fileName, line: line + 1, // Convert to 1-based column: character + 1, // Convert to 1-based }; }; /** * Parse a TypeScript generic type parameter declaration into structured info. * * @param param - the TypeScript type parameter declaration node * @returns structured `GenericParamJson` with name, constraint, and default type */ export const parseGenericParam = (param: ts.TypeParameterDeclaration): GenericParamJson => { const result: GenericParamJson = { name: param.name.text, }; if (param.constraint) { result.constraint = param.constraint.getText(); } if (param.default) { result.defaultType = param.default.getText(); } return result; }; /** * Extract modifier keywords from a node's modifiers. * * Returns an array of modifier strings like `['public', 'readonly', 'static']`. */ export const extractModifiers = ( modifiers: ReadonlyArray<ts.ModifierLike> | undefined, ): Array<DeclarationModifier> => { const modifierFlags: Array<DeclarationModifier> = []; if (!modifiers) return modifierFlags; for (const mod of modifiers) { if (mod.kind === ts.SyntaxKind.PublicKeyword) modifierFlags.push('public'); else if (mod.kind === ts.SyntaxKind.ProtectedKeyword) modifierFlags.push('protected'); else if (mod.kind === ts.SyntaxKind.ReadonlyKeyword) modifierFlags.push('readonly'); else if (mod.kind === ts.SyntaxKind.StaticKeyword) modifierFlags.push('static'); else if (mod.kind === ts.SyntaxKind.AbstractKeyword) modifierFlags.push('abstract'); } return modifierFlags; }; /** * Append a `(call)` or `(construct)` signature member to a declaration. * * Captures the extraction logic shared by interface processing * (`extractTypeInfo`) and type-alias property processing * (`extractTypeAliasProperties`): type signature, parameters, generics, * overloads, and TSDoc. The TSDoc source node is supplied by the caller — * interfaces look it up in `node.members` (skipping TSDoc when no inline * signature is declared, even if one is inherited), type aliases use * `sig.getDeclaration()`. * * @param getSignatures - thunk to retrieve `getCallSignatures()` / * `getConstructSignatures()`; called inside the try so checker errors are * captured as diagnostics * @param signatureKind - `'call'` (member kind: function, includes returnType) * or `'construct'` (member kind: constructor, no returnType) * @param resolveTsdocNode - callback returning the AST node to parse TSDoc * from, or `undefined` to skip TSDoc resolution * @param paramValidationFallbackNode - location used by `validateParamKeys` * when `resolveTsdocNode` returns `undefined` * @param declaration - parent declaration (mutated; appended to `members`) * @param errorContext.node - parent node used to locate diagnostics * @param errorContext.kindLabel - `'interface'` or `'type'`, included in the * diagnostic message * * @mutates declaration - appends a member when signatures are present; * sets `partial: true` on extraction failure * @mutates diagnostics - adds `signature_analysis_failed` on checker error */ export const emitCallOrConstructSignature = ( getSignatures: () => ReadonlyArray<ts.Signature>, signatureKind: 'call' | 'construct', resolveTsdocNode: (sig: ts.Signature) => ts.Node | undefined, paramValidationFallbackNode: ts.Node, declaration: DeclarationJsonBuild, checker: ts.TypeChecker, diagnostics: Array<Diagnostic>, errorContext: {node: ts.Node; kindLabel: string}, ): void => { try { const signatures = getSignatures(); if (signatures.length === 0) return; const memberName = signatureKind === 'call' ? '(call)' : '(construct)'; const memberKind: MemberKind = signatureKind === 'call' ? 'function' : 'constructor'; const member: MemberJsonBuild = {name: memberName, kind: memberKind}; const sig = signatures[0]!; member.typeSignature = checker.signatureToString(sig); if (signatureKind === 'call') { member.returnType = checker.typeToString(checker.getReturnTypeOfSignature(sig)); } const tsdocNode = resolveTsdocNode(sig); const tsdoc = tsdocNode ? parseComment(tsdocNode, tsdocNode.getSourceFile()) : undefined; applyToDeclaration(member, tsdoc); member.parameters = extractSignatureParameters(sig, checker, tsdoc?.params); validateParamKeys( tsdoc?.params, member.parameters, tsdocNode ?? paramValidationFallbackNode, memberName, diagnostics, ); const sigDecl = sig.getDeclaration(); if (ts.isFunctionLike(sigDecl) && sigDecl.typeParameters?.length) { member.genericParams = sigDecl.typeParameters.map(parseGenericParam); } if (signatures.length > 1) { member.overloads = extractOverloads(signatures, checker, tsdoc, memberName, diagnostics); } (declaration.members ??= []).push(member); } catch (err) { declaration.partial = true; const loc = getNodeLocation(errorContext.node); diagnostics.push({ kind: 'signature_analysis_failed', file: loc.file, line: loc.line, column: loc.column, message: `Failed to analyze ${signatureKind} signatures for ${errorContext.kindLabel} "${declaration.name}": ${err instanceof Error ? err.message : String(err)}`, severity: 'warning', functionName: declaration.name ?? '<default export>', }); } };
{ "path": "typescript-extract-shared.ts", "declarations": [ { "name": "inferDeclarationKind", "kind": "function", "docComment": "Infer declaration kind from symbol and node.\n\nMaps TypeScript constructs to `DeclarationKind`:\n- Classes → `'class'`\n- Functions (declarations, expressions, arrows) → `'function'`\n- Interfaces → `'interface'`\n- Type aliases → `'type'`\n- Enums (regular and const) → `'enum'`\n- Variables → `'variable'` (unless function-valued → `'function'`)\n\nNote: namespace re-exports (`export * as ns from './x'`) have no inline\ndeclaration form in TypeScript and are caught upstream in `analyzeExports`\nvia `classifyNamespaceReExport`. They never reach this function. A direct\ncall here on a `ValueModule` symbol would fall through to `'variable'` and\nleak `typeof import(\"/abs/path\")` into the output — keep the namespace\ndispatch in `analyzeExports`.", "typeSignature": "(symbol: Symbol, node: Node): \"function\" | \"type\" | \"variable\" | \"class\" | \"interface\" | \"enum\" | \"component\" | \"snippet\" | \"namespace\"", "sourceLine": 50, "parameters": [ { "name": "symbol", "type": "Symbol" }, { "name": "node", "type": "Node" } ], "returnType": "\"function\" | \"type\" | \"variable\" | \"class\" | \"interface\" | \"enum\" | \"component\" | \"snippet\" | \"namespace\"" }, { "name": "extractSignatureParameters", "kind": "function", "docComment": "Extract parameters from a TypeScript signature with TSDoc descriptions and default values.\n\nShared helper for extracting parameter information from both standalone functions\nand class methods/constructors.", "typeSignature": "(sig: Signature, checker: TypeChecker, tsdocParams: Record<string, string> | undefined): { name: string; type: string; optional: boolean; rest: boolean; description?: string | undefined; defaultValue?: string | undefined; propertyDescriptions?: Record<...> | undefined; }[]", "sourceLine": 89, "parameters": [ { "name": "sig", "type": "Signature", "description": "the TypeScript signature to extract parameters from" }, { "name": "checker", "type": "TypeChecker", "description": "TypeScript type checker for type resolution" }, { "name": "tsdocParams", "type": "Record<string, string> | undefined", "description": "record of parameter names to TSDoc descriptions (from `TsdocParsedComment.params`)" } ], "returnType": "{ name: string; type: string; optional: boolean; rest: boolean; description?: string | undefined; defaultValue?: string | undefined; propertyDescriptions?: Record<string, string> | undefined; }[]", "returnDescription": "array of `ParameterJson` objects" }, { "name": "populateCallableMember", "kind": "function", "docComment": "Populate the callable fields of a declaration or member from its call/construct\nsignatures: `typeSignature`, `parameters`, `overloads`, and (unless\n`includeReturn` is false) `returnType` / `returnDescription`.\n\nThe shared core of every named-callable extractor — standalone functions,\ninterface methods, class methods/constructors, and type-alias function\nproperties. Callers differ in how they obtain `signatures` (symbol type,\nconstructor declarations, property call signatures) and in their own\ntry/catch + diagnostic kind, so those stay at the callsite; this captures\nonly the identical projection from a resolved signature list onto the build\ntarget. No-op when `signatures` is empty.", "typeSignature": "(target: MemberJsonBuild | DeclarationJsonBuild, signatures: readonly Signature[], checker: TypeChecker, tsdoc: TsdocParsedComment | undefined, paramValidationNode: Node, name: string, diagnostics: ({ ...; } | ... 12 more ... | { ...; })[], includeReturn?: boolean): void", "sourceLine": 325, "mutates": { "target": "sets typeSignature, parameters, overloads, returnType, returnDescription", "diagnostics": "via `validateParamKeys` / `extractOverloads`" }, "parameters": [ { "name": "target", "type": "MemberJsonBuild | DeclarationJsonBuild", "description": "declaration or member build object (mutated)" }, { "name": "signatures", "type": "readonly Signature[]", "description": "public call/construct signatures (`signatures[0]` is primary)" }, { "name": "checker", "type": "TypeChecker" }, { "name": "tsdoc", "type": "TsdocParsedComment | undefined", "description": "parsed TSDoc for the target (supplies `@param`/`@returns`)" }, { "name": "paramValidationNode", "type": "Node", "description": "node `validateParamKeys` reports `unknown_param` against" }, { "name": "name", "type": "string", "description": "target name, for diagnostic messages" }, { "name": "diagnostics", "type": "({ symbolName: string; file: string; message: string; severity: \"error\" | \"warning\"; kind: \"type_extraction_failed\"; line?: number | undefined; column?: number | undefined; } | { functionName: string; ... 5 more ...; column?: number | undefined; } | ... 11 more ... | { ...; })[]" }, { "name": "includeReturn", "type": "boolean", "description": "set `false` for constructors (no return type/description)", "defaultValue": "true" } ], "returnType": "void" }, { "name": "isExternalIntersectionBranch", "kind": "function", "docComment": "Determine whether an intersection branch refers to a type whose declarations\nall live in external files (e.g., `HTMLAttributes<HTMLDivElement>` from svelte's\nd.ts). Inline object-literal branches and unrecognized node shapes return false\n(treated as local).", "typeSignature": "(branchNode: TypeNode, checker: TypeChecker, isExternalFile: IsExternalFile): boolean", "sourceLine": 369, "parameters": [ { "name": "branchNode", "type": "TypeNode" }, { "name": "checker", "type": "TypeChecker" }, { "name": "isExternalFile", "type": "IsExternalFile" } ], "returnType": "boolean" }, { "name": "resolveIntersectionTypeNode", "kind": "function", "docComment": "Resolve the `IntersectionTypeNode` for a type node, walking through type\nalias indirection. Returns `undefined` when the underlying type is an\nintersection but no AST node is recoverable (rare — synthesized intersections).", "typeSignature": "(type: Type, typeNode: Node): IntersectionTypeNode | undefined", "sourceLine": 389, "parameters": [ { "name": "type", "type": "Type" }, { "name": "typeNode", "type": "Node" } ], "returnType": "IntersectionTypeNode | undefined" }, { "name": "filterExternalProperties", "kind": "function", "docComment": "Partition a type's properties into local (kept) and external (dropped), and\ncollect the external type references that contributed the dropped ones.\n\nApplies to any composition shape — intersection, union, bare reference,\nindexed-access — not only intersections. Membership is decided per property\nby declaration origin (`isExternalProperty`), which is structure-agnostic:\nTypeScript preserves original declaration sources on derived properties, so\nthe test gives the right answer through utility-type wrappers (Partial, Pick,\n`OmitStrict`) too. A property with no declarations (synthesized) is treated as\nlocal and kept. The external-type labels for `intersects` come from an AST\nwalk (`collectExternalTypeRefs`) — the authoritative source for the\n`&`/`|`/index-access shape inference would otherwise erase.", "typeSignature": "(type: Type, typeNode: Node, checker: TypeChecker, isExternalFile: IsExternalFile): { properties: Symbol[]; externalTypes: string[]; }", "sourceLine": 494, "parameters": [ { "name": "type", "type": "Type" }, { "name": "typeNode", "type": "Node" }, { "name": "checker", "type": "TypeChecker" }, { "name": "isExternalFile", "type": "IsExternalFile" } ], "returnType": "{ properties: Symbol[]; externalTypes: string[]; }" }, { "name": "detectReactivity", "kind": "function", "docComment": "Detect a Svelte 5 reactivity rune from a variable or property initializer.\n\nInspects the AST since runes erase to their inner type after type-checking.\nReturns `undefined` for any non-rune expression. See the `Reactivity` enum\nin `types.ts` for the rationale on running this on every file regardless of\nextension.", "typeSignature": "(initializer: Expression | undefined): \"$state\" | \"$state.raw\" | \"$derived\" | \"$derived.by\" | undefined", "sourceLine": 519, "parameters": [ { "name": "initializer", "type": "Expression | undefined" } ], "returnType": "\"$state\" | \"$state.raw\" | \"$derived\" | \"$derived.by\" | undefined" }, { "name": "getNodeLocation", "kind": "function", "docComment": "Extract line and column from a TypeScript node.\nReturns 1-based line and column numbers.", "typeSignature": "(node: Node): { file: string; line: number; column: number; }", "sourceLine": 558, "parameters": [ { "name": "node", "type": "Node" } ], "returnType": "{ file: string; line: number; column: number; }" }, { "name": "parseGenericParam", "kind": "function", "docComment": "Parse a TypeScript generic type parameter declaration into structured info.", "typeSignature": "(param: TypeParameterDeclaration): { name: string; constraint?: string | undefined; defaultType?: string | undefined; }", "sourceLine": 574, "parameters": [ { "name": "param", "type": "TypeParameterDeclaration", "description": "the TypeScript type parameter declaration node" } ], "returnType": "{ name: string; constraint?: string | undefined; defaultType?: string | undefined; }", "returnDescription": "structured `GenericParamJson` with name, constraint, and default type" }, { "name": "extractModifiers", "kind": "function", "docComment": "Extract modifier keywords from a node's modifiers.\n\nReturns an array of modifier strings like `['public', 'readonly', 'static']`.", "typeSignature": "(modifiers: readonly ModifierLike[] | undefined): (\"public\" | \"protected\" | \"readonly\" | \"static\" | \"abstract\" | \"getter\" | \"setter\")[]", "sourceLine": 595, "parameters": [ { "name": "modifiers", "type": "readonly ModifierLike[] | undefined" } ], "returnType": "(\"public\" | \"protected\" | \"readonly\" | \"static\" | \"abstract\" | \"getter\" | \"setter\")[]" }, { "name": "emitCallOrConstructSignature", "kind": "function", "docComment": "Append a `(call)` or `(construct)` signature member to a declaration.\n\nCaptures the extraction logic shared by interface processing\n(`extractTypeInfo`) and type-alias property processing\n(`extractTypeAliasProperties`): type signature, parameters, generics,\noverloads, and TSDoc. The TSDoc source node is supplied by the caller —\ninterfaces look it up in `node.members` (skipping TSDoc when no inline\nsignature is declared, even if one is inherited), type aliases use\n`sig.getDeclaration()`.", "typeSignature": "(getSignatures: () => readonly Signature[], signatureKind: \"call\" | \"construct\", resolveTsdocNode: (sig: Signature) => Node | undefined, paramValidationFallbackNode: Node, declaration: DeclarationJsonBuild, checker: TypeChecker, diagnostics: ({ ...; } | ... 12 more ... | { ...; })[], errorContext: { ...; }): void", "sourceLine": 641, "mutates": { "declaration": "appends a member when signatures are present;", "diagnostics": "adds `signature_analysis_failed` on checker error" }, "parameters": [ { "name": "getSignatures", "type": "() => readonly Signature[]", "description": "thunk to retrieve `getCallSignatures()` /\n`getConstructSignatures()`; called inside the try so checker errors are\ncaptured as diagnostics" }, { "name": "signatureKind", "type": "\"call\" | \"construct\"", "description": "`'call'` (member kind: function, includes returnType)\nor `'construct'` (member kind: constructor, no returnType)" }, { "name": "resolveTsdocNode", "type": "(sig: Signature) => Node | undefined", "description": "callback returning the AST node to parse TSDoc\nfrom, or `undefined` to skip TSDoc resolution" }, { "name": "paramValidationFallbackNode", "type": "Node", "description": "location used by `validateParamKeys`\nwhen `resolveTsdocNode` returns `undefined`" }, { "name": "declaration", "type": "DeclarationJsonBuild", "description": "parent declaration (mutated; appended to `members`)" }, { "name": "checker", "type": "TypeChecker" }, { "name": "diagnostics", "type": "({ symbolName: string; file: string; message: string; severity: \"error\" | \"warning\"; kind: \"type_extraction_failed\"; line?: number | undefined; column?: number | undefined; } | { functionName: string; ... 5 more ...; column?: number | undefined; } | ... 11 more ... | { ...; })[]" }, { "name": "errorContext", "type": "{ node: Node; kindLabel: string; }", "propertyDescriptions": { "node": "parent node used to locate diagnostics", "kindLabel": "`'interface'` or `'type'`, included in the\ndiagnostic message" } } ], "returnType": "void" } ], "moduleComment": "Shared utilities for the per-declaration extractors in `typescript-extract-*.ts`.\n\nHolds helpers used across function, type, and class extraction: signature\nparameter extraction, overload detection, generic parsing, modifier\nextraction, location reporting, intersection-property filtering, and runes\ndetection.\n\n@see `typescript-extract-function.ts`, `typescript-extract-type.ts`,\n `typescript-extract-class.ts` for the per-declaration extractors that\n build on these helpers", "dependencies": [ "declaration-build.ts", "diagnostics.ts", "tsdoc.ts", "types.ts", "typescript-program.ts" ], "dependents": [ "svelte.ts", "typescript-exports.ts", "typescript-extract-class.ts", "typescript-extract-function.ts", "typescript-extract-type-properties.ts", "typescript-extract-type.ts" ] }