diff --git a/server/utils/docs/format.ts b/server/utils/docs/format.ts index 0027d6e7c6..68d30044ac 100644 --- a/server/utils/docs/format.ts +++ b/server/utils/docs/format.ts @@ -19,7 +19,7 @@ export function getNodeSignature(node: DenoDocNode): string | null { case 'function': { const typeParams = node.functionDef?.typeParams?.map(t => t.name).join(', ') const typeParamsStr = typeParams ? `<${typeParams}>` : '' - const params = node.functionDef?.params?.map(p => formatParam(p)).join(', ') || '' + const params = formatParams(node.functionDef?.params) const ret = formatType(node.functionDef?.returnType) || 'void' const asyncStr = node.functionDef?.isAsync ? 'async ' : '' return `${asyncStr}function ${name}${typeParamsStr}(${params}): ${ret}` @@ -60,10 +60,18 @@ export function getNodeSignature(node: DenoDocNode): string | null { /** * Format a function parameter. */ -export function formatParam(param: FunctionParam): string { +export function formatParam(param: FunctionParam, index = 0): string { + const name = param.name || `arg_${index}` const optional = param.optional ? '?' : '' const type = formatType(param.tsType) - return type ? `${param.name}${optional}: ${type}` : `${param.name}${optional}` + return type ? `${name}${optional}: ${type}` : `${name}${optional}` +} + +/** + * Format a function parameter list. + */ +export function formatParams(params?: FunctionParam[]): string { + return params?.map((param, index) => formatParam(param, index)).join(', ') || '' } /** @@ -116,7 +124,7 @@ const TYPE_FORMATTERS: Partial string>> function formatFnOrConstructorType(fn: NonNullable): string { const typeParams = fn.typeParams?.map(t => t.name).join(', ') const typeParamsStr = typeParams ? `<${typeParams}>` : '' - const params = fn.params.map(p => formatParam(p)).join(', ') + const params = formatParams(fn.params) const ret = formatType(fn.tsType) || 'void' return `${typeParamsStr}(${params}) => ${ret}` } @@ -129,7 +137,7 @@ function formatTypeLiteralType(lit: NonNullable): string parts.push(`${ro}${prop.name}${opt}: ${formatType(prop.tsType) || 'unknown'}`) } for (const method of lit.methods) { - const params = method.params?.map(p => formatParam(p)).join(', ') || '' + const params = formatParams(method.params) const ret = formatType(method.returnType) || 'void' parts.push(`${method.name}(${params}): ${ret}`) } diff --git a/server/utils/docs/render.ts b/server/utils/docs/render.ts index a70f856551..aa98bee960 100644 --- a/server/utils/docs/render.ts +++ b/server/utils/docs/render.ts @@ -8,7 +8,7 @@ import type { DenoDocNode, JsDocTag } from '#shared/types/deno-doc' import { highlightCodeBlock } from '../shiki' -import { formatParam, formatType, getNodeSignature } from './format' +import { formatParams, formatType, getNodeSignature } from './format' import { groupMergedByKind } from './processing' import { escapeHtml, createSymbolId, parseJsDocLinks, renderMarkdown } from './text' import type { MergedSymbol, SymbolLookup } from './types' @@ -306,7 +306,7 @@ function renderClassMembers(def: NonNullable): string { lines.push(`
`) lines.push(`

Constructor

`) for (const ctor of constructors) { - const params = ctor.params?.map(p => formatParam(p)).join(', ') || '' + const params = formatParams(ctor.params) lines.push(`
constructor(${escapeHtml(params)})
`) } lines.push(`
`) @@ -350,7 +350,7 @@ function renderClassMembers(def: NonNullable): string { if (regularMethods.length > 0) { const methodItems: DefinitionListItem[] = regularMethods.map(method => { - const params = method.functionDef?.params?.map(p => formatParam(p)).join(', ') || '' + const params = formatParams(method.functionDef?.params) const ret = formatType(method.functionDef?.returnType) || 'void' const staticStr = method.isStatic ? 'static ' : '' @@ -397,7 +397,7 @@ function renderInterfaceMembers(def: NonNullable): lines.push(`

Methods

`) lines.push(`
`) for (const method of methods) { - const params = method.params?.map(p => formatParam(p)).join(', ') || '' + const params = formatParams(method.params) const ret = formatType(method.returnType) || 'void' lines.push( `
${escapeHtml(method.name)}(${escapeHtml(params)}): ${escapeHtml(ret)}
`, diff --git a/shared/types/deno-doc.ts b/shared/types/deno-doc.ts index 80a30b2b97..403bbc7fb5 100644 --- a/shared/types/deno-doc.ts +++ b/shared/types/deno-doc.ts @@ -61,7 +61,7 @@ export interface TsType { /** Function parameter from deno doc */ export interface FunctionParam { kind: string - name: string + name?: string optional?: boolean tsType?: TsType } diff --git a/test/fixtures/esm-sh/doc-nodes/tanstack-highlight@0.0.9-create-highlighter.json b/test/fixtures/esm-sh/doc-nodes/tanstack-highlight@0.0.9-create-highlighter.json new file mode 100644 index 0000000000..d438d602b0 --- /dev/null +++ b/test/fixtures/esm-sh/doc-nodes/tanstack-highlight@0.0.9-create-highlighter.json @@ -0,0 +1,73 @@ +{ + "name": "createHighlighter", + "isDefault": false, + "kind": "function", + "functionDef": { + "params": [ + { + "kind": "object", + "props": [ + { + "kind": "assign", + "key": "fallbackLanguage" + }, + { + "kind": "assign", + "key": "languages" + } + ], + "optional": false, + "tsType": { + "repr": "", + "kind": "typeLiteral", + "typeLiteral": { + "properties": [ + { + "name": "fallbackLanguage", + "optional": true, + "tsType": { + "repr": "string", + "kind": "keyword", + "keyword": "string" + } + }, + { + "name": "languages", + "optional": false, + "tsType": { + "repr": "ReadonlyArray", + "kind": "typeRef", + "typeRef": { + "typeParams": [ + { + "repr": "LanguageDefinition", + "kind": "typeRef", + "typeRef": { + "typeName": "LanguageDefinition" + } + } + ], + "typeName": "ReadonlyArray" + } + } + } + ], + "methods": [], + "callSignatures": [], + "indexSignatures": [] + } + } + } + ], + "returnType": { + "repr": "Highlighter", + "kind": "typeRef", + "typeRef": { + "typeName": "Highlighter" + } + }, + "isAsync": false, + "isGenerator": false, + "typeParams": [] + } +} diff --git a/test/unit/server/utils/docs/format.spec.ts b/test/unit/server/utils/docs/format.spec.ts index 07319ba6b4..74fba21f2b 100644 --- a/test/unit/server/utils/docs/format.spec.ts +++ b/test/unit/server/utils/docs/format.spec.ts @@ -1,7 +1,7 @@ import { readFileSync } from 'node:fs' import { resolve } from 'node:path' import { describe, expect, it } from 'vitest' -import { formatType, getNodeSignature } from '#server/utils/docs/format' +import { formatParams, formatType, getNodeSignature } from '#server/utils/docs/format' import type { DenoDocNode } from '#shared/types/deno-doc' function loadFixture(name: string): DenoDocNode { @@ -84,3 +84,29 @@ describe('issue #1411 - linkdave@0.0.2 unknown types', () => { expect(pickSig).not.toContain('= unknown') }) }) + +describe('anonymous function parameters', () => { + it('uses a positional fallback for a destructured parameter', () => { + const node = loadFixture('tanstack-highlight@0.0.9-create-highlighter.json') + + expect(getNodeSignature(node)).toBe( + 'function createHighlighter(arg_0: { fallbackLanguage?: string; languages: ReadonlyArray }): Highlighter', + ) + }) + + it('uses the zero-based parameter index in fallback names', () => { + expect( + formatParams([ + { + kind: 'identifier', + name: 'value', + tsType: { repr: 'string', kind: 'keyword', keyword: 'string' }, + }, + { + kind: 'object', + tsType: { repr: 'object', kind: 'keyword', keyword: 'object' }, + }, + ]), + ).toBe('value: string, arg_1: object') + }) +})