From 0411de8298946c3d2326b99f27e08309c8b86584 Mon Sep 17 00:00:00 2001 From: Royston Shufflebotham Date: Sun, 18 Jun 2017 16:09:46 +0100 Subject: [PATCH] Add support for JsDoc tags (fix issue #28) The TypeScript compiler helpfully splits JsDoc comments into two, exposing the bulk of the text via symbol.getDocumentationComment() and the JsDoc tags via symbol.getJsDocTags(). react-docgen expects to be given the full JsDoc comment including the tags, so we need to reconstitute the full comment back again from the parts exposed by TypeScript. This also DRYs out the comment parsing, to make whitespace trimming consistent. --- src/__tests__/data/transformAST.tsx | 29 ++++++++++++++-- src/__tests__/data/transformAST_external.ts | 4 ++- src/__tests__/getFileDocumentation.spec.ts | 2 +- src/__tests__/transformAST.spec.ts | 15 ++++++-- src/printUtils.ts | 5 +++ src/transformAST.ts | 38 +++++++++++++++++---- 6 files changed, 80 insertions(+), 13 deletions(-) diff --git a/src/__tests__/data/transformAST.tsx b/src/__tests__/data/transformAST.tsx index 109ffea8..9f9d7302 100644 --- a/src/__tests__/data/transformAST.tsx +++ b/src/__tests__/data/transformAST.tsx @@ -8,7 +8,11 @@ export const exportedVar = 10; /** unexportedVarFunction comment */ const unexportedVarFunction = (param1: string): number => 0 ; -/** exportedVarFunction comment */ +/** exportedVarFunction comment + * + * @tag1 + * @tag2 partA partB partC + */ export const exportedVarFunction = (param1: number, param2: string): string => ""; function unexportedFunction(param1: number): string { @@ -22,8 +26,21 @@ function exportedFunction(param1: string, param2: number): number { interface UnexportedInterface { /** prop1 comment */ prop1: string; + + /** + * prop2 comment + * @tag1 + * @tag2 partA partB partC + */ + prop2?: string; } +/** + * Interface comment + * + * @tag1 + * @tag2 partA partB partC + */ export interface ExportedInterface { /** prop1 comment */ prop1: string; @@ -46,7 +63,10 @@ class UnexportedClass extends OurBaseClass { } } -/** ExportedClass comment */ +/** ExportedClass comment + * @tag1 partA partB + * @tag2 + */ export class ExportedClass { method1(): string { return ""; @@ -73,7 +93,10 @@ export const exportedExternalHoc1 = externalHoc(ExportedClass); /** exportedExternalHoc2 comment */ export const exportedExternalHoc2 = externalHoc(exportedFunction); -/** exported intersection type */ +/** exported intersection type + * @tag1 partA partB + * @tag2 + */ export type ExportedType1 = React.HTMLAttributes & { /** the first property */ prop1: "value1" | "value2"; diff --git a/src/__tests__/data/transformAST_external.ts b/src/__tests__/data/transformAST_external.ts index 2d9e23e8..4ba267c9 100644 --- a/src/__tests__/data/transformAST_external.ts +++ b/src/__tests__/data/transformAST_external.ts @@ -2,7 +2,9 @@ export interface ExternalInterfaceBase { prop1ExternalInterfaceBase: string; } -/** ExternalInterface comment */ +/** ExternalInterface comment + * @tag + */ export interface ExternalInterface extends ExternalInterfaceBase { /** prop1 comment */ prop1OnExternalInterface: string; diff --git a/src/__tests__/getFileDocumentation.spec.ts b/src/__tests__/getFileDocumentation.spec.ts index c4a6a2da..b55d84e9 100644 --- a/src/__tests__/getFileDocumentation.spec.ts +++ b/src/__tests__/getFileDocumentation.spec.ts @@ -272,7 +272,7 @@ describe('getFileDocumentation', () => { assert.isNotNull(r1.propInterface); const p1 = r1.propInterface; assert.equal(p1.name, 'Props'); - assert.equal(p1.comment, 'Props comment '); + assert.equal(p1.comment, 'Props comment'); assert.equal(p1.members.length, 2); assert.equal(p1.members[0].name, 'isFlippedX'); assert.equal(p1.members[0].comment, 'whether the image is flipped horizontally'); diff --git a/src/__tests__/transformAST.spec.ts b/src/__tests__/transformAST.spec.ts index cded3adc..96fc6a48 100644 --- a/src/__tests__/transformAST.spec.ts +++ b/src/__tests__/transformAST.spec.ts @@ -44,7 +44,7 @@ describe('transformAST', () => { assert.equal(r4.name, 'exportedVarFunction'); assert.equal(r4.exported, true); assert.equal(r4.kind, 'arrowFunction'); - assert.equal(r4.comment, 'exportedVarFunction comment'); + assert.equal(r4.comment, 'exportedVarFunction comment\n@tag1\n@tag2 partA partB partC'); assert.equal(r4.arrowFunctionType, 'string'); assert.deepEqual(r4.arrowFunctionParams, ['number', 'string']); @@ -98,10 +98,18 @@ describe('transformAST', () => { 'isOwn': true, 'comment': 'prop1 comment', 'values': [], + }, { + 'name': 'prop2', + 'type': 'string', + 'isRequired': false, + 'isOwn': true, + 'comment': 'prop2 comment\n@tag1\n@tag2 partA partB partC', + 'values': [], }]); const r2 = result[1]; assert.equal(r2.name, 'ExportedInterface'); + assert.equal(r2.comment, 'Interface comment\n@tag1\n@tag2 partA partB partC'); assert.equal(r2.exported, true); assert.deepEqual(r2.properties, [{ 'name': 'prop1', @@ -147,6 +155,7 @@ describe('transformAST', () => { const r4 = result[3]; assert.equal(r4.name, 'ExternalInterface'); assert.equal(r4.exported, true); + assert.equal(r4.comment, 'ExternalInterface comment\n@tag'); }); it('should provide data about classes', () => { @@ -161,7 +170,7 @@ describe('transformAST', () => { const r2 = result[2]; assert.equal(r2.name, 'ExportedClass'); assert.equal(r2.exported, true); - assert.equal(r2.comment, 'ExportedClass comment'); + assert.equal(r2.comment, 'ExportedClass comment\n@tag1 partA partB\n@tag2'); assert.deepEqual(r2.methods, [{name: 'method1'}, {name: 'method2'}]); const r4 = result[3]; @@ -175,6 +184,8 @@ describe('transformAST', () => { assert.equal(target.types.length, 1); const t1 = target.types[0]; assert.equal(t1.name, 'ExportedType1'); + assert.equal(t1.comment, 'exported intersection type\n@tag1 partA partB\n@tag2'); + // because ExportedType1 inherites from built in type and can // change over time we don't use exact number here assert.isTrue(t1.properties.length > 200); diff --git a/src/printUtils.ts b/src/printUtils.ts index 34311faf..a4003929 100644 --- a/src/printUtils.ts +++ b/src/printUtils.ts @@ -77,6 +77,11 @@ export function simplePrint(checker: ts.TypeChecker, node: ts.Node, indent = 0) if (comments.length > 0) { info.push(prefix + 'comment: \'' + comments.map(i => i.text).join('; ') + '\''); } + const jsdoctags = s.getJsDocTags(); + if (jsdoctags.length > 0) { + info.push(prefix + 'jsdoctags: \'' + + jsdoctags.map(i => `@${i.name} ${i.text}`).join('; ') + '\''); + } } if (node.kind === ts.SyntaxKind.FunctionDeclaration) { diff --git a/src/transformAST.ts b/src/transformAST.ts index 0111cfbd..75ffd43d 100644 --- a/src/transformAST.ts +++ b/src/transformAST.ts @@ -29,6 +29,32 @@ function isNodeExported(node: ts.Node): boolean { return false; } +/** + * Rebuilds a full JsDoc comment symbol, reconsitituting + * from the parts that TypeScript has broken it into. + */ +function getFullJsDocComment(symbol: ts.Symbol) { + if (!symbol) { + return ''; + } + + const mainComment = ts.displayPartsToString(symbol.getDocumentationComment()); + const tags = symbol.getJsDocTags() || []; + + // Transform { name: 'tag', text: 'text1 text2' } into + // '@tag text1 text2' + const tagComments = tags.map(t => { + let result = '@' + t.name; + if (t.text) { + result += ' ' + t.text; + } + return result; + }); + + const fullComment = mainComment + '\n' + tagComments.join('\n'); + return fullComment.trim(); +} + function getType(prop: ts.PropertySignature): MemberType { const unionType = prop.type as ts.UnionTypeNode; if (unionType && unionType.types) { @@ -75,7 +101,7 @@ function getProperties(checker: ts.TypeChecker, type: ts.Type, parent: ts.Node): type: typeInfo.type, values: typeInfo.values || [], isRequired: !prop.questionToken, - comment: ts.displayPartsToString(symbol.getDocumentationComment()).trim(), + comment: getFullJsDocComment(symbol), }; }); } @@ -145,7 +171,7 @@ export function transformAST(sourceFile: ts.SourceFile, checker: ts.TypeChecker) return { name: identifier.text, exported: isNodeExported(i), - comment: symbol ? ts.displayPartsToString(symbol.getDocumentationComment()).trim() : '', + comment: getFullJsDocComment(symbol), type: varType.symbol ? varType.symbol.getName() : 'unknown', kind, arrowFunctionType, @@ -163,7 +189,7 @@ export function transformAST(sourceFile: ts.SourceFile, checker: ts.TypeChecker) const type = checker.getTypeAtLocation(i.name); return { name: symbol.name, - comment: ts.displayPartsToString(symbol.getDocumentationComment()).trim(), + comment: getFullJsDocComment(symbol), exported: isNodeExported(i), properties: getProperties(checker, type, i), }; @@ -196,7 +222,7 @@ export function transformAST(sourceFile: ts.SourceFile, checker: ts.TypeChecker) name: i.name.getText(), properties, exported: isNodeExported(i), - comment: !symbol ? "" : ts.displayPartsToString(symbol.getDocumentationComment()) + comment: getFullJsDocComment(symbol) }; }); @@ -231,7 +257,7 @@ export function transformAST(sourceFile: ts.SourceFile, checker: ts.TypeChecker) // in that case we need to include the interface explicitly interfaces.push({ name: taType.symbol.name, - comment: ts.displayPartsToString(taType.symbol.getDocumentationComment()).trim(), + comment: getFullJsDocComment(taType.symbol), exported: true, // it has to be exported in order to be used, properties: getProperties(checker, taType, null), }); @@ -251,7 +277,7 @@ export function transformAST(sourceFile: ts.SourceFile, checker: ts.TypeChecker) name: symbol.name, exported: isNodeExported(i), baseType: baseType, - comment: ts.displayPartsToString(symbol.getDocumentationComment()).trim(), + comment: getFullJsDocComment(symbol), methods: getMethods(checker, type, i), }; });