Skip to content

Commit 61883cf

Browse files
committed
Support JSDocs on variable declarations
1 parent c9eea9c commit 61883cf

File tree

7 files changed

+236
-24
lines changed

7 files changed

+236
-24
lines changed

src/parser.ts

Lines changed: 44 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -168,7 +168,7 @@ export function parseFromProgram(
168168
function visit(node: ts.Node) {
169169
// function x(props: type) { return <div/> }
170170
if (ts.isFunctionDeclaration(node) && node.name && node.parameters.length === 1) {
171-
parseFunctionComponent(node);
171+
parseFunctionComponent(node, node);
172172
}
173173
// const x = ...
174174
else if (ts.isVariableStatement(node)) {
@@ -193,14 +193,14 @@ export function parseFromProgram(
193193
node.getSourceFile(),
194194
);
195195
} else if (checkDeclarations) {
196-
parseFunctionComponent(variableNode);
196+
parseFunctionComponent(variableNode, node);
197197
}
198198
} else if (
199199
(ts.isArrowFunction(variableNode.initializer) ||
200200
ts.isFunctionExpression(variableNode.initializer)) &&
201201
variableNode.initializer.parameters.length === 1
202202
) {
203-
parseFunctionComponent(variableNode);
203+
parseFunctionComponent(variableNode, node);
204204
}
205205
// x = react.memo((props:type) { return <div/> })
206206
else if (
@@ -216,7 +216,12 @@ export function parseFromProgram(
216216
) {
217217
const propsType = checker.getTypeAtLocation(arg.parameters[0]);
218218
if (propsType) {
219-
parseComponentProps(variableNode.name.getText(), propsType, node.getSourceFile());
219+
parseComponentProps(
220+
variableNode.name.getText(),
221+
propsType,
222+
node.getSourceFile(),
223+
node,
224+
);
220225
}
221226
}
222227
}
@@ -261,7 +266,10 @@ export function parseFromProgram(
261266
return false;
262267
}
263268

264-
function parseFunctionComponent(node: ts.VariableDeclaration | ts.FunctionDeclaration) {
269+
function parseFunctionComponent(
270+
node: ts.VariableDeclaration | ts.FunctionDeclaration,
271+
documentationNode: ts.Node,
272+
) {
265273
if (!node.name) {
266274
return;
267275
}
@@ -344,13 +352,18 @@ export function parseFromProgram(
344352
}
345353
return propType;
346354
}),
347-
getDocumentation(symbol),
355+
getDocumentationFromNode(documentationNode),
348356
node.getSourceFile().fileName,
349357
),
350358
);
351359
}
352360

