Skip to content

Commit 58da597

Browse files
RYGRITghostdevv
andauthored
fix: separate class getters from methods in docs rendering (#1949)
Co-authored-by: Willow (GHOST) <git@willow.sh>
1 parent ee3ffaa commit 58da597

File tree

3 files changed

+196
-25
lines changed

3 files changed

+196
-25
lines changed

server/utils/docs/render.ts

Lines changed: 62 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -254,6 +254,33 @@ async function renderJsDocTags(tags: JsDocTag[], symbolLookup: SymbolLookup): Pr
254254
// Member Rendering
255255
// =============================================================================
256256

257+
type DefinitionListItem = {
258+
signature: string
259+
description?: string
260+
}
261+
262+
function renderMemberList(title: string, items: DefinitionListItem[]): string {
263+
const lines: string[] = []
264+
265+
if (items.length === 0) {
266+
return ''
267+
}
268+
269+
lines.push(`<div class="docs-members">`)
270+
lines.push(`<h4>${title}</h4>`)
271+
lines.push(`<dl>`)
272+
for (const item of items) {
273+
lines.push(`<dt><code>${escapeHtml(item.signature)}</code></dt>`)
274+
if (item.description) {
275+
lines.push(`<dd>${escapeHtml(item.description.split('\n')[0] ?? '')}</dd>`)
276+
}
277+
}
278+
lines.push(`</dl>`)
279+
lines.push(`</div>`)
280+
281+
return lines.join('\n')
282+
}
283+
257284
/**
258285
* Render class members (constructor, properties, methods).
259286
*/
@@ -272,44 +299,54 @@ function renderClassMembers(def: NonNullable<DenoDocNode['classDef']>): string {
272299
}
273300

274301
if (properties && properties.length > 0) {
275-
lines.push(`<div class="docs-members">`)
276-
lines.push(`<h4>Properties</h4>`)
277-
lines.push(`<dl>`)
278-
for (const prop of properties) {
302+
const propertyItems: DefinitionListItem[] = properties.map(prop => {
279303
const modifiers: string[] = []
280304
if (prop.isStatic) modifiers.push('static')
281305
if (prop.readonly) modifiers.push('readonly')
282306
const modStr = modifiers.length > 0 ? `${modifiers.join(' ')} ` : ''
283307
const type = formatType(prop.tsType)
284308
const opt = prop.optional ? '?' : ''
285-
lines.push(
286-
`<dt><code>${escapeHtml(modStr)}${escapeHtml(prop.name)}${opt}: ${escapeHtml(type)}</code></dt>`,
287-
)
288-
if (prop.jsDoc?.doc) {
289-
lines.push(`<dd>${escapeHtml(prop.jsDoc.doc.split('\n')[0] ?? '')}</dd>`)
309+
const typeStr = type ? `: ${type}` : ''
310+
311+
return {
312+
signature: `${modStr}${prop.name}${opt}${typeStr}`,
313+
description: prop.jsDoc?.doc,
290314
}
291-
}
292-
lines.push(`</dl>`)
293-
lines.push(`</div>`)
315+
})
316+
317+
lines.push(renderMemberList('Properties', propertyItems))
294318
}
295319

296-
if (methods && methods.length > 0) {
297-
lines.push(`<div class="docs-members">`)
298-
lines.push(`<h4>Methods</h4>`)
299-
lines.push(`<dl>`)
300-
for (const method of methods) {
320+
const getters = methods?.filter(m => m.kind === 'getter') || []
321+
const regularMethods = methods?.filter(m => m.kind !== 'getter') || []
322+
323+
if (getters.length > 0) {
324+
const getterItems: DefinitionListItem[] = getters.map(getter => {
325+
const ret = formatType(getter.functionDef?.returnType) || 'unknown'
326+
const staticStr = getter.isStatic ? 'static ' : ''
327+
328+
return {
329+
signature: `${staticStr}get ${getter.name}: ${ret}`,
330+
description: getter.jsDoc?.doc,
331+
}
332+
})
333+
334+
lines.push(renderMemberList('Getters', getterItems))
335+
}
336+
337+
if (regularMethods.length > 0) {
338+
const methodItems: DefinitionListItem[] = regularMethods.map(method => {
301339
const params = method.functionDef?.params?.map(p => formatParam(p)).join(', ') || ''
302340
const ret = formatType(method.functionDef?.returnType) || 'void'
303341
const staticStr = method.isStatic ? 'static ' : ''
304-
lines.push(
305-
`<dt><code>${escapeHtml(staticStr)}${escapeHtml(method.name)}(${escapeHtml(params)}): ${escapeHtml(ret)}</code></dt>`,
306-
)
307-
if (method.jsDoc?.doc) {
308-
lines.push(`<dd>${escapeHtml(method.jsDoc.doc.split('\n')[0] ?? '')}</dd>`)
342+
343+
return {
344+
signature: `${staticStr}${method.name}(${params}): ${ret}`,
345+
description: method.jsDoc?.doc,
309346
}
310-
}
311-
lines.push(`</dl>`)
312-
lines.push(`</div>`)
347+
})
348+
349+
lines.push(renderMemberList('Methods', methodItems))
313350
}
314351

315352
return lines.join('\n')

shared/types/deno-doc.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -100,6 +100,7 @@ export interface DenoDocNode {
100100
}>
101101
methods?: Array<{
102102
name: string
103+
kind?: 'method' | 'getter'
103104
isStatic?: boolean
104105
functionDef?: {
105106
params?: FunctionParam[]
Lines changed: 133 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,133 @@
1+
import { describe, expect, it } from 'vitest'
2+
import { renderDocNodes } from '../../../../../server/utils/docs/render'
3+
import type { DenoDocNode } from '#shared/types/deno-doc'
4+
import type { MergedSymbol } from '../../../../../server/utils/docs/types'
5+
6+
// =============================================================================
7+
// Issue #1943: class getters shown as methods
8+
// https://github.com/npmx-dev/npmx.dev/issues/1943
9+
// =============================================================================
10+
11+
function createClassSymbol(classDef: DenoDocNode['classDef']): MergedSymbol {
12+
const node: DenoDocNode = {
13+
name: 'TestClass',
14+
kind: 'class',
15+
classDef,
16+
}
17+
return {
18+
name: 'TestClass',
19+
kind: 'class',
20+
nodes: [node],
21+
}
22+
}
23+
24+
describe('issue #1943 - class getters separated from methods', () => {
25+
it('renders getters under a "Getters" heading, not "Methods"', async () => {
26+
const symbol = createClassSymbol({
27+
methods: [
28+
{
29+
name: 'clientId',
30+
kind: 'getter',
31+
functionDef: {
32+
returnType: { repr: 'string', kind: 'keyword', keyword: 'string' },
33+
},
34+
},
35+
],
36+
})
37+
38+
const html = await renderDocNodes([symbol], new Map())
39+
40+
expect(html).toContain('<h4>Getters</h4>')
41+
expect(html).toContain('get clientId')
42+
expect(html).not.toContain('<h4>Methods</h4>')
43+
})
44+
45+
it('renders regular methods under "Methods" heading', async () => {
46+
const symbol = createClassSymbol({
47+
methods: [
48+
{
49+
name: 'connect',
50+
kind: 'method',
51+
functionDef: {
52+
params: [],
53+
returnType: { repr: 'void', kind: 'keyword', keyword: 'void' },
54+
},
55+
},
56+
],
57+
})
58+
59+
const html = await renderDocNodes([symbol], new Map())
60+
61+
expect(html).toContain('<h4>Methods</h4>')
62+
expect(html).toContain('connect(')
63+
expect(html).not.toContain('<h4>Getters</h4>')
64+
})
65+
66+
it('renders both getters and methods in separate sections', async () => {
67+
const symbol = createClassSymbol({
68+
methods: [
69+
{
70+
name: 'clientId',
71+
kind: 'getter',
72+
functionDef: {
73+
returnType: { repr: 'string', kind: 'keyword', keyword: 'string' },
74+
},
75+
jsDoc: { doc: 'The client ID' },
76+
},
77+
{
78+
name: 'connect',
79+
kind: 'method',
80+
functionDef: {
81+
params: [
82+
{
83+
kind: 'identifier',
84+
name: 'url',
85+
tsType: { repr: 'string', kind: 'keyword', keyword: 'string' },
86+
},
87+
],
88+
returnType: { repr: 'void', kind: 'keyword', keyword: 'void' },
89+
},
90+
jsDoc: { doc: 'Connect to server' },
91+
},
92+
],
93+
})
94+
95+
const html = await renderDocNodes([symbol], new Map())
96+
97+
// Both sections should exist
98+
expect(html).toContain('<h4>Getters</h4>')
99+
expect(html).toContain('<h4>Methods</h4>')
100+
101+
// Getter should use "get" prefix without parentheses
102+
expect(html).toContain('get clientId')
103+
expect(html).toContain('The client ID')
104+
105+
// Method should have parentheses
106+
expect(html).toContain('connect(')
107+
expect(html).toContain('Connect to server')
108+
109+
// Getters section should appear before Methods section
110+
const gettersIndex = html.indexOf('<h4>Getters</h4>')
111+
const methodsIndex = html.indexOf('<h4>Methods</h4>')
112+
expect(gettersIndex).toBeLessThan(methodsIndex)
113+
})
114+
115+
it('renders static getter correctly', async () => {
116+
const symbol = createClassSymbol({
117+
methods: [
118+
{
119+
name: 'instance',
120+
kind: 'getter',
121+
isStatic: true,
122+
functionDef: {
123+
returnType: { repr: 'TestClass', kind: 'typeRef', typeRef: { typeName: 'TestClass' } },
124+
},
125+
},
126+
],
127+
})
128+
129+
const html = await renderDocNodes([symbol], new Map())
130+
131+
expect(html).toContain('static get instance')
132+
})
133+
})

0 commit comments

Comments
 (0)