Skip to content

Commit ee67ac9

Browse files
committed
Parse hooks
1 parent a43ebfb commit ee67ac9

File tree

9 files changed

+241
-17
lines changed

9 files changed

+241
-17
lines changed

src/parser.ts

Lines changed: 53 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
11
import * as ts from 'typescript';
22
import * as t from './types';
3-
import { Documentation } from './types/documentation';
43

54
/**
65
* Options that specify how the parser should act
@@ -167,8 +166,12 @@ export function parseFromProgram(
167166

168167
function visit(node: ts.Node) {
169168
// function x(props: type) { return <div/> }
170-
if (ts.isFunctionDeclaration(node) && node.name && node.parameters.length === 1) {
171-
parseFunctionComponent(node, node);
169+
if (ts.isFunctionDeclaration(node) && node.name) {
170+
if (node.name.getText().startsWith('use')) {
171+
parseHook(node);
172+
} else if (node.parameters.length === 1) {
173+
parseFunctionComponent(node, node);
174+
}
172175
}
173176
// const x = ...
174177
else if (ts.isVariableStatement(node)) {
@@ -303,7 +306,7 @@ export function parseFromProgram(
303306
const props: Record<string, t.PropNode> = {};
304307
const usedPropsPerSignature: Set<String>[] = [];
305308
programNode.body = programNode.body.filter((node) => {
306-
if (node.name === componentName) {
309+
if (node.name === componentName && t.isComponentNode(node)) {
307310
const usedProps: Set<string> = new Set();
308311
// squash props
309312
node.props.forEach((propNode) => {
@@ -397,6 +400,49 @@ export function parseFromProgram(
397400
);
398401
}
399402

403+
function parseHook(node: ts.VariableDeclaration | ts.FunctionDeclaration) {
404+
if (!node.name) {
405+
return;
406+
}
407+
408+
const symbol = checker.getSymbolAtLocation(node.name);
409+
if (!symbol) {
410+
return;
411+
}
412+
const hookName = node.name.getText();
413+
414+
const type = checker.getTypeOfSymbolAtLocation(symbol, symbol.valueDeclaration!);
415+
const typeStack = new Set<number>([(type as any).id]);
416+
417+
const checkedSignatures = type.getCallSignatures().map((signature) => {
418+
return {
419+
parameters: signature.parameters.map((param) =>
420+
t.parameterNode(checkSymbol(param, typeStack)),
421+
),
422+
423+
returnValue: checkType(signature.getReturnType(), typeStack, hookName),
424+
};
425+
});
426+
427+
if (checkedSignatures.length === 0) {
428+
return;
429+
}
430+
431+
if (checkedSignatures.length === 1) {
432+
programNode.body.push(
433+
t.hookNode(
434+
hookName,
435+
checkedSignatures[0].parameters,
436+
checkedSignatures[0].returnValue,
437+
getDocumentationFromNode(node),
438+
node.getSourceFile().fileName,
439+
),
440+
);
441+
}
442+
443+
// TODO: handle multiple call signatures
444+
}
445+
400446
function checkSymbol(
401447
symbol: ts.Symbol,
402448
typeStack: Set<number>,
@@ -642,7 +688,7 @@ export function parseFromProgram(
642688
return t.simpleTypeNode('any');
643689
}
644690

645-
function getDocumentationFromSymbol(symbol?: ts.Symbol): Documentation | undefined {
691+
function getDocumentationFromSymbol(symbol?: ts.Symbol): t.Documentation | undefined {
646692
if (!symbol) {
647693
return undefined;
648694
}
@@ -656,7 +702,7 @@ export function parseFromProgram(
656702
return comment ? { description: comment } : undefined;
657703
}
658704

659-
function getDocumentationFromNode(node: ts.Node): Documentation | undefined {
705+
function getDocumentationFromNode(node: ts.Node): t.Documentation | undefined {
660706
const comments = ts.getJSDocCommentsAndTags(node);
661707
if (comments && comments.length === 1) {
662708
const commentNode = comments[0];
@@ -681,7 +727,7 @@ function hasFlag(typeFlags: number, flag: number) {
681727
return (typeFlags & flag) === flag;
682728
}
683729

684-
function getVisibilityFromJSDoc(doc: ts.JSDoc): Documentation['visibility'] | undefined {
730+
function getVisibilityFromJSDoc(doc: ts.JSDoc): t.Documentation['visibility'] | undefined {
685731
if (doc.tags?.some((tag) => tag.tagName.text === 'public')) {
686732
return 'public';
687733
}

src/types/index.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
export * from './nodes/baseNodes';
22
export * from './nodes/program';
33
export * from './nodes/component';
4+
export * from './nodes/hook';
45
export * from './nodes/prop';
56

67
export * from './props/simpleType';
@@ -11,3 +12,5 @@ export * from './props/union';
1112
export * from './props/literal';
1213
export * from './props/object';
1314
export * from './props/array';
15+
16+
export * from './documentation';

src/types/nodes/hook.ts

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
import { Documentation } from '../documentation';
2+
import { FunctionNode } from '../props/function';
3+
import { Node } from './baseNodes';
4+
5+
const typeString = 'hook';
6+
7+
export interface HookNode extends FunctionNode {
8+
name: string;
9+
parametersFilename?: string;
10+
description?: string;
11+
visibility?: Documentation['visibility'];
12+
}
13+
14+
export function hookNode(
15+
name: string,
16+
parameters: Node[],
17+
returnValue: Node,
18+
documentation: Documentation | undefined,
19+
parametersFilename: string | undefined,
20+
): HookNode {
21+
return {
22+
nodeType: typeString,
23+
name: name,
24+
parameters: parameters || [],
25+
returnValue,
26+
description: documentation?.description,
27+
visibility: documentation?.visibility,
28+
parametersFilename,
29+
};
30+
}
31+
32+
export function isHookNode(node: Node): node is HookNode {
33+
return node.nodeType === typeString;
34+
}

src/types/nodes/program.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,14 @@
11
import { Node } from './baseNodes';
22
import { ComponentNode } from './component';
3+
import { HookNode } from './hook';
34

45
const typeString = 'program';
56

67
export interface ProgramNode extends Node {
7-
body: ComponentNode[];
8+
body: (ComponentNode | HookNode)[];
89
}
910

10-
export function programNode(body?: ComponentNode[]): ProgramNode {
11+
export function programNode(body?: (ComponentNode | HookNode)[]): ProgramNode {
1112
return {
1213
nodeType: typeString,
1314
body: body || [],

src/types/props/function.ts

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

33
const typeString = 'function';
44

5-
interface FunctionNode extends Node {
5+
export interface FunctionNode extends Node {
66
parameters: Node[];
77
returnValue: Node;
88
}

src/types/props/parameter.ts

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

33
const typeString = 'parameter';
44

5-
interface ParameterNode extends Node {
5+
export interface ParameterNode extends Node {
66
parameterType: Node;
77
}
88

test/hook-function/input.ts

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
/**
2+
* A hook defined as a function.
3+
*
4+
* @internal
5+
*/
6+
export function useHook(parameters: HookParameters): HookReturnValue {
7+
return {
8+
getProps(externalProps) {
9+
return externalProps;
10+
},
11+
ref(element: HTMLElement) {},
12+
};
13+
}
14+
15+
interface HookParameters {
16+
value: string;
17+
severity: number;
18+
onChange: (value: string) => void;
19+
}
20+
21+
interface HookReturnValue {
22+
getProps: (externalProps: React.HTMLAttributes<HTMLElement>) => React.HTMLAttributes<HTMLElement>;
23+
ref: React.RefCallback<HTMLElement>;
24+
}

