Skip to content

Commit

Permalink
feat: add filenames to component and proptype nodes (#9)
Browse files Browse the repository at this point in the history
  • Loading branch information
eps1lon authored Apr 5, 2020
1 parent fd028e6 commit ce9a700
Show file tree
Hide file tree
Showing 8 changed files with 2,680 additions and 12 deletions.
37 changes: 29 additions & 8 deletions src/parser.ts
Original file line number Diff line number Diff line change
Expand Up @@ -165,7 +165,11 @@ export function parseFromProgram(
type.aliasTypeArguments &&
checker.getFullyQualifiedName(type.aliasSymbol) === 'React.ComponentType'
) {
parsePropsType(variableNode.name.getText(), type.aliasTypeArguments[0]);
parsePropsType(
variableNode.name.getText(),
type.aliasTypeArguments[0],
node.getSourceFile()
);
}
} else if (
(ts.isArrowFunction(variableNode.initializer) ||
Expand All @@ -190,7 +194,8 @@ export function parseFromProgram(
if (symbol) {
parsePropsType(
variableNode.name.getText(),
checker.getTypeOfSymbolAtLocation(symbol, symbol.valueDeclaration)
checker.getTypeOfSymbolAtLocation(symbol, symbol.valueDeclaration),
node.getSourceFile()
);
}
}
Expand All @@ -210,7 +215,11 @@ export function parseFromProgram(
if (!arg.typeArguments) return;

if (reactImports.includes(arg.expression.getText())) {
parsePropsType(node.name.getText(), checker.getTypeAtLocation(arg.typeArguments[0]));
parsePropsType(
node.name.getText(),
checker.getTypeAtLocation(arg.typeArguments[0]),
node.getSourceFile()
);
}
}
}
Expand Down Expand Up @@ -251,21 +260,24 @@ export function parseFromProgram(
signature.parameters[0].valueDeclaration
);

parsePropsType(node.name.getText(), type);
parsePropsType(node.name.getText(), type, node.getSourceFile());
}

function parsePropsType(name: string, type: ts.Type) {
function parsePropsType(name: string, type: ts.Type, sourceFile: ts.SourceFile | undefined) {
const properties = type
.getProperties()
.filter((symbol) => shouldInclude({ name: symbol.getName(), depth: 1 }));
if (properties.length === 0) {
return;
}

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

programNode.body.push(
t.componentNode(
name,
properties.map((x) => checkSymbol(x, [(type as any).id]))
properties.map((x) => checkSymbol(x, [(type as any).id])),
propsFilename
)
);
}
Expand All @@ -274,6 +286,8 @@ export function parseFromProgram(
const declarations = symbol.getDeclarations();
const declaration = declarations && declarations[0];

const symbolFilenames = getSymbolFileNames(symbol);

// TypeChecker keeps the name for
// { a: React.ElementType, b: React.ReactElement | boolean }
// but not
Expand All @@ -298,7 +312,8 @@ export function parseFromProgram(
return t.propTypeNode(
symbol.getName(),
getDocumentation(symbol),
declaration.questionToken ? t.unionNode([t.undefinedNode(), elementNode]) : elementNode
declaration.questionToken ? t.unionNode([t.undefinedNode(), elementNode]) : elementNode,
symbolFilenames
);
}
}
Expand Down Expand Up @@ -330,7 +345,7 @@ export function parseFromProgram(
parsedType = checkType(type, typeStack, symbol.getName());
}

return t.propTypeNode(symbol.getName(), getDocumentation(symbol), parsedType);
return t.propTypeNode(symbol.getName(), getDocumentation(symbol), parsedType, symbolFilenames);
}