353-
function parseComponentProps(name: string, type: ts.Type, sourceFile: ts.SourceFile | undefined) {
361+
function parseComponentProps(
362+
name: string,
363+
type: ts.Type,
364+
sourceFile: ts.SourceFile | undefined,
365+
documentationNode: ts.Node | undefined = undefined,
366+
) {
354367
let allProperties: ts.Symbol[];
355368
if (type.isUnion()) {
356369
allProperties = type.types.flatMap((x) => x.getProperties());
@@ -368,11 +381,15 @@ export function parseFromProgram(
368381

369382
const propsFilename = sourceFile !== undefined ? sourceFile.fileName : undefined;
370383

384+
const docs = documentationNode
385+
? getDocumentationFromNode(documentationNode)
386+
: getDocumentationFromSymbol(checker.getSymbolAtLocation(type.symbol?.valueDeclaration!));
387+
371388
programNode.body.push(
372389
t.componentNode(
373390
name,
374391
filteredProperties.map((x) => checkSymbol(x, new Set([(type as any).id]))),
375-
getDocumentation(checker.getSymbolAtLocation(type.symbol?.valueDeclaration!)),
392+
docs,
376393
propsFilename,
377394
),
378395
);
@@ -411,7 +428,7 @@ export function parseFromProgram(
411428

412429
return t.propNode(
413430
symbol.getName(),
414-
getDocumentation(symbol),
431+
getDocumentationFromSymbol(symbol),
415432
elementNode,
416433
!!declaration.questionToken,
417434
symbolFilenames,
@@ -456,7 +473,7 @@ export function parseFromProgram(
456473

457474
return t.propNode(
458475
symbol.getName(),
459-
getDocumentation(symbol),
476+
getDocumentationFromSymbol(symbol),
460477
parsedType,
461478
Boolean(declaration && ts.isPropertySignature(declaration) && declaration.questionToken),
462479
symbolFilenames,
@@ -543,7 +560,7 @@ export function parseFromProgram(
543560
if (type.isLiteral()) {
544561
return t.literalNode(
545562
type.isStringLiteral() ? `"${type.value}"` : type.value,
546-
getDocumentation(type.symbol)?.description,
563+
getDocumentationFromSymbol(type.symbol)?.description,
547564
);
548565
}
549566
return t.literalNode(checker.typeToString(type));
@@ -623,31 +640,34 @@ export function parseFromProgram(
623640
return t.simpleTypeNode('any');
624641
}
625642

626-
function getDocumentation(symbol?: ts.Symbol): Documentation | undefined {
643+
function getDocumentationFromSymbol(symbol?: ts.Symbol): Documentation | undefined {
627644
if (!symbol) {
628645
return undefined;
629646
}
630647

631648
const decl = symbol.getDeclarations();
632649
if (decl) {
633-
// @ts-ignore - Private method
634-
const comments = ts.getJSDocCommentsAndTags(decl[0]) as any[];
635-
if (comments && comments.length === 1) {
636-
const commentNode = comments[0];
637-
if (ts.isJSDoc(commentNode)) {
638-
return {
639-
description: commentNode.comment as string | undefined,
640-
defaultValue: commentNode.tags?.find((t) => t.tagName.text === 'default')?.comment,
641-
visibility: getVisibilityFromJSDoc(commentNode),
642-
};
643-
}
644-
}
650+
return getDocumentationFromNode(decl[0]);
645651
}
646652

647653
const comment = ts.displayPartsToString(symbol.getDocumentationComment(checker));
648654
return comment ? { description: comment } : undefined;
649655
}
650656

657+
function getDocumentationFromNode(node: ts.Node): Documentation | undefined {
658+
const comments = ts.getJSDocCommentsAndTags(node);
659+
if (comments && comments.length === 1) {
660+
const commentNode = comments[0];
661+
if (ts.isJSDoc(commentNode)) {
662+
return {
663+
description: commentNode.comment as string | undefined,
664+
defaultValue: commentNode.tags?.find((t) => t.tagName.text === 'default')?.comment,
665+
visibility: getVisibilityFromJSDoc(commentNode),
666+
};
667+
}
668+
}
669+
}
670+
651671
function getSymbolFileNames(symbol: ts.Symbol): Set<string> {
652672
const declarations = symbol.getDeclarations() || [];
653673

test/forwardRef/input.tsx

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
import * as React from 'react';
2+
3+
/**
4+
* A test component
5+
*/
6+
export const TestComponent = React.forwardRef(function TestComponent(
7+
props: Props,
8+
ref: React.ForwardedRef<HTMLDivElement>,
9+
) {
10+
return <div {...props} ref={ref} />;
11+
});
12+
13+
interface Props {
14+
className?: string;
15+
id?: string;
16+
}

test/forwardRef/output.json

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
{
2+
"nodeType": "program",
3+
"body": [
4+
{
5+
"nodeType": "component",
6+
"name": "TestComponent",
7+
"description": "A test component",
8+
"props": [
9+
{
10+
"nodeType": "prop",
11+
"name": "className",
12+
"propType": {
13+
"nodeType": "union",
14+
"types": [
15+
{
16+
"nodeType": "simpleType",
17+
"typeName": "undefined"
18+
},
19+
{
20+
"nodeType": "simpleType",
21+
"typeName": "string"
22+
}
23+
]
24+
},
25+
"optional": true,
26+
"filenames": {}
27+
},
28+
{
29+
"nodeType": "prop",
30+
"name": "id",
31+
"propType": {
32+
"nodeType": "union",
33+
"types": [
34+
{
35+
"nodeType": "simpleType",
36+
"typeName": "undefined"
37+
},
38+
{
39+
"nodeType": "simpleType",
40+
"typeName": "string"
41+
}
42+
]
43+
},
44+
"optional": true,
45+
"filenames": {}
46+
}
47+
]
48+
}
49+
]
50+
}

test/function-variable/input.tsx

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
import * as React from 'react';
2+
3+
/**
4+
* A test component
5+
*/
6+
export const TestComponent = function TestComponent(props: Props): React.ReactElement {
7+
return <div {...props} />;
8+
};
9+
10+
interface Props {
11+
className?: string;
12+
id?: string;
13+
}

test/function-variable/output.json

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
{
2+
"nodeType": "program",
3+
"body": [
4+
{
5+
"nodeType": "component",
6+
"name": "TestComponent",
7+
"description": "A test component",
8+
"props": [
9+
{
10+
"nodeType": "prop",
11+
"name": "className",
12+
"propType": {
13+
"nodeType": "union",
14+
"types": [
15+
{
16+
"nodeType": "simpleType",
17+
"typeName": "undefined"
18+
},
19+
{
20+
"nodeType": "simpleType",
21+
"typeName": "string"
22+
}
23+
]
24+
},
25+
"optional": true,
26+
"filenames": {}
27+
},
28+
{
29+
"nodeType": "prop",
30+
"name": "id",
31+
"propType": {
32+
"nodeType": "union",
33+
"types": [
34+
{
35+
"nodeType": "simpleType",
36+
"typeName": "undefined"
37+
},
38+
{
39+
"nodeType": "simpleType",
40+
"typeName": "string"
41+
}
42+
]
43+
},
44+
"optional": true,
45+
"filenames": {}
46+
}
47+
]
48+
}
49+
]
50+
}

test/memo/input.tsx

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
import * as React from 'react';
2+
3+
/**
4+
* A test component
5+
*/
6+
export const TestComponent = React.memo(function TestComponent(props: Props) {
7+
return <div {...props} />;
8+
});
9+
10+
interface Props {
11+
className?: string;
12+
id?: string;
13+
}

test/memo/output.json

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
{
2+
"nodeType": "program",
3+
"body": [
4+
{
5+
"nodeType": "component",
6+
"name": "TestComponent",
7+
"description": "A test component",
8+
"props": [
9+
{
10+
"nodeType": "prop",
11+
"name": "className",
12+
"propType": {
13+
"nodeType": "union",
14+
"types": [
15+
{
16+
"nodeType": "simpleType",
17+
"typeName": "undefined"
18+
},
19+
{
20+
"nodeType": "simpleType",
21+
"typeName": "string"
22+
}
23+
]
24+
},
25+
"optional": true,
26+
"filenames": {}
27+
},
28+
{
29+
"nodeType": "prop",
30+
"name": "id",
31+
"propType": {
32+
"nodeType": "union",
33+
"types": [
34+
{
35+
"nodeType": "simpleType",
36+
"typeName": "undefined"
37+
},
38+
{
39+
"nodeType": "simpleType",
40+
"typeName": "string"
41+
}
42+
]
43+
},
44+
"optional": true,
45+
"filenames": {}
46+
}
47+
]
48+
}
49+
]
50+
}

0 commit comments

Comments
 (0)