test/hook-function/output.json

Lines changed: 108 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,108 @@
1+
{
2+
"nodeType": "program",
3+
"body": [
4+
{
5+
"nodeType": "hook",
6+
"name": "useHook",
7+
"description": "A hook defined as a function.",
8+
"visibility": "internal",
9+
"parameters": [
10+
{
11+
"nodeType": "parameter",
12+
"parameterType": {
13+
"nodeType": "prop",
14+
"name": "parameters",
15+
"propType": {
16+
"nodeType": "interface",
17+
"types": [
18+
{
19+
"nodeType": "prop",
20+
"name": "value",
21+
"propType": {
22+
"nodeType": "simpleType",
23+
"typeName": "string"
24+
},
25+
"optional": false,
26+
"filenames": {}
27+
},
28+
{
29+
"nodeType": "prop",
30+
"name": "severity",
31+
"propType": {
32+
"nodeType": "simpleType",
33+
"typeName": "number"
34+
},
35+
"optional": false,
36+
"filenames": {}
37+
},
38+
{
39+
"nodeType": "prop",
40+
"name": "onChange",
41+
"propType": {
42+
"nodeType": "function",
43+
"parameters": [
44+
{
45+
"nodeType": "parameter",
46+
"parameterType": {
47+
"nodeType": "prop",
48+
"name": "value",
49+
"propType": {
50+
"nodeType": "simpleType",
51+
"typeName": "string"
52+
},
53+
"optional": false,
54+
"filenames": {}
55+
}
56+
}
57+
],
58+
"returnValue": {
59+
"nodeType": "simpleType",
60+
"typeName": "void"
61+
}
62+
},
63+
"optional": false,
64+
"filenames": {}
65+
}
66+
]
67+
},
68+
"optional": false,
69+
"filenames": {}
70+
}
71+
}
72+
],
73+
"returnValue": {
74+
"nodeType": "interface",
75+
"types": [
76+
{
77+
"nodeType": "prop",
78+
"name": "getProps",
79+
"propType": {
80+
"nodeType": "function",
81+
"parameters": [
82+
{
83+
"nodeType": "parameter",
84+
"parameterType": {
85+
"nodeType": "prop",
86+
"name": "externalProps",
87+
"propType": {
88+
"nodeType": "simpleType",
89+
"typeName": "React.HTMLAttributes"
90+
},
91+
"optional": false,
92+
"filenames": {}
93+
}
94+
}
95+
],
96+
"returnValue": {
97+
"nodeType": "simpleType",
98+
"typeName": "React.HTMLAttributes"
99+
}
100+
},
101+
"optional": false,
102+
"filenames": {}
103+
}
104+
]
105+
}
106+
}
107+
]
108+
}

test/index.test.ts

Lines changed: 14 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -23,12 +23,20 @@ for (const testCase of testCases) {
2323
const ast = rae.parseFromProgram(testCase, program);
2424

2525
const newAST = rae.programNode(
26-
ast.body.map((component) => {
27-
expect(component.propsFilename).toBe(testCase);
28-
return {
29-
...component,
30-
propsFilename: undefined,
31-
};
26+
ast.body.map((componentOrHook) => {
27+
if (rae.isComponentNode(componentOrHook)) {
28+
expect(componentOrHook.propsFilename).toBe(testCase);
29+
return {
30+
...componentOrHook,
31+
propsFilename: undefined,
32+
};
33+
} else {
34+
expect(componentOrHook.parametersFilename).toBe(testCase);
35+
return {
36+
...componentOrHook,
37+
parametersFilename: undefined,
38+
};
39+
}
3240
}),
3341
);
3442

0 commit comments

Comments
 (0)