function checkType(type: ts.Type, typeStack: number[], name: string): t.Node {
Expand Down Expand Up @@ -468,4 +483,10 @@ export function parseFromProgram(
const comment = ts.displayPartsToString(symbol.getDocumentationComment(checker));
return comment ? comment : undefined;
}

function getSymbolFileNames(symbol: ts.Symbol): Set<string> {
const declarations = symbol.getDeclarations() || [];

return new Set(declarations.map((declaration) => declaration.getSourceFile().fileName));
}
}
8 changes: 7 additions & 1 deletion src/types/nodes/component.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,13 +5,19 @@ const typeString = 'ComponentNode';

export interface ComponentNode extends DefinitionHolder {
name: string;
propsFilename?: string;
}

export function componentNode(name: string, types?: PropTypeNode[]): ComponentNode {
export function componentNode(
name: string,
types: PropTypeNode[],
propsFilename: string | undefined
): ComponentNode {
return {
type: typeString,
name: name,
types: types || [],
propsFilename,
};
}

Expand Down
5 changes: 4 additions & 1 deletion src/types/nodes/proptype.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,18 +6,21 @@ export interface PropTypeNode extends Node {
name: string;
jsDoc?: string;
propType: Node;
filenames: Set<string>;
}

export function propTypeNode(
name: string,
jsDoc: string | undefined,
propType: Node
propType: Node,
filenames: Set<string>
): PropTypeNode {
return {
type: typeString,
name,
jsDoc,
propType,
filenames,
};
}

Expand Down
13 changes: 11 additions & 2 deletions test/index.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,12 +30,21 @@ for (const testCase of testCases) {
const ast = ttp.parseFromProgram(testCase, program, options.parser);

//#region Check AST matches
// propsFilename will be different depending on where the project is on disk
// Manually check that it's correct and then delete it
const newAST = ttp.programNode(
ast.body.map((component) => {
expect(component.propsFilename).toBe(testCase);
return { ...component, propsFilename: undefined };
})
);

if (fs.existsSync(astPath)) {
expect(ast).toMatchObject(JSON.parse(fs.readFileSync(astPath, 'utf8')));
expect(newAST).toMatchObject(JSON.parse(fs.readFileSync(astPath, 'utf8')));
} else {
fs.writeFileSync(
astPath,
prettier.format(JSON.stringify(ast), {
prettier.format(JSON.stringify(newAST), {
...prettierConfig,
filepath: astPath,
})
Expand Down
20 changes: 20 additions & 0 deletions test/injector/should-include-filename-based/input.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import * as React from 'react';

// it's technically not correct since this descripts props the component
// sees not just the one available to the user. We're abusing this to provide
// some concrete documentation for `key` regarding this component
export interface SnackBarProps extends React.HTMLAttributes<any> {
/**
* some hints about state reset that relates to prop of this component
*/
key?: any;
}

export function Snackbar(props: SnackBarProps) {
return <div {...props} />;
}

// here we don't care about `key`
export function SomeOtherComponent(props: { children?: React.ReactNode }) {
return <div>{props.children}</div>;
}
19 changes: 19 additions & 0 deletions test/injector/should-include-filename-based/options.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import * as path from 'path';
import { TestOptions } from '../../types';

const options: TestOptions = {
injector: {
includeUnusedProps: true,
shouldInclude: ({ prop }) => {
let isLocallyTyped = false;
prop.filenames.forEach((filename) => {
if (!path.relative(__dirname, filename).startsWith('..')) {
isLocallyTyped = true;
}
});
return isLocallyTyped;
},
},
};

export default options;
24 changes: 24 additions & 0 deletions test/injector/should-include-filename-based/output.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import * as React from 'react';
import PropTypes from 'prop-types';
function Snackbar(props) {
return <div {...props} />;
}
// here we don't care about `key`

Snackbar.propTypes = {
/**
* some hints about state reset that relates to prop of this component
*/
key: PropTypes.any,
};

export { Snackbar };
function SomeOtherComponent(props) {
return <div>{props.children}</div>;
}

SomeOtherComponent.propTypes = {
children: PropTypes.node,
};

export { SomeOtherComponent };
Loading

0 comments on commit ce9a700

Please sign in to comment.