Skip to content

Commit 15b57cc

Browse files
committed
Extract richer function props information
1 parent bda81e3 commit 15b57cc

File tree

10 files changed

+104
-49
lines changed

10 files changed

+104
-49
lines changed

.editorconfig

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,13 @@
11
root = true
22

33
[*]
4-
indent_style = tab
4+
indent_style = tab
5+
insert_final_newline = true
6+
trim_trailing_whitespace = true
7+
8+
[*.md]
9+
trim_trailing_whitespace = false
10+
11+
[.{yml,yaml}]
12+
indent_style = space
13+
indent_size = 2

.prettierignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
pnpm-lock.yaml

src/config.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,15 +8,15 @@ import path from 'path';
88
*/
99
export function loadConfig(tsConfigPath: string) {
1010
const { config, error } = ts.readConfigFile(tsConfigPath, (filePath) =>
11-
fs.readFileSync(filePath).toString()
11+
fs.readFileSync(filePath).toString(),
1212
);
1313

1414
if (error) throw error;
1515

1616
const { options, errors } = ts.parseJsonConfigFileContent(
1717
config,
1818
ts.sys,
19-
path.dirname(tsConfigPath)
19+
path.dirname(tsConfigPath),
2020
);
2121

2222
if (errors.length > 0) throw errors[0];

src/parser.ts

Lines changed: 43 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -51,7 +51,7 @@ export function createProgram(files: string[], options: ts.CompilerOptions) {
5151
export function parseFile(
5252
filePath: string,
5353
options: ts.CompilerOptions,
54-
parserOptions: Partial<ParserOptions> = {}
54+
parserOptions: Partial<ParserOptions> = {},
5555
) {
5656
const program = ts.createProgram([filePath], options);
5757
return parseFromProgram(filePath, program, parserOptions);
@@ -66,7 +66,7 @@ export function parseFile(
6666
export function parseFromProgram(
6767
filePath: string,
6868
program: ts.Program,
69-
parserOptions: Partial<ParserOptions> = {}
69+
parserOptions: Partial<ParserOptions> = {},
7070
) {
7171
const { checkDeclarations = false } = parserOptions;
7272

@@ -168,7 +168,7 @@ export function parseFromProgram(
168168
parsePropsType(
169169
variableNode.name.getText(),
170170
type.aliasTypeArguments[0],
171-
node.getSourceFile()
171+
node.getSourceFile(),
172172
);
173173
} else if (checkDeclarations) {
174174
parseFunctionComponent(variableNode);
@@ -216,7 +216,7 @@ export function parseFromProgram(
216216
parsePropsType(
217217
node.name.getText(),
218218
checker.getTypeAtLocation(arg.typeArguments[0]),
219-
node.getSourceFile()
219+
node.getSourceFile(),
220220
);
221221
}
222222
}
@@ -225,11 +225,15 @@ export function parseFromProgram(
225225
function isTypeJSXElementLike(type: ts.Type): boolean {
226226
if (type.isUnion()) {
227227
return type.types.every(
228-
(subType) => subType.flags & ts.TypeFlags.Null || isTypeJSXElementLike(subType)
228+
(subType) => subType.flags & ts.TypeFlags.Null || isTypeJSXElementLike(subType),
229229
);
230230
} else if (type.symbol) {
231231
const name = checker.getFullyQualifiedName(type.symbol);
232-
return name === 'global.JSX.Element' || name === 'React.ReactElement';
232+
return (
233+
name === 'global.JSX.Element' ||
234+
name === 'React.ReactElement' ||
235+
name === 'React.JSX.Element'
236+
);
233237
}
234238

235239
return false;
@@ -246,15 +250,15 @@ export function parseFromProgram(
246250
}
247251
const componentName = node.name.getText();
248252

249-
const type = checker.getTypeOfSymbolAtLocation(symbol, symbol.valueDeclaration);
253+
const type = checker.getTypeOfSymbolAtLocation(symbol, symbol.valueDeclaration!);
250254
type.getCallSignatures().forEach((signature) => {
251255
if (!isTypeJSXElementLike(signature.getReturnType())) {
252256
return;
253257
}
254258

255259
const propsType = checker.getTypeOfSymbolAtLocation(
256260
signature.parameters[0],
257-
signature.parameters[0].valueDeclaration
261+
signature.parameters[0].valueDeclaration!,
258262
);
259263

260264
parsePropsType(componentName, propsType, node.getSourceFile());
@@ -282,7 +286,7 @@ export function parseFromProgram(
282286
currentTypeNode.jsDoc,
283287
t.unionNode([currentTypeNode.propType, typeNode.propType]),
284288
new Set(Array.from(currentTypeNode.filenames).concat(Array.from(typeNode.filenames))),
285-
undefined
289+
undefined,
286290
);
287291
}
288292

@@ -311,8 +315,8 @@ export function parseFromProgram(
311315
}
312316
return propType;
313317
}),
314-
node.getSourceFile().fileName
315-
)
318+
node.getSourceFile().fileName,
319+
),
316320
);
317321
}
318322

@@ -330,8 +334,8 @@ export function parseFromProgram(
330334
t.componentNode(
331335
name,
332336
properties.map((x) => checkSymbol(x, new Set([(type as any).id]))),
333-
propsFilename
334-
)
337+
propsFilename,
338+
),
335339
);
336340
}
337341

