Skip to content

Commit f6c1103

Browse files
committed
feat: enum
1 parent 2652c36 commit f6c1103

File tree

7 files changed

+195
-57
lines changed

7 files changed

+195
-57
lines changed

.idea/dictionaries/develar.xml

Lines changed: 7 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
{
22
"name": "ts-jsdoc",
33
"description": "Transform TypeScript to JSDoc annotated JS code",
4-
"version": "1.0.7",
4+
"version": "1.1.0",
55
"license": "MIT",
66
"bin": {
77
"ts2jsdoc": "out/ts2jsdoc.js"

src/JsDocGenerator.ts

Lines changed: 92 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ import * as path from "path"
33
import { emptyDir, readdir, readFile, readJson, writeFile } from "fs-extra-p"
44
import { JsDocRenderer } from "./JsDocRenderer"
55
import { checkErrors, processTree } from "./util"
6-
import { Class, Member, MethodDescriptor, Property, SourceFileDescriptor, SourceFileModuleInfo, Variable } from "./psi"
6+
import { Class, Descriptor, Member, MethodDescriptor, Property, SourceFileDescriptor, SourceFileModuleInfo, Type, Variable } from "./psi"
77
import BluebirdPromise from "bluebird-lst"
88

99
export interface TsToJsdocOptions {
@@ -44,7 +44,7 @@ export async function generateAndWrite(basePath: string, config: ts.ParsedComman
4444
continue
4545
}
4646

47-
moveMember(psi.functions, mainPsi.functions, name) || moveMember(psi.variables, mainPsi.variables, name)
47+
moveMember(psi.functions, mainPsi.functions, name) || moveMember(psi.members, mainPsi.members, name)
4848
}
4949
}
5050

@@ -53,12 +53,6 @@ export async function generateAndWrite(basePath: string, config: ts.ParsedComman
5353
const existingClassExampleDirs = exampleDir == null ? null : new Set((await readdir(exampleDir)).filter(it => it[0] != "." && !it.includes(".")))
5454

5555
for (const [moduleId, psi] of moduleNameToResult.entries()) {
56-
let result = ""
57-
const externalToModuleName = new Map<string, string>()
58-
for (const d of copyAndSort(psi.variables)) {
59-
result += generator.renderer.renderVariable(d)
60-
}
61-
6256
const modulePathMapper: ModulePathMapper = oldPath => {
6357
if (!oldPath.startsWith("module:")) {
6458
return oldPath
@@ -81,6 +75,17 @@ export async function generateAndWrite(basePath: string, config: ts.ParsedComman
8175
return oldPath
8276
}
8377

78+
let result = ""
79+
const externalToModuleName = new Map<string, string>()
80+
for (const d of copyAndSort(psi.members)) {
81+
if ((<any>d).kind == null) {
82+
result += generator.renderer.renderVariable(<Variable>d, modulePathMapper)
83+
}
84+
else {
85+
result += generator.renderer.renderMember(<Descriptor>d)
86+
}
87+
}
88+
8489
for (const d of copyAndSort(psi.classes)) {
8590
let examples: Array<Example> = []
8691
if (existingClassExampleDirs != null && existingClassExampleDirs.has(d.name)) {
@@ -215,7 +220,7 @@ export class JsDocGenerator {
215220

216221
const classes: Array<Class> = []
217222
const functions: Array<MethodDescriptor> = []
218-
const variables: Array<Variable> = []
223+
const members: Array<Variable | Descriptor> = []
219224

220225
processTree(sourceFile, (node) => {
221226
if (node.kind === ts.SyntaxKind.InterfaceDeclaration || node.kind === ts.SyntaxKind.ClassDeclaration) {
@@ -240,20 +245,26 @@ export class JsDocGenerator {
240245
else if (node.kind === ts.SyntaxKind.VariableStatement) {
241246
const descriptor = this.describeVariable(<ts.VariableStatement>node)
242247
if (descriptor != null) {
243-
variables.push(descriptor)
248+
members.push(descriptor)
249+
}
250+
}
251+
else if (node.kind === ts.SyntaxKind.EnumDeclaration) {
252+
const descriptor = this.describeEnum(<ts.EnumDeclaration>node)
253+
if (descriptor != null) {
254+
members.push(descriptor)
244255
}
245256
}
246257
return true
247258
})
248259

249260
const existingPsi = this.moduleNameToResult.get(moduleId.id)
250261
if (existingPsi == null) {
251-
this.moduleNameToResult.set(moduleId.id, {classes, functions, variables})
262+
this.moduleNameToResult.set(moduleId.id, {classes, functions, members})
252263
}
253264
else {
254265
existingPsi.classes.push(...classes)
255266
existingPsi.functions.push(...functions)
256-
existingPsi.variables.push(...variables)
267+
existingPsi.members.push(...members)
257268
}
258269
}
259270

@@ -292,7 +303,7 @@ export class JsDocGenerator {
292303
this.mainMappings.set(this.sourceFileToModuleId(sourceFile).id, names)
293304
}
294305

295-
getTypeNamePathByNode(node: ts.Node): Array<string> | null {
306+
getTypeNamePathByNode(node: ts.Node): Array<string | Type> | null {
296307
if (node.kind === ts.SyntaxKind.UnionType) {
297308
return this.typesToList((<ts.UnionType>(<any>node)).types, node)
298309
}
@@ -318,33 +329,46 @@ export class JsDocGenerator {
318329
const text = (<ts.LiteralLikeNode>(<any>(<ts.LiteralTypeNode>node).literal)).text
319330
return [`"${text}"`]
320331
}
332+
else if (node.kind === ts.SyntaxKind.TypeLiteral) {
333+
// todo
334+
return ['Object.<string, any>']
335+
}
321336

322337
const type = this.program.getTypeChecker().getTypeAtLocation(node)
323-
if (type == null) {
324-
return null
338+
return type == null ? null : this.getTypeNames(type, node)
339+
}
340+
341+
private typesToList(types: Array<ts.Type>, node: ts.Node) {
342+
const typeNames: Array<string | Type> = []
343+
for (const type of types) {
344+
const name = (<any>type).kind == null ? [this.getTypeNamePath(<any>type)] : this.getTypeNamePathByNode(<any>type)
345+
if (name == null) {
346+
throw new Error("cannot get name for " + node.getText(node.getSourceFile()))
347+
}
348+
typeNames.push(...name)
325349
}
350+
return typeNames
351+
}
326352

353+
getTypeNames(type: ts.Type, node: ts.Node): Array<string | Type> | null {
327354
if (type.flags & ts.TypeFlags.UnionOrIntersection && !(type.flags & ts.TypeFlags.Enum)) {
328355
return this.typesToList((<ts.UnionOrIntersectionType>type).types, node)
329356
}
330-
357+
331358
let result = this.getTypeNamePath(type)
332359
if (result == null) {
333360
throw new Error("Cannot infer getTypeNamePath")
334361
}
335-
return [result]
336-
}
337362

338-
private typesToList(types: Array<ts.Type>, node: ts.Node) {
339-
const typeNames: Array<string> = []
340-
for (const type of types) {
341-
const name = (<any>type).kind == null ? [this.getTypeNamePath(<any>type)] : this.getTypeNamePathByNode(<any>type)
342-
if (name == null) {
343-
throw new Error("cannot get name for " + node.getText(node.getSourceFile()))
363+
const typeArguments = (<ts.TypeReference>type).typeArguments
364+
if (typeArguments != null) {
365+
const subTypes = []
366+
for (const type of typeArguments) {
367+
subTypes.push(...this.getTypeNames(type, node))
344368
}
345-
typeNames.push(...name)
369+
return [{name: result, subTypes: subTypes}]
346370
}
347-
return typeNames
371+
return [result]
348372
}
349373

350374
getTypeNamePath(type: ts.Type): string | null {
@@ -407,6 +431,47 @@ export class JsDocGenerator {
407431
console.warn(`Cannot find parent for ${symbol}`)
408432
return null
409433
}
434+
435+
private describeEnum(node: ts.EnumDeclaration): Descriptor {
436+
const flags = ts.getCombinedModifierFlags(node)
437+
if (!(flags & ts.ModifierFlags.Export)) {
438+
return null
439+
}
440+
441+
const type = {
442+
names: ["number"]
443+
}
444+
445+
const name = (<ts.Identifier>node.name).text
446+
const moduleId = this.computeTypePath()
447+
const id = `${moduleId}.${name}`
448+
449+
const properties: Array<Descriptor> = []
450+
for (const member of node.members) {
451+
const name = (<ts.Identifier>member.name).text
452+
properties.push({
453+
name: name,
454+
kind: "member",
455+
scope: "static",
456+
memberof: id,
457+
type: type,
458+
})
459+
}
460+
461+
// we don't set readonly because it is clear that enum is not mutable
462+
// e.g. jsdoc2md wil add useless "Read only: true"
463+
return {
464+
node: node,
465+
id: id,
466+
name: name,
467+
longname: id,
468+
kind: "enum",
469+
scope: "static",
470+
memberof: moduleId,
471+
type: type,
472+
properties: properties,
473+
}
474+
}
410475

411476
private describeVariable(node: ts.VariableStatement): Variable {
412477
const flags = ts.getCombinedModifierFlags(node)
@@ -456,7 +521,7 @@ export class JsDocGenerator {
456521
const className = (<ts.Identifier>nodeDeclaration.name).text
457522

458523
const clazz = <ts.ClassDeclaration>node
459-
let parents: Array<string> = []
524+
let parents: Array<string | Type> = []
460525
if (clazz.heritageClauses != null) {
461526
for (const heritageClause of clazz.heritageClauses) {
462527
if (heritageClause.types != null) {

src/JsDocRenderer.ts

Lines changed: 60 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import * as ts from "typescript"
22
import * as path from "path"
33
import { Example, JsDocGenerator, ModulePathMapper } from "./JsDocGenerator"
44
import { parse as parseJsDoc, Tag } from "doctrine"
5-
import { Class, MethodDescriptor, Property, Variable } from "./psi"
5+
import { Class, Descriptor, MethodDescriptor, Property, Type, Variable } from "./psi"
66

77
export class JsDocRenderer {
88
indent: string = ""
@@ -54,7 +54,8 @@ export class JsDocRenderer {
5454
}
5555

5656
for (const parent of descriptor.parents) {
57-
tags.push(`@extends ${modulePathMapper(parent)}`)
57+
// ignore <> type params because JsDoc expects namepath, but not type expression
58+
tags.push(`@extends ${renderType(parent, modulePathMapper, true)}`)
5859
}
5960

6061
JsDocRenderer.renderProperties(descriptor.properties, tags, modulePathMapper)
@@ -96,7 +97,7 @@ export class JsDocRenderer {
9697
returns = tag
9798
}
9899
else {
99-
tags.push(`@${tag.title} ${tag.description}`)
100+
tags.push(printTag(tag))
100101
}
101102
}
102103
}
@@ -112,17 +113,17 @@ export class JsDocRenderer {
112113

113114
const tag = paramNameToInfo.get(name)
114115
text += ` ${name}`
115-
if (tag != null) {
116+
if (tag != null && tag.description != null) {
116117
text += ` ${tag.description}`
117118
}
118119
tags.push(text)
119120
}
120121

121122
const signature = this.generator.program.getTypeChecker().getSignatureFromDeclaration(method.node)
122-
const returnType = this.generator.getTypeNamePath(signature.getReturnType())
123+
const returnTypes = this.generator.getTypeNames(signature.getReturnType(), method.node)
123124
// http://stackoverflow.com/questions/4759175/how-to-return-void-in-jsdoc
124-
if (returnType !== "void") {
125-
let text = `@returns ${renderTypes([returnType])}`
125+
if (!returnTypes.includes("void")) {
126+
let text = `@returns ${renderTypes(returnTypes, modulePathMapper)}`
126127
if (returns != null) {
127128
text += ` ${returns.description}`
128129
}
@@ -152,10 +153,10 @@ export class JsDocRenderer {
152153
return null
153154
}
154155

155-
renderVariable(descriptor: Variable): string {
156+
renderVariable(descriptor: Variable, modulePathMapper: ModulePathMapper): string {
156157
this.indent = ""
157158

158-
const tags = [`@type ${renderTypes(descriptor.types)}`]
159+
const tags = [`@type ${renderTypes(descriptor.types, modulePathMapper)}`]
159160

160161
if (descriptor.isConst) {
161162
tags.push("@constant")
@@ -166,6 +167,23 @@ export class JsDocRenderer {
166167
result += `export var ${descriptor.name}\n`
167168
return result
168169
}
170+
171+
renderMember(descriptor: Descriptor) {
172+
const tags = [
173+
"@enum {number}"
174+
]
175+
176+
if (descriptor.readonly) {
177+
tags.push("@readonly")
178+
}
179+
for (const property of descriptor.properties) {
180+
tags.push(`@property ${property.name}`)
181+
}
182+
183+
let result = this.formatComment(descriptor.node!, tags)
184+
result += `export var ${descriptor.name}\n`
185+
return result
186+
}
169187

170188
// form http://stackoverflow.com/questions/10490713/how-to-document-the-properties-of-the-object-in-the-jsdoc-3-tag-this
171189
// doesn't produce properties table, so, we use property tags
@@ -246,26 +264,44 @@ function parseExistingJsDoc(node: ts.Node, tags: Array<string>): string | null {
246264
const parsed = existingJsDoc == null ? null : parseJsDoc(existingJsDoc, {unwrap: true})
247265
if (parsed != null) {
248266
for (const tag of parsed.tags) {
249-
let text = `@${tag.title}`
250-
251-
const caption = (<any>tag).caption
252-
if (caption != null) {
253-
text += ` <caption>${caption}</caption>`
254-
}
255-
256-
if (tag.description != null) {
257-
text += ` ${tag.description}`
258-
}
259-
tags.push(text)
267+
tags.push(printTag(tag))
260268
}
261269
}
262270

263271
return parsed == null ? null : parsed.description
264272
}
265273

266-
function renderTypes(names: Array<string>, modulePathMapper?: ModulePathMapper) {
267-
if (modulePathMapper != null) {
268-
names = names.map(it => modulePathMapper(it) || it)
274+
function printTag(tag: Tag) {
275+
let text = `@${tag.title}`
276+
277+
const caption = (<any>tag).caption
278+
if (caption != null) {
279+
text += ` <caption>${caption}</caption>`
280+
}
281+
282+
if (tag.description != null) {
283+
text += ` ${tag.description}`
284+
}
285+
return text
286+
}
287+
288+
// (oldPath: string) => oldPath
289+
290+
function renderTypes(names: Array<string | Type>, modulePathMapper: ModulePathMapper): string {
291+
return `{${_renderTypes(names, modulePathMapper)}}`
292+
}
293+
294+
function _renderTypes(names: Array<string | Type>, modulePathMapper: ModulePathMapper): string {
295+
return names.map(it => renderType(it, modulePathMapper)).join(" | ")
296+
}
297+
298+
function renderType(name: string | Type, modulePathMapper: ModulePathMapper, ignoreSubtypes = false): string {
299+
if (typeof name === "string") {
300+
return modulePathMapper(name)
301+
}
302+
const type = <Type>name
303+
if (ignoreSubtypes) {
304+
return modulePathMapper(type.name)
269305
}
270-
return `{${names.join(" | ")}}`
306+
return modulePathMapper(type.name) + "<" + _renderTypes(type.subTypes, modulePathMapper) + ">"
271307
}

0 commit comments

Comments
 (0)