Skip to content

Commit ce9a700

Browse files
authored
feat: add filenames to component and proptype nodes (#9)
1 parent fd028e6 commit ce9a700

File tree

8 files changed

+2680
-12
lines changed

8 files changed

+2680
-12
lines changed

src/parser.ts

Lines changed: 29 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -165,7 +165,11 @@ export function parseFromProgram(
165165
type.aliasTypeArguments &&
166166
checker.getFullyQualifiedName(type.aliasSymbol) === 'React.ComponentType'
167167
) {
168-
parsePropsType(variableNode.name.getText(), type.aliasTypeArguments[0]);
168+
parsePropsType(
169+
variableNode.name.getText(),
170+
type.aliasTypeArguments[0],
171+
node.getSourceFile()
172+
);
169173
}
170174
} else if (
171175
(ts.isArrowFunction(variableNode.initializer) ||
@@ -190,7 +194,8 @@ export function parseFromProgram(
190194
if (symbol) {
191195
parsePropsType(
192196
variableNode.name.getText(),
193-
checker.getTypeOfSymbolAtLocation(symbol, symbol.valueDeclaration)
197+
checker.getTypeOfSymbolAtLocation(symbol, symbol.valueDeclaration),
198+
node.getSourceFile()
194199
);
195200
}
196201
}
@@ -210,7 +215,11 @@ export function parseFromProgram(
210215
if (!arg.typeArguments) return;
211216

212217
if (reactImports.includes(arg.expression.getText())) {
213-
parsePropsType(node.name.getText(), checker.getTypeAtLocation(arg.typeArguments[0]));
218+
parsePropsType(
219+
node.name.getText(),
220+
checker.getTypeAtLocation(arg.typeArguments[0]),
221+
node.getSourceFile()
222+
);
214223
}
215224
}
216225
}
@@ -251,21 +260,24 @@ export function parseFromProgram(
251260
signature.parameters[0].valueDeclaration
252261
);
253262

254-
parsePropsType(node.name.getText(), type);
263+
parsePropsType(node.name.getText(), type, node.getSourceFile());
255264
}
256265

257-
function parsePropsType(name: string, type: ts.Type) {
266+
function parsePropsType(name: string, type: ts.Type, sourceFile: ts.SourceFile | undefined) {
258267
const properties = type
259268
.getProperties()
260269
.filter((symbol) => shouldInclude({ name: symbol.getName(), depth: 1 }));
261270
if (properties.length === 0) {
262271
return;
263272
}
264273

274+
const propsFilename = sourceFile !== undefined ? sourceFile.fileName : undefined;
275+
265276
programNode.body.push(
266277
t.componentNode(
267278
name,
268-
properties.map((x) => checkSymbol(x, [(type as any).id]))
279+
properties.map((x) => checkSymbol(x, [(type as any).id])),
280+
propsFilename
269281
)
270282
);
271283
}
@@ -274,6 +286,8 @@ export function parseFromProgram(
274286
const declarations = symbol.getDeclarations();
275287
const declaration = declarations && declarations[0];
276288

289+
const symbolFilenames = getSymbolFileNames(symbol);
290+
277291
// TypeChecker keeps the name for
278292
// { a: React.ElementType, b: React.ReactElement | boolean }
279293
// but not
@@ -298,7 +312,8 @@ export function parseFromProgram(
298312
return t.propTypeNode(
299313
symbol.getName(),
300314
getDocumentation(symbol),
301-
declaration.questionToken ? t.unionNode([t.undefinedNode(), elementNode]) : elementNode
315+
declaration.questionToken ? t.unionNode([t.undefinedNode(), elementNode]) : elementNode,
316+
symbolFilenames
302317
);
303318
}
304319
}
@@ -330,7 +345,7 @@ export function parseFromProgram(
330345
parsedType = checkType(type, typeStack, symbol.getName());
331346
}
332347

333-
return t.propTypeNode(symbol.getName(), getDocumentation(symbol), parsedType);
348+
return t.propTypeNode(symbol.getName(), getDocumentation(symbol), parsedType, symbolFilenames);
334349
}
335350

336351
function checkType(type: ts.Type, typeStack: number[], name: string): t.Node {
@@ -468,4 +483,10 @@ export function parseFromProgram(
468483
const comment = ts.displayPartsToString(symbol.getDocumentationComment(checker));
469484
return comment ? comment : undefined;
470485
}
486+
487+
function getSymbolFileNames(symbol: ts.Symbol): Set<string> {
488+
const declarations = symbol.getDeclarations() || [];
489+
490+
return new Set(declarations.map((declaration) => declaration.getSourceFile().fileName));
491+
}
471492
}