@@ -361,25 +365,25 @@ export function parseFromProgram(
361365
name === 'React.Component'
362366
) {
363367
const elementNode = t.elementNode(
364-
name === 'React.ReactElement' ? 'element' : 'elementType'
368+
name === 'React.ReactElement' ? 'element' : 'elementType',
365369
);
366370

367371
return t.propTypeNode(
368372
symbol.getName(),
369373
getDocumentation(symbol),
370374
declaration.questionToken ? t.unionNode([t.undefinedNode(), elementNode]) : elementNode,
371375
symbolFilenames,
372-
(symbol as any).id
376+
(symbol as any).id,
373377
);
374378
}
375379
}
376380

377381
const symbolType = declaration
378382
? // The proptypes aren't detailed enough that we need all the different combinations
379-
// so we just pick the first and ignore the rest
380-
checker.getTypeOfSymbolAtLocation(symbol, declaration)
383+
// so we just pick the first and ignore the rest
384+
checker.getTypeOfSymbolAtLocation(symbol, declaration)
381385
: // The properties of Record<..., ...> don't have a declaration, but the symbol has a type property
382-
((symbol as any).type as ts.Type);
386+
((symbol as any).type as ts.Type);
383387
// get `React.ElementType` from `C extends React.ElementType`
384388
const declaredType =
385389
declaration !== undefined ? checker.getTypeAtLocation(declaration) : undefined;
@@ -415,7 +419,7 @@ export function parseFromProgram(
415419
getDocumentation(symbol),
416420
parsedType,
417421
symbolFilenames,
418-
(symbol as any).id
422+
(symbol as any).id,
419423
);
420424
}
421425

