Skip to content

Commit c19166f

Browse files
committed
Document optional parameters and their default values
1 parent 2ebe18d commit c19166f

File tree

8 files changed

+160
-62
lines changed

8 files changed

+160
-62
lines changed

src/models/types/function.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,8 @@ export class Parameter implements SerializableNode {
4040
public type: TypeNode,
4141
public name: string,
4242
public documentation: Documentation | undefined,
43+
public optional: boolean,
44+
public defaultValue: unknown | undefined,
4345
) {}
4446

4547
toObject(): Record<string, unknown> {
@@ -48,6 +50,8 @@ export class Parameter implements SerializableNode {
4850
name: this.name,
4951
type: this.type.toObject(),
5052
documentation: this.documentation?.toObject(),
53+
optional: this.optional || undefined,
54+
defaultValue: this.defaultValue,
5155
};
5256
}
5357
}

src/parsers/documentationParser.ts

Lines changed: 1 addition & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,7 @@ export function getDocumentationFromNode(node: ts.Node): Documentation | undefin
3333
return new Documentation(
3434
commentNode.comment as string | undefined,
3535
commentNode.tags?.find((t) => t.tagName.text === 'default')?.comment,
36-
getVisibilityFromJSDoc(commentNode) ?? 'public',
36+
getVisibilityFromJSDoc(commentNode),
3737
tags ?? [],
3838
);
3939
}
@@ -56,25 +56,6 @@ function getVisibilityFromJSDoc(doc: ts.JSDoc): Documentation['visibility'] | un
5656
return undefined;
5757
}
5858

