Skip to content

Commit

Permalink
Add support for JsDoc tags (fix issue #28)
Browse files Browse the repository at this point in the history
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.
  • Loading branch information
Royston-Shufflebotham-i2 committed Jun 20, 2017
1 parent 737de76 commit 0411de8
Show file tree
Hide file tree
Showing 6 changed files with 80 additions and 13 deletions.
29 changes: 26 additions & 3 deletions src/__tests__/data/transformAST.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand All @@ -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;
Expand All @@ -46,7 +63,10 @@ class UnexportedClass extends OurBaseClass<ExportedInterface, {}> {
}
}

/** ExportedClass comment */
/** ExportedClass comment
* @tag1 partA partB
* @tag2
*/
export class ExportedClass {
method1(): string {
return "";
Expand All @@ -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<HTMLImageElement> & {
/** the first property */
prop1: "value1" | "value2";
Expand Down
4 changes: 3 additions & 1 deletion src/__tests__/data/transformAST_external.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,9 @@ export interface ExternalInterfaceBase {
prop1ExternalInterfaceBase: string;
}

/** ExternalInterface comment */
/** ExternalInterface comment
* @tag
*/
export interface ExternalInterface extends ExternalInterfaceBase {
/** prop1 comment */
prop1OnExternalInterface: string;
Expand Down
2 changes: 1 addition & 1 deletion src/__tests__/getFileDocumentation.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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');
Expand Down
15 changes: 13 additions & 2 deletions src/__tests__/transformAST.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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']);

Expand Down Expand Up @@ -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',
Expand Down Expand Up @@ -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', () => {
Expand All @@ -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];
Expand All @@ -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);
Expand Down
5 changes: 5 additions & 0 deletions src/printUtils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down
38 changes: 32 additions & 6 deletions src/transformAST.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down Expand Up @@ -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),
};
});
}
Expand Down Expand Up @@ -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,
Expand All @@ -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),
};
Expand Down Expand Up @@ -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)
};
});

Expand Down Expand Up @@ -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),
});
Expand All @@ -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),
};
});
Expand Down

0 comments on commit 0411de8

Please sign in to comment.