@@ -433,6 +437,7 @@ export function parseFromProgram(
433437
const typeName = symbol ? checker.getFullyQualifiedName(symbol) : null;
434438
switch (typeName) {
435439
case 'global.JSX.Element':
440+
case 'React.JSX.Element':
436441
case 'React.ReactElement': {
437442
return t.elementNode('element');
438443
}
@@ -486,7 +491,7 @@ export function parseFromProgram(
486491
if (type.isLiteral()) {
487492
return t.literalNode(
488493
type.isStringLiteral() ? `"${type.value}"` : type.value,
489-
getDocumentation(type.symbol)
494+
getDocumentation(type.symbol),
490495
);
491496
}
492497
return t.literalNode(checker.typeToString(type));
@@ -496,8 +501,20 @@ export function parseFromProgram(
496501
return t.literalNode('null');
497502
}
498503

499-
if (type.getCallSignatures().length) {
500-
return t.functionNode();
504+
if (type.getCallSignatures().length === 1) {
505+
const signature = type.getCallSignatures()[0];
506+
return t.functionNode(
507+
signature.parameters.map((param) =>
508+
t.parameterNode(
509+
param.name,
510+
checker.getTypeOfSymbol(param)?.getSymbol()?.name ?? 'unknown',
511+
),
512+
),
513+
// TODO: parse return type
514+
t.anyNode(),
515+
);
516+
} else if (type.getCallSignatures().length > 1) {
517+
return t.functionNode([], t.anyNode());
501518
}
502519

503520
// Object-like type
@@ -508,13 +525,13 @@ export function parseFromProgram(
508525
shouldResolveObject({ name, propertyCount: properties.length, depth: typeStack.size })
509526
) {
510527
const filtered = properties.filter((symbol) =>
511-
shouldInclude({ name: symbol.getName(), depth: typeStack.size + 1 })
528+
shouldInclude({ name: symbol.getName(), depth: typeStack.size + 1 }),
512529
);
513530
if (filtered.length > 0) {
514531
return t.interfaceNode(
515532
filtered.map((x) =>
516-
checkSymbol(x, new Set([...typeStack.values(), (type as any).id]))
517-
)
533+
checkSymbol(x, new Set([...typeStack.values(), (type as any).id])),
534+
),
518535
);
519536
}
520537
}
@@ -532,7 +549,7 @@ export function parseFromProgram(
532549
}
533550

534551
console.warn(
535-
`Unable to handle node of type "ts.TypeFlags.${ts.TypeFlags[type.flags]}", using any`
552+
`Unable to handle node of type "ts.TypeFlags.${ts.TypeFlags[type.flags]}", using any`,
536553
);
537554
return t.anyNode();
538555
}

src/types/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ export * from './nodes/component';
44
export * from './nodes/proptype';
55

66
export * from './props/function';
7+
export * from './props/parameter';
78
export * from './props/interface';
89
export * from './props/string';
910
export * from './props/union';

src/types/nodes/component.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ export interface ComponentNode extends DefinitionHolder {
1111
export function componentNode(
1212
name: string,
1313
types: PropTypeNode[],
14-
propsFilename: string | undefined
14+
propsFilename: string | undefined,
1515
): ComponentNode {
1616
return {
1717
type: typeString,

src/types/nodes/proptype.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ export function propTypeNode(
1818
jsDoc: string | undefined,
1919
propType: Node,
2020
filenames: Set<string>,
21-
id: number | undefined
21+
id: number | undefined,
2222
): PropTypeNode {
2323
return {
2424
type: typeString,

src/types/props/function.ts

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,16 @@ import { Node } from '../nodes/baseNodes';
22

33
const typeString = 'FunctionNode';
44

5-
export function functionNode(): Node {
5+
interface FunctionNode extends Node {
6+
parameters: Node[];
7+
returnValue: Node;
8+
}
9+
10+
export function functionNode(parameters: Node[], returnValue: Node): FunctionNode {
611
return {
712
type: typeString,
13+
parameters,
14+
returnValue,
815
};
916
}
1017

src/types/props/parameter.ts

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
import { Node } from '../nodes/baseNodes';
2+
3+
const typeString = 'ParameterNode';
4+
5+
interface ParameterNode extends Node {
6+
name: string;
7+
parameterType: string;
8+
}
9+
10+
export function parameterNode(name: string, parameterType: string): ParameterNode {
11+
return {
12+
type: typeString,
13+
name,
14+
parameterType,
15+
};
16+
}
17+
18+
export function isParameterNode(node: Node) {
19+
return node.type === typeString;
20+
}

tsconfig.json

Lines changed: 17 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1,21 +1,21 @@
11
{
2-
"compilerOptions": {
3-
"rootDir": "./src",
4-
"outDir": "./dist",
5-
"tsBuildInfoFile": "./dist/.tsbuildinfo",
6-
"target": "ES2022",
7-
"moduleResolution": "node16",
8-
"module": "node16",
9-
"jsx": "react-jsx",
2+
"compilerOptions": {
3+
"rootDir": "./src",
4+
"outDir": "./dist",
5+
"tsBuildInfoFile": "./dist/.tsbuildinfo",
6+
"target": "ES2022",
7+
"moduleResolution": "node16",
8+
"module": "node16",
9+
"jsx": "react-jsx",
1010

11-
"strict": true,
12-
"declaration": true,
13-
"declarationMap": true,
14-
"sourceMap": true,
15-
"composite": true,
11+
"strict": true,
12+
"declaration": true,
13+
"declarationMap": true,
14+
"sourceMap": true,
15+
"composite": true,
1616

17-
"esModuleInterop": true,
18-
"isolatedModules": true
19-
},
20-
"include": ["./src/**/*"]
17+
"esModuleInterop": true,
18+
"isolatedModules": true
19+
},
20+
"include": ["./src/**/*"]
2121
}

0 commit comments

Comments
 (0)