src/types/nodes/component.ts

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,13 +5,19 @@ const typeString = 'ComponentNode';
55

66
export interface ComponentNode extends DefinitionHolder {
77
name: string;
8+
propsFilename?: string;
89
}
910

10-
export function componentNode(name: string, types?: PropTypeNode[]): ComponentNode {
11+
export function componentNode(
12+
name: string,
13+
types: PropTypeNode[],
14+
propsFilename: string | undefined
15+
): ComponentNode {
1116
return {
1217
type: typeString,
1318
name: name,
1419
types: types || [],
20+
propsFilename,
1521
};
1622
}
1723

src/types/nodes/proptype.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,18 +6,21 @@ export interface PropTypeNode extends Node {
66
name: string;
77
jsDoc?: string;
88
propType: Node;
9+
filenames: Set<string>;
910
}
1011

1112
export function propTypeNode(
1213
name: string,
1314
jsDoc: string | undefined,
14-
propType: Node
15+
propType: Node,
16+
filenames: Set<string>
1517
): PropTypeNode {
1618
return {
1719
type: typeString,
1820
name,
1921
jsDoc,
2022
propType,
23+
filenames,
2124
};
2225
}
2326

test/index.test.ts

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -30,12 +30,21 @@ for (const testCase of testCases) {
3030
const ast = ttp.parseFromProgram(testCase, program, options.parser);
3131

3232
//#region Check AST matches
33+
// propsFilename will be different depending on where the project is on disk
34+
// Manually check that it's correct and then delete it
35+
const newAST = ttp.programNode(
36+
ast.body.map((component) => {
37+
expect(component.propsFilename).toBe(testCase);
38+
return { ...component, propsFilename: undefined };
39+
})
40+
);
41+
3342
if (fs.existsSync(astPath)) {
34-
expect(ast).toMatchObject(JSON.parse(fs.readFileSync(astPath, 'utf8')));
43+
expect(newAST).toMatchObject(JSON.parse(fs.readFileSync(astPath, 'utf8')));
3544
} else {
3645
fs.writeFileSync(
3746
astPath,
38-
prettier.format(JSON.stringify(ast), {
47+
prettier.format(JSON.stringify(newAST), {
3948
...prettierConfig,
4049
filepath: astPath,
4150
})
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
import * as React from 'react';
2+
3+
// it's technically not correct since this descripts props the component
4+
// sees not just the one available to the user. We're abusing this to provide
5+
// some concrete documentation for `key` regarding this component
6+
export interface SnackBarProps extends React.HTMLAttributes<any> {
7+
/**
8+
* some hints about state reset that relates to prop of this component
9+
*/
10+
key?: any;
11+
}
12+
13+
export function Snackbar(props: SnackBarProps) {
14+
return <div {...props} />;
15+
}
16+
17+
// here we don't care about `key`
18+
export function SomeOtherComponent(props: { children?: React.ReactNode }) {
19+
return <div>{props.children}</div>;
20+
}
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
import * as path from 'path';
2+
import { TestOptions } from '../../types';
3+
4+
const options: TestOptions = {
5+
injector: {
6+
includeUnusedProps: true,
7+
shouldInclude: ({ prop }) => {
8+
let isLocallyTyped = false;
9+
prop.filenames.forEach((filename) => {
10+
if (!path.relative(__dirname, filename).startsWith('..')) {
11+
isLocallyTyped = true;
12+
}
13+
});
14+
return isLocallyTyped;
15+
},
16+
},
17+
};
18+
19+
export default options;
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
import * as React from 'react';
2+
import PropTypes from 'prop-types';
3+
function Snackbar(props) {
4+
return <div {...props} />;
5+
}
6+
// here we don't care about `key`
7+
8+
Snackbar.propTypes = {
9+
/**
10+
* some hints about state reset that relates to prop of this component
11+
*/
12+
key: PropTypes.any,
13+
};
14+
15+
export { Snackbar };
16+
function SomeOtherComponent(props) {
17+
return <div>{props.children}</div>;
18+
}
19+
20+
SomeOtherComponent.propTypes = {
21+
children: PropTypes.node,
22+
};
23+
24+
export { SomeOtherComponent };

0 commit comments

Comments
 (0)