59-
export function getParameterDescriptionFromNode(node: ts.Node) {
60-
const comments = ts.getJSDocCommentsAndTags(node);
61-
if (comments && comments.length >= 1) {
62-
const commentNode = comments[0];
63-
if (ts.isJSDoc(commentNode)) {
64-
const paramComments: Record<string, string> = {};
65-
commentNode.tags?.forEach((tag) => {
66-
if (ts.isJSDocParameterTag(tag) && typeof tag.comment === 'string') {
67-
paramComments[tag.name.getText()] = tag.comment.replace(/^[\s-*:]+/g, '');
68-
}
69-
});
70-
71-
return paramComments;
72-
}
73-
}
74-
75-
return {};
76-
}
77-
7859
function parseTag(tag: ts.JSDocTag): DocumentationTag {
7960
if (ts.isJSDocTypeTag(tag)) {
8061
return {

src/parsers/functionParser.ts

Lines changed: 50 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
11
import ts, { FunctionDeclaration } from 'typescript';
22
import { type ParserContext } from '../parser';
3-
import { getParameterDescriptionFromNode } from './documentationParser';
43
import { resolveType } from './typeResolver';
54
import { FunctionNode, CallSignature, Documentation, Parameter } from '../models';
65

@@ -34,8 +33,6 @@ function parseFunctionSignature(
3433
context: ParserContext,
3534
skipResolvingComplexTypes: boolean = false,
3635
): CallSignature {
37-
const { checker } = context;
38-
3936
// Node that possibly has JSDocs attached to it
4037
let documentationNodeCandidate: ts.Node | undefined = undefined;
4138

@@ -66,40 +63,9 @@ function parseFunctionSignature(
6663
}
6764
}
6865

69-
const parameterDescriptions = documentationNodeCandidate
70-
? getParameterDescriptionFromNode(documentationNodeCandidate)
71-
: {};
72-
73-
const parameters = signature.parameters.map((parameterSymbol) => {
74-
const parameterDeclaration = parameterSymbol.valueDeclaration as ts.ParameterDeclaration;
75-
const parameterType = resolveType(
76-
checker.getTypeOfSymbolAtLocation(parameterSymbol, parameterSymbol.valueDeclaration!),
77-
parameterSymbol.getName(),
78-
context,
79-
skipResolvingComplexTypes,
80-
);
81-
82-
const documentation = new Documentation(parameterDescriptions[parameterSymbol.getName()]);
83-
const initializer = parameterDeclaration.initializer;
84-
if (initializer) {
85-
const initializerType = checker.getTypeAtLocation(initializer);
86-
if (initializerType.flags & ts.TypeFlags.Literal) {
87-
if (initializerType.isStringLiteral()) {
88-
documentation.defaultValue = `"${initializer.getText()}"`;
89-
} else {
90-
documentation.defaultValue = initializer.getText();
91-
}
92-
}
93-
}
94-
95-
const hasDocumentation = documentation.description || documentation.defaultValue;
96-
97-
return new Parameter(
98-
parameterType,
99-
parameterSymbol.getName(),
100-
hasDocumentation ? documentation : undefined,
101-
);
102-
});
66+
const parameters = signature.parameters.map((parameterSymbol) =>
67+
parseParameter(parameterSymbol, context, skipResolvingComplexTypes),
68+
);
10369

10470
const returnValueType = resolveType(
10571
signature.getReturnType(),
@@ -109,3 +75,50 @@ function parseFunctionSignature(
10975

11076
return new CallSignature(parameters, returnValueType);
11177
}
78+
79+
function parseParameter(
80+
parameterSymbol: ts.Symbol,
81+
context: ParserContext,
82+
skipResolvingComplexTypes: boolean,
83+
): Parameter {
84+
const { checker } = context;
85+
const parameterDeclaration = parameterSymbol.valueDeclaration as ts.ParameterDeclaration;
86+
const parameterType = resolveType(
87+
checker.getTypeOfSymbolAtLocation(parameterSymbol, parameterSymbol.valueDeclaration!),
88+
parameterSymbol.getName(),
89+
context,
90+
skipResolvingComplexTypes,
91+
);
92+
93+
const summary = parameterSymbol
94+
.getDocumentationComment(checker)
95+
.map((comment) => comment.text)
96+
.join('\n')
97+
.replace(/^[\s-*:]*/, '');
98+
99+
const documentation = summary?.length ? new Documentation(summary) : undefined;
100+
101+
let defaultValue: string | undefined;
102+
const initializer = parameterDeclaration.initializer;
103+
if (initializer) {
104+
const initializerType = checker.getTypeAtLocation(initializer);
105+
if (initializerType.flags & ts.TypeFlags.Literal) {
106+
if (initializerType.isStringLiteral()) {
107+
defaultValue = `"${initializerType.value}"`;
108+
} else if (initializerType.isLiteral()) {
109+
defaultValue = initializerType.value.toString();
110+
} else {
111+
defaultValue = initializer.getText();
112+
}
113+
}
114+
}
115+
116+
return new Parameter(
117+
parameterType,
118+
parameterSymbol.getName(),
119+
documentation,
120+
parameterDeclaration.questionToken !== undefined ||
121+
parameterDeclaration.initializer !== undefined,
122+
defaultValue,
123+
);
124+
}
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
const DEFAULT_VALUE = 'default';
2+
3+
/**
4+
* Function with optional parameters
5+
*
6+
* @param optional The optional parameter.
7+
* @param withInlineDefault The parameter with a default value.
8+
* @param withReferencedDefault The parameter with a default value from a constant.
9+
*/
10+
export function test(
11+
required: number,
12+
optional?: number,
13+
withInlineDefault = 42,
14+
withReferencedDefault = DEFAULT_VALUE,
15+
) {}
Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,80 @@
1+
{
2+
"name": "test/function-optional-parameters/input",
3+
"exports": [
4+
{
5+
"name": "test",
6+
"type": {
7+
"kind": "function",
8+
"name": "test",
9+
"callSignatures": [
10+
{
11+
"parameters": [
12+
{
13+
"nodeType": "parameter",
14+
"name": "required",
15+
"type": {
16+
"kind": "intrinsic",
17+
"name": "number"
18+
}
19+
},
20+
{
21+
"nodeType": "parameter",
22+
"name": "optional",
23+
"type": {
24+
"kind": "union",
25+
"types": [
26+
{
27+
"kind": "intrinsic",
28+
"name": "number"
29+
},
30+
{
31+
"kind": "intrinsic",
32+
"name": "undefined"
33+
}
34+
]
35+
},
36+
"documentation": {
37+
"description": "The optional parameter."
38+
},
39+
"optional": true
40+
},
41+
{
42+
"nodeType": "parameter",
43+
"name": "withInlineDefault",
44+
"type": {
45+
"kind": "intrinsic",
46+
"name": "number"
47+
},
48+
"documentation": {
49+
"description": "The parameter with a default value."
50+
},
51+
"optional": true,
52+
"defaultValue": "42"
53+
},
54+
{
55+
"nodeType": "parameter",
56+
"name": "withReferencedDefault",
57+
"type": {
58+
"kind": "intrinsic",
59+
"name": "string"
60+
},
61+
"documentation": {
62+
"description": "The parameter with a default value from a constant."
63+
},
64+
"optional": true,
65+
"defaultValue": "\"default\""
66+
}
67+
],
68+
"returnValueType": {
69+
"kind": "intrinsic",
70+
"name": "void"
71+
}
72+
}
73+
]
74+
},
75+
"documentation": {
76+
"description": "Function with optional parameters"
77+
}
78+
}
79+
]
80+
}

test/hook-multiple-parameters/output.json

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -66,9 +66,10 @@
6666
]
6767
},
6868
"documentation": {
69-
"description": "The severity.",
70-
"defaultValue": "\"'low'\""
71-
}
69+
"description": "The severity."
70+
},
71+
"optional": true,
72+
"defaultValue": "\"low\""
7273
}
7374
],
7475
"returnValueType": {

test/jsdoc-extra-tags/output.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818
},
1919
"documentation": {
2020
"description": "This is a test.",
21+
"visibility": "public",
2122
"tags": [
2223
{
2324
"name": "example",

test/jsdocs/output.json

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -92,7 +92,8 @@
9292
},
9393
"documentation": {
9494
"description": "The second parameter"
95-
}
95+
},
96+
"optional": true
9697
}
9798
],
9899
"returnValueType": {
@@ -131,7 +132,8 @@
131132
},
132133
"documentation": {
133134
"description": "The second parameter"
134-
}
135+
},
136+
"optional": true
135137
}
136138
],
137139
"returnValueType": {
@@ -143,6 +145,7 @@
143145
},
144146
"documentation": {
145147
"description": "A test function",
148+
"visibility": "public",
146149
"tags": [
147150
{
148151
"name": "remarks",

0 commit comments

Comments
 (0)