From 50b535ff6ae5c0d16e0df5863c5dfff943c1e71e Mon Sep 17 00:00:00 2001 From: Pavel Vasek Date: Wed, 26 Apr 2017 22:59:10 +0200 Subject: [PATCH] Big refactoring with following goals - splitting AST transformation / deciding which parts are react components - easier to maintain - support for high order components --- .../components/HocComponent.tsx | 35 + package.json | 7 +- src/__tests__/__sourceMapInit.ts | 2 + .../data/ColumnHigherOrderComponent.tsx | 74 +++ src/__tests__/data/transformAST.tsx | 55 ++ src/__tests__/docgenConverter.spec.ts | 75 +-- ...r.spec.ts => getFileDocumentation.spec.ts} | 128 ++-- src/__tests__/transformAST.spec.ts | 111 ++++ src/docgenConverter.ts | 96 +-- src/getFileDocumentation.ts | 153 +++++ src/index.ts | 11 +- src/model.ts | 77 +++ src/nodeUtils.ts | 615 ------------------ src/parser.ts | 183 ------ src/print.ts | 16 + src/printUtils.ts | 149 +++++ src/propTypesParser.ts | 28 +- src/transformAST.ts | 181 ++++++ 18 files changed, 1022 insertions(+), 974 deletions(-) create mode 100644 examples/react-styleguidist-example/components/HocComponent.tsx create mode 100644 src/__tests__/__sourceMapInit.ts create mode 100644 src/__tests__/data/ColumnHigherOrderComponent.tsx create mode 100644 src/__tests__/data/transformAST.tsx rename src/__tests__/{parser.spec.ts => getFileDocumentation.spec.ts} (55%) create mode 100644 src/__tests__/transformAST.spec.ts create mode 100644 src/getFileDocumentation.ts create mode 100644 src/model.ts delete mode 100644 src/parser.ts create mode 100644 src/print.ts create mode 100644 src/printUtils.ts create mode 100644 src/transformAST.ts diff --git a/examples/react-styleguidist-example/components/HocComponent.tsx b/examples/react-styleguidist-example/components/HocComponent.tsx new file mode 100644 index 00000000..858721d0 --- /dev/null +++ b/examples/react-styleguidist-example/components/HocComponent.tsx @@ -0,0 +1,35 @@ +import * as React from 'react'; + +/** + * Row properties. + */ +export interface IRowProps { + /** prop1 description */ + prop1?: string; + /** prop2 description */ + prop2: number; + /** + * prop3 description + */ + prop3: () => void; + /** prop4 description */ + prop4: 'option1' | 'option2' | "option3"; +} + +/** + * Form row. + */ +class Component extends React.Component { + + render() { + return
Test
; + } +}; + +export function hoc(Component: T): T { + // do whatever you need but return the same type T + return Component as T; +} + +/** This example shows HocComponent */ +export const HocComponent = hoc(Component); \ No newline at end of file diff --git a/package.json b/package.json index 796f70fe..3fce95a1 100644 --- a/package.json +++ b/package.json @@ -1,13 +1,16 @@ { "name": "react-docgen-typescript", - "version": "0.0.8", + "version": "0.0.9", "description": "", "main": "lib/index.js", "scripts": { + "tsc": "tsc", "prepublish": "tsc -d", "test": "tsc && mocha ./lib/**/__tests__/**.js", "test:debug": "tsc && mocha --debug ./lib/**/__tests__/**.js", - "example": "tsc && node ./node_modules/react-styleguidist/bin/styleguidist server --config ./examples/react-styleguidist-example/styleguide.config.js" + "example": "tsc && node ./node_modules/react-styleguidist/bin/styleguidist server --config ./examples/react-styleguidist-example/styleguide.config.js", + "print": "npm run tsc && node ./lib/print.js", + "print:sample1": "npm run tsc && node ./lib/print.js ./src/__tests__/data/ColumnHigherOrderComponent.tsx simple" }, "author": "pvasek", "license": "MIT", diff --git a/src/__tests__/__sourceMapInit.ts b/src/__tests__/__sourceMapInit.ts new file mode 100644 index 00000000..786e316d --- /dev/null +++ b/src/__tests__/__sourceMapInit.ts @@ -0,0 +1,2 @@ +import * as sourceMapSupport from "source-map-support"; +sourceMapSupport.install(); \ No newline at end of file diff --git a/src/__tests__/data/ColumnHigherOrderComponent.tsx b/src/__tests__/data/ColumnHigherOrderComponent.tsx new file mode 100644 index 00000000..84c9de7a --- /dev/null +++ b/src/__tests__/data/ColumnHigherOrderComponent.tsx @@ -0,0 +1,74 @@ +import * as React from 'react'; +/** + * Column properties. + */ +export interface IColumnProps extends React.HTMLAttributes { + /** prop1 description */ + prop1?: string; + /** prop2 description */ + prop2: number; + /** + * prop3 description + */ + prop3: () => void; + /** prop4 description */ + prop4: 'option1' | 'option2' | "option3"; +} + +/** + * Form column. + */ +class Column extends React.Component { + public static defaultProps: Partial = { + prop1: 'prop1' + }; + + render() { + const {prop1} = this.props; + return
{prop1}
; + } +} + +/** + * Row properties. + */ +export interface IRowProps { + /** prop1 description */ + prop1?: string; + /** prop2 description */ + prop2: number; + /** + * prop3 description + */ + prop3: () => void; + /** prop4 description */ + prop4: 'option1' | 'option2' | "option3"; +} + +/** + * Form row. + */ +const Row = (props: IRowProps) => { + const innerFunc = (props: IRowProps) => { + return Inner Func + }; + const innerNonExportedFunc = (props: IRowProps) => { + return Inner Func + }; + return
Test
; +}; + +function hoc(C: T): T { + return ((props) =>
{C}
) as any as T; +} + +/** ColumnHighOrderComponent1 specific comment */ +export const ColumnHighOrderComponent1 = hoc(Column); + +export const ColumnHighOrderComponent2 = hoc(Column); + +/** RowHighOrderComponent1 specific comment */ +export const RowHighOrderComponent1 = hoc(Row); + +export const RowHighOrderComponent2 = hoc(Row); + diff --git a/src/__tests__/data/transformAST.tsx b/src/__tests__/data/transformAST.tsx new file mode 100644 index 00000000..1a0cf4d1 --- /dev/null +++ b/src/__tests__/data/transformAST.tsx @@ -0,0 +1,55 @@ +const unexportedVar = 10; +export const exportedVar = 10; + +/** unexportedVarFunction comment */ +const unexportedVarFunction = (param1: string): number => 0 +; +/** exportedVarFunction comment */ +export const exportedVarFunction = (param1: number, param2: string): string => ""; + +function unexportedFunction(param1: number): string { + return ""; +} + +function exportedFunction(param1: string, param2: number): number { + return 0; +} + +interface UnexportedInterface { + /** prop1 comment */ + prop1: string; +} + +export interface ExportedInterface { + /** prop1 comment */ + prop1: string; + /** prop2 comment */ + prop2: string; +} + +export class OurBaseClass { +} + +/** UnexportedClass comment */ +class UnexportedClass extends OurBaseClass { + method1(): string { + return ""; + } +} + +/** ExportedClass comment */ +export class ExportedClass { + method1(): string { + return ""; + } + method2(): number { + return 0; + } +} + +export function hoc(component: T): T { + return component; +} + +/** exportedHoc comment */ +export const exportedHoc = hoc(ExportedClass); \ No newline at end of file diff --git a/src/__tests__/docgenConverter.spec.ts b/src/__tests__/docgenConverter.spec.ts index 2622ec8a..b5d3e7d1 100644 --- a/src/__tests__/docgenConverter.spec.ts +++ b/src/__tests__/docgenConverter.spec.ts @@ -1,34 +1,27 @@ import { assert } from 'chai'; import * as path from 'path'; -import { getDocumentation } from '../parser'; -import { convertToDocgen, StyleguidistComponent } from "../docgenConverter"; +import { getFileDocumentation } from '../getFileDocumentation'; +import { convertToDocgen } from '../docgenConverter'; +import { StyleguidistComponent } from '../propTypesParser'; describe('docgenConverter', () => { it('Should work with class Component', () => { const result = convertToDocgen({ - classes: [ + components: [ { name: 'name1', comment: 'comment1', extends: 'Component', - propInterface: 'PropsInterface', - } - ], - interfaces: [ - { - name: 'PropsInterface', - comment: 'props comment', - members: [ - { - name: 'prop1', - comment: 'prop1 comment', - isRequired: true, - text: 'prop1 text', - type: 'prop1 type' - } - ] - } - ] + propInterface: { + name: 'PropsInterface', + comment: 'props comment', + members: [{ + name: 'prop1', + comment: 'prop1 comment', + isRequired: true, + type: 'prop1 type' + }]}, + }] }); assert.equal('name1', result.displayName); @@ -41,29 +34,21 @@ describe('docgenConverter', () => { it('Should work with functional StatelessComponent', () => { const result = convertToDocgen({ - classes: [ + components: [ { name: 'name1', comment: 'comment1', extends: 'StatelessComponent', - propInterface: 'PropsInterface', - } - ], - interfaces: [ - { - name: 'PropsInterface', - comment: 'props comment', - members: [ - { - name: 'prop1', - comment: 'prop1 comment', - isRequired: true, - text: 'prop1 text', - type: 'prop1 type' - } - ] - } - ] + propInterface: { + name: 'PropsInterface', + comment: 'props comment', + members: [{ + name: 'prop1', + comment: 'prop1 comment', + isRequired: true, + type: 'prop1 type' + }]} + }] }); assert.equal('name1', result.displayName); @@ -81,15 +66,14 @@ describe('docgenConverter', () => { console.warn = () => warnCallCount++; try { result = convertToDocgen({ - classes: [ + components: [ { name: 'name1', comment: 'comment1', extends: 'Component', propInterface: null, } - ], - interfaces: [] + ] }); } finally { console.warn = originalWarn; @@ -107,15 +91,14 @@ describe('docgenConverter', () => { console.warn = () => warnCallCount++; try { result = convertToDocgen({ - classes: [ + components: [ { name: 'name1', comment: 'comment1', extends: 'PureComponent', propInterface: null, } - ], - interfaces: [] + ] }); } finally { console.warn = originalWarn; diff --git a/src/__tests__/parser.spec.ts b/src/__tests__/getFileDocumentation.spec.ts similarity index 55% rename from src/__tests__/parser.spec.ts rename to src/__tests__/getFileDocumentation.spec.ts index e038a197..6afbd13f 100644 --- a/src/__tests__/parser.spec.ts +++ b/src/__tests__/getFileDocumentation.spec.ts @@ -1,22 +1,21 @@ import { assert } from 'chai'; import * as path from 'path'; -import { getDocumentation } from '../parser'; +import { getFileDocumentation } from '../getFileDocumentation'; -describe('parser', () => { +describe('getFileDocumentation', () => { it('Should parse class-based components', () => { const fileName = path.join(__dirname, '../../src/__tests__/data/Column.tsx'); // it's running in ./temp - const result = getDocumentation(fileName); - assert.ok(result.classes); - assert.ok(result.interfaces); - assert.equal(1, result.classes.length); - assert.equal(1, result.interfaces.length); + const result = getFileDocumentation(fileName); + assert.ok(result.components); + assert.equal(1, result.components.length); - const c = result.classes[0]; + const c = result.components[0]; assert.equal('Column', c.name); assert.equal('Form column.', c.comment); assert.equal('Component', c.extends); + assert.isNotNull(c.propInterface); - const i = result.interfaces[0]; + const i = c.propInterface; assert.equal('IColumnProps', i.name); assert.equal('Column properties.', i.comment); assert.equal(4, i.members.length); @@ -39,32 +38,50 @@ describe('parser', () => { it('Should parse class-based components with unexported props interface', () => { const fileName = path.join(__dirname, '../../src/__tests__/data/ColumnWithoutExportedProps.tsx'); // it's running in ./temp - const result = getDocumentation(fileName); - assert.ok(result.classes); - assert.ok(result.interfaces); - assert.equal(1, result.classes.length); - assert.equal(0, result.interfaces.length); + const result = getFileDocumentation(fileName); + assert.ok(result.components); + assert.equal(1, result.components.length); - const c = result.classes[0]; + const c = result.components[0]; assert.equal('Column', c.name); assert.equal('Form column.', c.comment); assert.equal('Component', c.extends); + assert.isNotNull(c.propInterface); + + const i = c.propInterface; + assert.equal('IColumnProps', i.name); + assert.equal('Column properties.', i.comment); + assert.equal(4, i.members.length); + assert.equal('prop1', i.members[0].name); + assert.equal('prop1 description', i.members[0].comment); + assert.equal(false, i.members[0].isRequired); + + assert.equal('prop2', i.members[1].name); + assert.equal('prop2 description', i.members[1].comment); + assert.equal(true, i.members[1].isRequired); + + assert.equal('prop3', i.members[2].name); + assert.equal('prop3 description', i.members[2].comment); + assert.equal(true, i.members[2].isRequired); + + assert.equal('prop4', i.members[3].name); + assert.equal('prop4 description', i.members[3].comment); + assert.equal(true, i.members[3].isRequired); }); it('Should parse functional components', () => { const fileName = path.join(__dirname, '../../src/__tests__/data/Row.tsx'); // it's running in ./temp - const result = getDocumentation(fileName); - assert.ok(result.classes); - assert.ok(result.interfaces); - assert.equal(1, result.classes.length); - assert.equal(1, result.interfaces.length); + const result = getFileDocumentation(fileName); + assert.ok(result.components); + assert.equal(1, result.components.length); - const c = result.classes[0]; + const c = result.components[0]; assert.equal('Row', c.name); assert.equal('Form row.', c.comment); assert.equal('StatelessComponent', c.extends); + assert.isNotNull(c.propInterface) - const i = result.interfaces[0]; + const i = c.propInterface; assert.equal('IRowProps', i.name); assert.equal('Row properties.', i.comment); assert.equal(4, i.members.length); @@ -87,19 +104,18 @@ describe('parser', () => { it('Should parse class-based pure components', () => { const fileName = path.join(__dirname, '../../src/__tests__/data/PureRow.tsx'); // it's running in ./temp - const result = getDocumentation(fileName); + const result = getFileDocumentation(fileName); - assert.ok(result.classes); - assert.ok(result.interfaces); - assert.equal(1, result.classes.length); - assert.equal(1, result.interfaces.length); + assert.ok(result.components); + assert.equal(1, result.components.length); - const c = result.classes[0]; + const c = result.components[0]; assert.equal('Row', c.name); assert.equal('Form row.', c.comment); assert.equal('PureComponent', c.extends); + assert.isNotNull(c.propInterface); - const i = result.interfaces[0]; + const i = c.propInterface assert.equal('IRowProps', i.name); assert.equal('Row properties.', i.comment); assert.equal(4, i.members.length); @@ -122,18 +138,17 @@ describe('parser', () => { it('Should avoid parsing exported objects as components', () => { const fileName = path.join(__dirname, '../../src/__tests__/data/ConstExport.tsx'); // it's running in ./temp - const result = getDocumentation(fileName); - assert.ok(result.classes); - assert.ok(result.interfaces); - assert.equal(1, result.classes.length); - assert.equal(1, result.interfaces.length); + const result = getFileDocumentation(fileName); + assert.ok(result.components); + assert.equal(1, result.components.length); - const c = result.classes[0]; + const c = result.components[0]; assert.equal('Row', c.name); assert.equal('Form row.', c.comment); assert.equal('StatelessComponent', c.extends); + assert.isNotNull(c); - const i = result.interfaces[0]; + const i = c.propInterface; assert.equal('IRowProps', i.name); assert.equal('Row properties.', i.comment); assert.equal(4, i.members.length); @@ -154,4 +169,43 @@ describe('parser', () => { assert.equal(true, i.members[3].isRequired); }); -}); + it('Should parse higher order components', () => { + const fileName = path.join(__dirname, '../../src/__tests__/data/ColumnHigherOrderComponent.tsx'); // it's running in ./temp + const result = getFileDocumentation(fileName); + assert.ok(result.components); + assert.equal(4, result.components.length); + + const r1 = result.components[0]; + assert.equal('ColumnHighOrderComponent1', r1.name); + assert.equal('ColumnHighOrderComponent1 specific comment', r1.comment); + assert.equal('Column', r1.extends); + assert.isNotNull(r1.propInterface); + + const i = r1.propInterface; + assert.equal('IColumnProps', i.name); + assert.equal('Column properties.', i.comment); + assert.equal(4, i.members.length); + assert.equal('prop1', i.members[0].name); + assert.equal('prop1 description', i.members[0].comment); + assert.equal(false, i.members[0].isRequired); + + assert.equal('prop2', i.members[1].name); + assert.equal('prop2 description', i.members[1].comment); + assert.equal(true, i.members[1].isRequired); + + assert.equal('prop3', i.members[2].name); + assert.equal('prop3 description', i.members[2].comment); + assert.equal(true, i.members[2].isRequired); + + assert.equal('prop4', i.members[3].name); + assert.equal('prop4 description', i.members[3].comment); + assert.equal(true, i.members[3].isRequired); + + const r2 = result.components[1]; + assert.equal('ColumnHighOrderComponent2', r2.name); + assert.equal('Form column.', r2.comment); + assert.equal('Column', r2.extends); + assert.isNotNull(r2.propInterface); + + }); +}); \ No newline at end of file diff --git a/src/__tests__/transformAST.spec.ts b/src/__tests__/transformAST.spec.ts new file mode 100644 index 00000000..d5f5f260 --- /dev/null +++ b/src/__tests__/transformAST.spec.ts @@ -0,0 +1,111 @@ +import { assert } from 'chai'; +import * as path from 'path'; +import * as ts from 'typescript'; +import { simplePrint } from '../printUtils'; +import { transformAST } from '../transformAST'; + +const defaultOptions: ts.CompilerOptions = { + target: ts.ScriptTarget.Latest, + module: ts.ModuleKind.CommonJS +}; + +describe('transformAST', () => { + const fileName = path.join(__dirname, '../../src/__tests__/data/transformAST.tsx'); // it's running in ./temp + const program = ts.createProgram([fileName], defaultOptions); + const checker = program.getTypeChecker(); + const sourceFile = program.getSourceFile(fileName); + const target = transformAST(sourceFile, checker); + + it('should provide data about variables', () => { + const result = target.variables; + assert.equal(result.length, 5); + + const r1 = result[0]; + assert.equal(r1.name, 'unexportedVar'); + assert.equal(r1.exported, false); + assert.equal(r1.kind, 'literal'); + assert.equal(r1.initializerFlags, ts.TypeFlags.NumberLiteral); + + const r2 = result[1]; + assert.equal(r2.name, 'exportedVar'); + assert.equal(r2.exported, true); + assert.equal(r2.kind, 'literal'); + assert.equal(r2.initializerFlags, ts.TypeFlags.NumberLiteral); + + const r3 = result[2]; + assert.equal(r3.name, 'unexportedVarFunction'); + assert.equal(r3.exported, false); + assert.equal(r3.kind, 'arrowFunction'); + assert.equal(r3.comment, 'unexportedVarFunction comment'); + assert.equal(r3.arrowFunctionType, 'number'); + assert.deepEqual(r3.arrowFunctionParams, ['string']); + + const r4 = result[3]; + assert.equal(r4.name, 'exportedVarFunction'); + assert.equal(r4.exported, true); + assert.equal(r4.kind, 'arrowFunction'); + assert.equal(r4.comment, 'exportedVarFunction comment'); + assert.equal(r4.arrowFunctionType, 'string'); + assert.deepEqual(r4.arrowFunctionParams, ['number', 'string']); + + // hoc + const r5 = result[4]; + assert.equal(r5.name, 'exportedHoc'); + assert.equal(r5.exported, true); + assert.equal(r5.type, 'ExportedClass'); + assert.equal(r5.kind, 'callExpression'); + assert.equal(r5.comment, 'exportedHoc comment'); + assert.deepEqual(r5.callExpressionArguments, ['ExportedClass']); + }); + + it('should provide data about interfaces', () => { + const result = target.interfaces; + assert.equal(result.length, 2); + const r1 = result[0]; + assert.equal(r1.name, 'UnexportedInterface'); + assert.equal(r1.exported, false); + assert.deepEqual(r1.properties, [{ + 'name': 'prop1', + 'text': 'prop1: string;', + 'type': 'string', + 'isRequired': true, + 'comment': 'prop1 comment', + 'values': [], + }]); + + const r2 = result[1]; + assert.equal(r2.name, 'ExportedInterface'); + assert.equal(r2.exported, true); + assert.deepEqual(r2.properties, [{ + 'name': 'prop1', + 'text': 'prop1: string;', + 'type': 'string', + 'isRequired': true, + 'comment': 'prop1 comment', + 'values': [], + }, { + 'name': 'prop2', + 'text': 'prop2: string;', + 'type': 'string', + 'isRequired': true, + 'comment': 'prop2 comment', + 'values': [], + }]); + }); + + it('should provide data about classes', () => { + const result = target.classes; + assert.equal(result.length, 3); + const r1 = result[1]; + assert.equal(r1.name, 'UnexportedClass'); + assert.equal(r1.exported, false); + assert.equal(r1.comment, 'UnexportedClass comment'); + assert.deepEqual(r1.methods, [{name: 'method1'}]); + + const r2 = result[2]; + assert.equal(r2.name, 'ExportedClass'); + assert.equal(r2.exported, true); + assert.equal(r2.comment, 'ExportedClass comment'); + assert.deepEqual(r2.methods, [{name: 'method1'}, {name: 'method2'}]); + }); +}); \ No newline at end of file diff --git a/src/docgenConverter.ts b/src/docgenConverter.ts index 7c026a97..afcaf291 100644 --- a/src/docgenConverter.ts +++ b/src/docgenConverter.ts @@ -1,58 +1,26 @@ -import { FileDoc, InterfaceDoc, MemberDoc } from './parser'; - -export interface StyleguidistProps { - [key: string]: PropItem; -} - -export interface StyleguidistComponent { - displayName: string; - description: string; - props: StyleguidistProps; -} +import { FileDoc, InterfaceDoc, MemberDoc } from './model'; +import { StyleguidistComponent, StyleguidistProps, PropItem } from './propTypesParser'; export function convertToDocgen(doc: FileDoc): StyleguidistComponent { - const reactClasses = doc.classes.filter(i => i.extends === 'Component' || i.extends === 'StatelessComponent' || i.extends === 'PureComponent'); + const components = doc.components; - if (reactClasses.length === 0) { + if (components.length === 0) { return null; } - const comp = reactClasses[0]; - const reactInterfaces = doc.interfaces.filter(i => i.name === comp.propInterface); + const comp = components[0]; - let props: any = {}; - if (reactInterfaces.length !== 0) { - props = getProps(reactInterfaces[0]); - } else { + if (!comp.propInterface) { console.warn('REACT-DOCGEN-TYPESCRIPT It seems that your props type is not exported. Add \'export\' keyword to your props definition.'); } return { displayName: comp.name, description: comp.comment, - props: props - } -} - -export interface PropItemType { - name: string; - value?: any; -} - -export interface PropItem { - required: boolean; - type: PropItemType; - description: string; - defaultValue: any; + props: comp.propInterface ? getProps(comp.propInterface) : {} + }; } -export interface PropsObject { - [key: string]: PropItem; -} -export interface Docgen { - description: string; - props: PropsObject; -} function getProps(props: InterfaceDoc): StyleguidistProps { return props.members.reduce((acc, i) => { @@ -62,55 +30,11 @@ function getProps(props: InterfaceDoc): StyleguidistProps { defaultValue: null, required: i.isRequired }; - if (i.values) { + if (i.values && i.values.length > 0) { item.description = item.description + ' (one of the following:' + i.values.join(',') + ')'; } acc[i.name] = item; return acc; }, {}); -} -/* - { - "props": { - "foo": { - "type": { - "name": "number" - }, - "required": false, - "description": "Description of prop \"foo\".", - "defaultValue": { - "value": "42", - "computed": false - } - }, - "bar": { - "type": { - "name": "custom" - }, - "required": false, - "description": "Description of prop \"bar\" (a custom validation function).", - "defaultValue": { - "value": "21", - "computed": false - } - }, - "baz": { - "type": { - "name": "union", - "value": [ - { - "name": "number" - }, - { - "name": "string" - } - ] - }, - "required": false, - "description": "" - } - }, - "description": "General component description." - } - */ \ No newline at end of file +} \ No newline at end of file diff --git a/src/getFileDocumentation.ts b/src/getFileDocumentation.ts new file mode 100644 index 00000000..963c16ab --- /dev/null +++ b/src/getFileDocumentation.ts @@ -0,0 +1,153 @@ +import * as ts from 'typescript'; +import { navigate, getFlatChildren } from './nodeUtils'; +import { transformAST } from './transformAST'; +import { + ClassEntry, + InterfaceEntry, + VariableEntry, + InterfaceDoc, + ComponentDoc, + FileDoc +} from './model'; + + +const defaultOptions: ts.CompilerOptions = { + target: ts.ScriptTarget.Latest, + module: ts.ModuleKind.CommonJS +}; + +function isClassComponent(entry: ClassEntry, exportedOnly = true): boolean { + return (exportedOnly === false || entry.exported) + && entry.baseType + && entry.baseType.name.indexOf('Component') > -1 + && entry.methods.some(j => j.name === 'render'); +} + +function isVarComponent( + entry: VariableEntry, + interfaces: InterfaceEntry[], + exportedOnly = true): boolean { + + return (exportedOnly === false || entry.exported) + && entry.kind === 'arrowFunction' + && entry.arrowFunctionParams.length === 1 + && interfaces.some(i => i.name === entry.arrowFunctionParams[0]); +} + +function isHocClassComponent(entry: VariableEntry, classes: ClassEntry[]): boolean { + return entry.exported + && entry.kind === 'callExpression' + && entry.type !== null + && classes + .filter(i => isClassComponent(i, false)) + .some(i => i.name === entry.type); +} + +function isVarClassComponent( + entry: VariableEntry, + variables: VariableEntry[], + interfaces: InterfaceEntry[]): boolean { + + return entry.exported + && entry.kind === 'callExpression' + && entry.type === '__function' + && entry.callExpressionArguments.length === 1 + && variables + .filter(i => isVarComponent(i, interfaces, false)) + .some(i => i.name === entry.callExpressionArguments[0]); +} + +function getInterfaceDoc(entry: InterfaceEntry): InterfaceDoc { + return { + name: entry.name, + comment: entry.comment, + members: entry.properties + } +} + +function getClassPropInterface(interfaces: InterfaceEntry[], classEntry: ClassEntry): InterfaceDoc { + if (classEntry.baseType.typeArguments.length === 0) { + return null; + } + + const propInterfaceName = classEntry.baseType.typeArguments[0]; + return getPropInterface(interfaces, propInterfaceName); +} + +function getVarPropInterface(interfaces: InterfaceEntry[], varEntry: VariableEntry): InterfaceDoc { + const propInterfaceName = varEntry.arrowFunctionParams[0]; + return getPropInterface(interfaces, varEntry.arrowFunctionParams[0]); +} + +function getPropInterface(interfaces: InterfaceEntry[], propInterfaceName: string): InterfaceDoc { + const matchedInterfaces = interfaces.filter(j => j.name === propInterfaceName); + if (matchedInterfaces.length === 0) { + console.warn(`Property interface ${propInterfaceName} cannot be found`) + return null; + } + + return getInterfaceDoc(matchedInterfaces[0]); +} +/** Generate documention for all classes in a set of .ts files */ +export function getFileDocumentation(fileName: string, options: ts.CompilerOptions = defaultOptions): FileDoc { + const components: ComponentDoc[] = []; + let program = ts.createProgram([fileName], options); + let checker = program.getTypeChecker(); + const model = transformAST(program.getSourceFile(fileName), checker); + + const { interfaces, classes, variables } = model; + + const classComponents: ComponentDoc[] = classes + .filter(i => isClassComponent(i)) + .map(i => ({ + name: i.name, + extends: i.baseType.name, + comment: i.comment, + propInterface: getClassPropInterface(interfaces, i), + })); + + const varComponents = variables + .filter(i => isVarComponent(i, interfaces)) + .map(i => ({ + name: i.name, + extends: 'StatelessComponent', + comment: i.comment, + propInterface: getVarPropInterface(interfaces, i), + })); + + const hocClassComponents = variables + .filter(i => isHocClassComponent(i, classes)) + .map(i => ({ + variable: i, + origin: classes.filter(c => c.name === i.type)[0] + })) + .map(i => ({ + name: i.variable.name, + extends: i.variable.type, + comment: i.variable.comment || i.origin.comment, + propInterface: getClassPropInterface(interfaces, i.origin), + })); + + const hocVarComponents = variables + .filter(i => isVarClassComponent(i, variables, interfaces)) + .map(i => ({ + variable: i, + origin: variables.filter(c => c.name === i.callExpressionArguments[0])[0] + })) + .map(i => ({ + name: i.variable.name, + extends: i.variable.type, + comment: i.variable.comment || i.origin.comment, + propInterface: getVarPropInterface(interfaces, i.origin), + })); + + return { + components: [ + ...classComponents, + ...varComponents, + ...hocClassComponents, + ...hocVarComponents, + ] + }; +} + \ No newline at end of file diff --git a/src/index.ts b/src/index.ts index 5a916887..6039f85b 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,8 +1,15 @@ -import { parse } from './propTypesParser'; -import { StyleguidistComponent, StyleguidistProps } from './docgenConverter'; +import { + parse, + StyleguidistComponent, + StyleguidistProps, + PropItem, + PropItemType, +} from './propTypesParser'; export { parse, StyleguidistComponent, StyleguidistProps, + PropItem, + PropItemType, } \ No newline at end of file diff --git a/src/model.ts b/src/model.ts new file mode 100644 index 00000000..cd91393f --- /dev/null +++ b/src/model.ts @@ -0,0 +1,77 @@ +import * as ts from 'typescript'; + +export interface MemberType { + type: string; + values?: string[]; +} + +export interface InterfaceEntry { + name: string; + properties: PropertyEntry[]; + exported: boolean; + comment: string; +} + +export type VeriableKind = 'arrowFunction' | 'literal' | 'callExpression' | 'unknown'; + +export interface VariableEntry { + name: string; + exported: boolean; + kind: VeriableKind; + type: string; + comment: string; + initializerFlags: ts.TypeFlags; + arrowFunctionType: string; + arrowFunctionParams: string[]; + callExpressionArguments: string[]; +} + +export interface PropertyEntry { + name: string; + type: string; + values: string[]; + isRequired: boolean; + comment: string; +} + +export interface MethodEntry { + name: string; +} + +export interface BaseClassEntry { + name: string; + typeArguments: string[]; +} + +export interface ClassEntry { + name: string; + exported: boolean; + comment: string; + baseType: BaseClassEntry; + methods: MethodEntry[]; +} + +export interface ComponentDoc { + name: string; + extends: string; + propInterface: InterfaceDoc; + comment: string; +} + +export interface InterfaceDoc { + name: string; + members: MemberDoc[]; + comment: string; +} + +export interface MemberDoc { + name: string; + type: string; + values?: string[]; + isRequired: boolean; + comment: string; +} + +export interface FileDoc { + components: ComponentDoc[]; +} \ No newline at end of file diff --git a/src/nodeUtils.ts b/src/nodeUtils.ts index 0dcdd28c..e0a853f1 100644 --- a/src/nodeUtils.ts +++ b/src/nodeUtils.ts @@ -28,619 +28,4 @@ export function getFlatChildren(node: ts.Node): ts.Node[] { f(node); return result; -} - -export function dumpNode(node: ts.Node, prefix = '') { - const a = node as any; - - const info = (a.name ? a.name.text : '') - || (a.text ? a.text : ''); - - console.log(prefix, kindToString(node.kind), ' ' + info); - - ts.forEachChild(node, i => dumpNode(i, prefix + ' ')); -} - - -function kindToString(kind: ts.SyntaxKind) { - switch (kind) { - case ts.SyntaxKind.EndOfFileToken: - return 'EndOfFileToken'; - case ts.SyntaxKind.SingleLineCommentTrivia: - return 'SingleLineCommentTrivia'; - case ts.SyntaxKind.MultiLineCommentTrivia: - return 'MultiLineCommentTrivia'; - case ts.SyntaxKind.NewLineTrivia: - return 'NewLineTrivia'; - case ts.SyntaxKind.WhitespaceTrivia: - return 'WhitespaceTrivia'; - case ts.SyntaxKind.ShebangTrivia: - return 'ShebangTrivia'; - case ts.SyntaxKind.ConflictMarkerTrivia: - return 'ConflictMarkerTrivia'; - case ts.SyntaxKind.NumericLiteral: - return 'NumericLiteral'; - case ts.SyntaxKind.StringLiteral: - return 'StringLiteral'; - case ts.SyntaxKind.RegularExpressionLiteral: - return 'RegularExpressionLiteral'; - case ts.SyntaxKind.NoSubstitutionTemplateLiteral: - return 'NoSubstitutionTemplateLiteral'; - case ts.SyntaxKind.TemplateHead: - return 'TemplateHead'; - case ts.SyntaxKind.TemplateMiddle: - return 'TemplateMiddle'; - case ts.SyntaxKind.TemplateTail: - return 'TemplateTail'; - case ts.SyntaxKind.OpenBraceToken: - return 'OpenBraceToken'; - case ts.SyntaxKind.CloseBraceToken: - return 'CloseBraceToken'; - case ts.SyntaxKind.OpenParenToken: - return 'OpenParenToken'; - case ts.SyntaxKind.CloseParenToken: - return 'CloseParenToken'; - case ts.SyntaxKind.OpenBracketToken: - return 'OpenBracketToken'; - case ts.SyntaxKind.CloseBracketToken: - return 'CloseBracketToken'; - case ts.SyntaxKind.DotToken: - return 'DotToken'; - case ts.SyntaxKind.DotDotDotToken: - return 'DotDotDotToken'; - case ts.SyntaxKind.SemicolonToken: - return 'SemicolonToken'; - case ts.SyntaxKind.CommaToken: - return 'CommaToken'; - case ts.SyntaxKind.LessThanToken: - return 'LessThanToken'; - case ts.SyntaxKind.LessThanSlashToken: - return 'LessThanSlashToken'; - case ts.SyntaxKind.GreaterThanToken: - return 'GreaterThanToken'; - case ts.SyntaxKind.LessThanEqualsToken: - return 'LessThanEqualsToken'; - case ts.SyntaxKind.GreaterThanEqualsToken: - return 'GreaterThanEqualsToken'; - case ts.SyntaxKind.EqualsEqualsToken: - return 'EqualsEqualsToken'; - case ts.SyntaxKind.ExclamationEqualsToken: - return 'ExclamationEqualsToken'; - case ts.SyntaxKind.EqualsEqualsEqualsToken: - return 'EqualsEqualsEqualsToken'; - case ts.SyntaxKind.ExclamationEqualsEqualsToken: - return 'ExclamationEqualsEqualsToken'; - case ts.SyntaxKind.EqualsGreaterThanToken: - return 'EqualsGreaterThanToken'; - case ts.SyntaxKind.PlusToken: - return 'PlusToken'; - case ts.SyntaxKind.MinusToken: - return 'MinusToken'; - case ts.SyntaxKind.AsteriskToken: - return 'AsteriskToken'; - case ts.SyntaxKind.AsteriskAsteriskToken: - return 'AsteriskAsteriskToken'; - case ts.SyntaxKind.SlashToken: - return 'SlashToken'; - case ts.SyntaxKind.PercentToken: - return 'PercentToken'; - case ts.SyntaxKind.PlusPlusToken: - return 'PlusPlusToken'; - case ts.SyntaxKind.MinusMinusToken: - return 'MinusMinusToken'; - case ts.SyntaxKind.LessThanLessThanToken: - return 'LessThanLessThanToken'; - case ts.SyntaxKind.GreaterThanGreaterThanToken: - return 'GreaterThanGreaterThanToken'; - case ts.SyntaxKind.GreaterThanGreaterThanGreaterThanToken: - return 'GreaterThanGreaterThanGreaterThanToken'; - case ts.SyntaxKind.AmpersandToken: - return 'AmpersandToken'; - case ts.SyntaxKind.BarToken: - return 'BarToken'; - case ts.SyntaxKind.CaretToken: - return 'CaretToken'; - case ts.SyntaxKind.ExclamationToken: - return 'ExclamationToken'; - case ts.SyntaxKind.TildeToken: - return 'TildeToken'; - case ts.SyntaxKind.AmpersandAmpersandToken: - return 'AmpersandAmpersandToken'; - case ts.SyntaxKind.BarBarToken: - return 'BarBarToken'; - case ts.SyntaxKind.QuestionToken: - return 'QuestionToken'; - case ts.SyntaxKind.ColonToken: - return 'ColonToken'; - case ts.SyntaxKind.AtToken: - return 'AtToken'; - case ts.SyntaxKind.EqualsToken: - return 'EqualsToken'; - case ts.SyntaxKind.PlusEqualsToken: - return 'PlusEqualsToken'; - case ts.SyntaxKind.MinusEqualsToken: - return 'MinusEqualsToken'; - case ts.SyntaxKind.AsteriskEqualsToken: - return 'AsteriskEqualsToken'; - case ts.SyntaxKind.AsteriskAsteriskEqualsToken: - return 'AsteriskAsteriskEqualsToken'; - case ts.SyntaxKind.SlashEqualsToken: - return 'SlashEqualsToken'; - case ts.SyntaxKind.PercentEqualsToken: - return 'PercentEqualsToken'; - case ts.SyntaxKind.LessThanLessThanEqualsToken: - return 'LessThanLessThanEqualsToken'; - case ts.SyntaxKind.GreaterThanGreaterThanEqualsToken: - return 'GreaterThanGreaterThanEqualsToken'; - case ts.SyntaxKind.GreaterThanGreaterThanGreaterThanEqualsToken: - return 'GreaterThanGreaterThanGreaterThanEqualsToken'; - case ts.SyntaxKind.AmpersandEqualsToken: - return 'AmpersandEqualsToken'; - case ts.SyntaxKind.BarEqualsToken: - return 'BarEqualsToken'; - case ts.SyntaxKind.CaretEqualsToken: - return 'CaretEqualsToken'; - case ts.SyntaxKind.Identifier: - return 'Identifier'; - case ts.SyntaxKind.BreakKeyword: - return 'BreakKeyword'; - case ts.SyntaxKind.CaseKeyword: - return 'CaseKeyword'; - case ts.SyntaxKind.CatchKeyword: - return 'CatchKeyword'; - case ts.SyntaxKind.ClassKeyword: - return 'ClassKeyword'; - case ts.SyntaxKind.ConstKeyword: - return 'ConstKeyword'; - case ts.SyntaxKind.ContinueKeyword: - return 'ContinueKeyword'; - case ts.SyntaxKind.DebuggerKeyword: - return 'DebuggerKeyword'; - case ts.SyntaxKind.DefaultKeyword: - return 'DefaultKeyword'; - case ts.SyntaxKind.DeleteKeyword: - return 'DeleteKeyword'; - case ts.SyntaxKind.DoKeyword: - return 'DoKeyword'; - case ts.SyntaxKind.ElseKeyword: - return 'ElseKeyword'; - case ts.SyntaxKind.EnumKeyword: - return 'EnumKeyword'; - case ts.SyntaxKind.ExportKeyword: - return 'ExportKeyword'; - case ts.SyntaxKind.ExtendsKeyword: - return 'ExtendsKeyword'; - case ts.SyntaxKind.FalseKeyword: - return 'FalseKeyword'; - case ts.SyntaxKind.FinallyKeyword: - return 'FinallyKeyword'; - case ts.SyntaxKind.ForKeyword: - return 'ForKeyword'; - case ts.SyntaxKind.FunctionKeyword: - return 'FunctionKeyword'; - case ts.SyntaxKind.IfKeyword: - return 'IfKeyword'; - case ts.SyntaxKind.ImportKeyword: - return 'ImportKeyword'; - case ts.SyntaxKind.InKeyword: - return 'InKeyword'; - case ts.SyntaxKind.InstanceOfKeyword: - return 'InstanceOfKeyword'; - case ts.SyntaxKind.NewKeyword: - return 'NewKeyword'; - case ts.SyntaxKind.NullKeyword: - return 'NullKeyword'; - case ts.SyntaxKind.ReturnKeyword: - return 'ReturnKeyword'; - case ts.SyntaxKind.SuperKeyword: - return 'SuperKeyword'; - case ts.SyntaxKind.SwitchKeyword: - return 'SwitchKeyword'; - case ts.SyntaxKind.ThisKeyword: - return 'ThisKeyword'; - case ts.SyntaxKind.ThrowKeyword: - return 'ThrowKeyword'; - case ts.SyntaxKind.TrueKeyword: - return 'TrueKeyword'; - case ts.SyntaxKind.TryKeyword: - return 'TryKeyword'; - case ts.SyntaxKind.TypeOfKeyword: - return 'TypeOfKeyword'; - case ts.SyntaxKind.VarKeyword: - return 'VarKeyword'; - case ts.SyntaxKind.VoidKeyword: - return 'VoidKeyword'; - case ts.SyntaxKind.WhileKeyword: - return 'WhileKeyword'; - case ts.SyntaxKind.WithKeyword: - return 'WithKeyword'; - case ts.SyntaxKind.ImplementsKeyword: - return 'ImplementsKeyword'; - case ts.SyntaxKind.InterfaceKeyword: - return 'InterfaceKeyword'; - case ts.SyntaxKind.LetKeyword: - return 'LetKeyword'; - case ts.SyntaxKind.PackageKeyword: - return 'PackageKeyword'; - case ts.SyntaxKind.PrivateKeyword: - return 'PrivateKeyword'; - case ts.SyntaxKind.ProtectedKeyword: - return 'ProtectedKeyword'; - case ts.SyntaxKind.PublicKeyword: - return 'PublicKeyword'; - case ts.SyntaxKind.StaticKeyword: - return 'StaticKeyword'; - case ts.SyntaxKind.YieldKeyword: - return 'YieldKeyword'; - case ts.SyntaxKind.AbstractKeyword: - return 'AbstractKeyword'; - case ts.SyntaxKind.AsKeyword: - return 'AsKeyword'; - case ts.SyntaxKind.AnyKeyword: - return 'AnyKeyword'; - case ts.SyntaxKind.AsyncKeyword: - return 'AsyncKeyword'; - case ts.SyntaxKind.AwaitKeyword: - return 'AwaitKeyword'; - case ts.SyntaxKind.BooleanKeyword: - return 'BooleanKeyword'; - case ts.SyntaxKind.ConstructorKeyword: - return 'ConstructorKeyword'; - case ts.SyntaxKind.DeclareKeyword: - return 'DeclareKeyword'; - case ts.SyntaxKind.GetKeyword: - return 'GetKeyword'; - case ts.SyntaxKind.IsKeyword: - return 'IsKeyword'; - case ts.SyntaxKind.ModuleKeyword: - return 'ModuleKeyword'; - case ts.SyntaxKind.NamespaceKeyword: - return 'NamespaceKeyword'; - case ts.SyntaxKind.RequireKeyword: - return 'RequireKeyword'; - case ts.SyntaxKind.NumberKeyword: - return 'NumberKeyword'; - case ts.SyntaxKind.SetKeyword: - return 'SetKeyword'; - case ts.SyntaxKind.StringKeyword: - return 'StringKeyword'; - case ts.SyntaxKind.SymbolKeyword: - return 'SymbolKeyword'; - case ts.SyntaxKind.TypeKeyword: - return 'TypeKeyword'; - case ts.SyntaxKind.FromKeyword: - return 'FromKeyword'; - case ts.SyntaxKind.GlobalKeyword: - return 'GlobalKeyword'; - case ts.SyntaxKind.OfKeyword: - return 'OfKeyword'; - case ts.SyntaxKind.QualifiedName: - return 'QualifiedName'; - case ts.SyntaxKind.ComputedPropertyName: - return 'ComputedPropertyName'; - case ts.SyntaxKind.TypeParameter: - return 'TypeParameter'; - case ts.SyntaxKind.Parameter: - return 'Parameter'; - case ts.SyntaxKind.Decorator: - return 'Decorator'; - case ts.SyntaxKind.PropertySignature: - return 'PropertySignature'; - case ts.SyntaxKind.PropertyDeclaration: - return 'PropertyDeclaration'; - case ts.SyntaxKind.MethodSignature: - return 'MethodSignature'; - case ts.SyntaxKind.MethodDeclaration: - return 'MethodDeclaration'; - case ts.SyntaxKind.Constructor: - return 'Constructor'; - case ts.SyntaxKind.GetAccessor: - return 'GetAccessor'; - case ts.SyntaxKind.SetAccessor: - return 'SetAccessor'; - case ts.SyntaxKind.CallSignature: - return 'CallSignature'; - case ts.SyntaxKind.ConstructSignature: - return 'ConstructSignature'; - case ts.SyntaxKind.IndexSignature: - return 'IndexSignature'; - case ts.SyntaxKind.TypePredicate: - return 'TypePredicate'; - case ts.SyntaxKind.TypeReference: - return 'TypeReference'; - case ts.SyntaxKind.FunctionType: - return 'FunctionType'; - case ts.SyntaxKind.ConstructorType: - return 'ConstructorType'; - case ts.SyntaxKind.TypeQuery: - return 'TypeQuery'; - case ts.SyntaxKind.TypeLiteral: - return 'TypeLiteral'; - case ts.SyntaxKind.ArrayType: - return 'ArrayType'; - case ts.SyntaxKind.TupleType: - return 'TupleType'; - case ts.SyntaxKind.UnionType: - return 'UnionType'; - case ts.SyntaxKind.IntersectionType: - return 'IntersectionType'; - case ts.SyntaxKind.ParenthesizedType: - return 'ParenthesizedType'; - case ts.SyntaxKind.ThisType: - return 'ThisType'; - case ts.SyntaxKind.StringLiteral: - return 'StringLiteral'; - case ts.SyntaxKind.ObjectBindingPattern: - return 'ObjectBindingPattern'; - case ts.SyntaxKind.ArrayBindingPattern: - return 'ArrayBindingPattern'; - case ts.SyntaxKind.BindingElement: - return 'BindingElement'; - case ts.SyntaxKind.ArrayLiteralExpression: - return 'ArrayLiteralExpression'; - case ts.SyntaxKind.ObjectLiteralExpression: - return 'ObjectLiteralExpression'; - case ts.SyntaxKind.PropertyAccessExpression: - return 'PropertyAccessExpression'; - case ts.SyntaxKind.ElementAccessExpression: - return 'ElementAccessExpression'; - case ts.SyntaxKind.CallExpression: - return 'CallExpression'; - case ts.SyntaxKind.NewExpression: - return 'NewExpression'; - case ts.SyntaxKind.TaggedTemplateExpression: - return 'TaggedTemplateExpression'; - case ts.SyntaxKind.TypeAssertionExpression: - return 'TypeAssertionExpression'; - case ts.SyntaxKind.ParenthesizedExpression: - return 'ParenthesizedExpression'; - case ts.SyntaxKind.FunctionExpression: - return 'FunctionExpression'; - case ts.SyntaxKind.ArrowFunction: - return 'ArrowFunction'; - case ts.SyntaxKind.DeleteExpression: - return 'DeleteExpression'; - case ts.SyntaxKind.TypeOfExpression: - return 'TypeOfExpression'; - case ts.SyntaxKind.VoidExpression: - return 'VoidExpression'; - case ts.SyntaxKind.AwaitExpression: - return 'AwaitExpression'; - case ts.SyntaxKind.PrefixUnaryExpression: - return 'PrefixUnaryExpression'; - case ts.SyntaxKind.PostfixUnaryExpression: - return 'PostfixUnaryExpression'; - case ts.SyntaxKind.BinaryExpression: - return 'BinaryExpression'; - case ts.SyntaxKind.ConditionalExpression: - return 'ConditionalExpression'; - case ts.SyntaxKind.TemplateExpression: - return 'TemplateExpression'; - case ts.SyntaxKind.YieldExpression: - return 'YieldExpression'; - case ts.SyntaxKind.SpreadElement: - return 'SpreadElement'; - case ts.SyntaxKind.ClassExpression: - return 'ClassExpression'; - case ts.SyntaxKind.OmittedExpression: - return 'OmittedExpression'; - case ts.SyntaxKind.ExpressionWithTypeArguments: - return 'ExpressionWithTypeArguments'; - case ts.SyntaxKind.AsExpression: - return 'AsExpression'; - case ts.SyntaxKind.TemplateSpan: - return 'TemplateSpan'; - case ts.SyntaxKind.SemicolonClassElement: - return 'SemicolonClassElement'; - case ts.SyntaxKind.Block: - return 'Block'; - case ts.SyntaxKind.VariableStatement: - return 'VariableStatement'; - case ts.SyntaxKind.EmptyStatement: - return 'EmptyStatement'; - case ts.SyntaxKind.ExpressionStatement: - return 'ExpressionStatement'; - case ts.SyntaxKind.IfStatement: - return 'IfStatement'; - case ts.SyntaxKind.DoStatement: - return 'DoStatement'; - case ts.SyntaxKind.WhileStatement: - return 'WhileStatement'; - case ts.SyntaxKind.ForStatement: - return 'ForStatement'; - case ts.SyntaxKind.ForInStatement: - return 'ForInStatement'; - case ts.SyntaxKind.ForOfStatement: - return 'ForOfStatement'; - case ts.SyntaxKind.ContinueStatement: - return 'ContinueStatement'; - case ts.SyntaxKind.BreakStatement: - return 'BreakStatement'; - case ts.SyntaxKind.ReturnStatement: - return 'ReturnStatement'; - case ts.SyntaxKind.WithStatement: - return 'WithStatement'; - case ts.SyntaxKind.SwitchStatement: - return 'SwitchStatement'; - case ts.SyntaxKind.LabeledStatement: - return 'LabeledStatement'; - case ts.SyntaxKind.ThrowStatement: - return 'ThrowStatement'; - case ts.SyntaxKind.TryStatement: - return 'TryStatement'; - case ts.SyntaxKind.DebuggerStatement: - return 'DebuggerStatement'; - case ts.SyntaxKind.VariableDeclaration: - return 'VariableDeclaration'; - case ts.SyntaxKind.VariableDeclarationList: - return 'VariableDeclarationList'; - case ts.SyntaxKind.FunctionDeclaration: - return 'FunctionDeclaration'; - case ts.SyntaxKind.ClassDeclaration: - return 'ClassDeclaration'; - case ts.SyntaxKind.InterfaceDeclaration: - return 'InterfaceDeclaration'; - case ts.SyntaxKind.TypeAliasDeclaration: - return 'TypeAliasDeclaration'; - case ts.SyntaxKind.EnumDeclaration: - return 'EnumDeclaration'; - case ts.SyntaxKind.ModuleDeclaration: - return 'ModuleDeclaration'; - case ts.SyntaxKind.ModuleBlock: - return 'ModuleBlock'; - case ts.SyntaxKind.CaseBlock: - return 'CaseBlock'; - case ts.SyntaxKind.ImportEqualsDeclaration: - return 'ImportEqualsDeclaration'; - case ts.SyntaxKind.ImportDeclaration: - return 'ImportDeclaration'; - case ts.SyntaxKind.ImportClause: - return 'ImportClause'; - case ts.SyntaxKind.NamespaceImport: - return 'NamespaceImport'; - case ts.SyntaxKind.NamedImports: - return 'NamedImports'; - case ts.SyntaxKind.ImportSpecifier: - return 'ImportSpecifier'; - case ts.SyntaxKind.ExportAssignment: - return 'ExportAssignment'; - case ts.SyntaxKind.ExportDeclaration: - return 'ExportDeclaration'; - case ts.SyntaxKind.NamedExports: - return 'NamedExports'; - case ts.SyntaxKind.ExportSpecifier: - return 'ExportSpecifier'; - case ts.SyntaxKind.MissingDeclaration: - return 'MissingDeclaration'; - case ts.SyntaxKind.ExternalModuleReference: - return 'ExternalModuleReference'; - case ts.SyntaxKind.JsxElement: - return 'JsxElement'; - case ts.SyntaxKind.JsxSelfClosingElement: - return 'JsxSelfClosingElement'; - case ts.SyntaxKind.JsxOpeningElement: - return 'JsxOpeningElement'; - case ts.SyntaxKind.JsxText: - return 'JsxText'; - case ts.SyntaxKind.JsxClosingElement: - return 'JsxClosingElement'; - case ts.SyntaxKind.JsxAttribute: - return 'JsxAttribute'; - case ts.SyntaxKind.JsxSpreadAttribute: - return 'JsxSpreadAttribute'; - case ts.SyntaxKind.JsxExpression: - return 'JsxExpression'; - case ts.SyntaxKind.CaseClause: - return 'CaseClause'; - case ts.SyntaxKind.DefaultClause: - return 'DefaultClause'; - case ts.SyntaxKind.HeritageClause: - return 'HeritageClause'; - case ts.SyntaxKind.CatchClause: - return 'CatchClause'; - case ts.SyntaxKind.PropertyAssignment: - return 'PropertyAssignment'; - case ts.SyntaxKind.ShorthandPropertyAssignment: - return 'ShorthandPropertyAssignment'; - case ts.SyntaxKind.EnumMember: - return 'EnumMember'; - case ts.SyntaxKind.SourceFile: - return 'SourceFile'; - case ts.SyntaxKind.JSDocTypeExpression: - return 'JSDocTypeExpression'; - case ts.SyntaxKind.JSDocAllType: - return 'JSDocAllType'; - case ts.SyntaxKind.JSDocUnknownType: - return 'JSDocUnknownType'; - case ts.SyntaxKind.JSDocArrayType: - return 'JSDocArrayType'; - case ts.SyntaxKind.JSDocUnionType: - return 'JSDocUnionType'; - case ts.SyntaxKind.JSDocTupleType: - return 'JSDocTupleType'; - case ts.SyntaxKind.JSDocNullableType: - return 'JSDocNullableType'; - case ts.SyntaxKind.JSDocNonNullableType: - return 'JSDocNonNullableType'; - case ts.SyntaxKind.JSDocRecordType: - return 'JSDocRecordType'; - case ts.SyntaxKind.JSDocRecordMember: - return 'JSDocRecordMember'; - case ts.SyntaxKind.JSDocTypeReference: - return 'JSDocTypeReference'; - case ts.SyntaxKind.JSDocOptionalType: - return 'JSDocOptionalType'; - case ts.SyntaxKind.JSDocFunctionType: - return 'JSDocFunctionType'; - case ts.SyntaxKind.JSDocVariadicType: - return 'JSDocVariadicType'; - case ts.SyntaxKind.JSDocConstructorType: - return 'JSDocConstructorType'; - case ts.SyntaxKind.JSDocThisType: - return 'JSDocThisType'; - case ts.SyntaxKind.JSDocComment: - return 'JSDocComment'; - case ts.SyntaxKind.JSDocTag: - return 'JSDocTag'; - case ts.SyntaxKind.JSDocParameterTag: - return 'JSDocParameterTag'; - case ts.SyntaxKind.JSDocReturnTag: - return 'JSDocReturnTag'; - case ts.SyntaxKind.JSDocTypeTag: - return 'JSDocTypeTag'; - case ts.SyntaxKind.JSDocTemplateTag: - return 'JSDocTemplateTag'; - case ts.SyntaxKind.SyntaxList: - return 'SyntaxList'; - case ts.SyntaxKind.Count: - return 'Count'; - case ts.SyntaxKind.FirstAssignment: - return 'FirstAssignment'; - case ts.SyntaxKind.LastAssignment: - return 'LastAssignment'; - case ts.SyntaxKind.FirstReservedWord: - return 'FirstReservedWord'; - case ts.SyntaxKind.LastReservedWord: - return 'LastReservedWord'; - case ts.SyntaxKind.FirstKeyword: - return 'FirstKeyword'; - case ts.SyntaxKind.LastKeyword: - return 'LastKeyword'; - case ts.SyntaxKind.FirstFutureReservedWord: - return 'FirstFutureReservedWord'; - case ts.SyntaxKind.LastFutureReservedWord: - return 'LastFutureReservedWord'; - case ts.SyntaxKind.FirstTypeNode: - return 'FirstTypeNode'; - case ts.SyntaxKind.LastTypeNode: - return 'LastTypeNode'; - case ts.SyntaxKind.FirstPunctuation: - return 'FirstPunctuation'; - case ts.SyntaxKind.LastPunctuation: - return 'LastPunctuation'; - case ts.SyntaxKind.FirstToken: - return 'FirstToken'; - case ts.SyntaxKind.LastToken: - return 'LastToken'; - case ts.SyntaxKind.FirstTriviaToken: - return 'FirstTriviaToken'; - case ts.SyntaxKind.LastTriviaToken: - return 'LastTriviaToken'; - case ts.SyntaxKind.FirstLiteralToken: - return 'FirstLiteralToken'; - case ts.SyntaxKind.LastLiteralToken: - return 'LastLiteralToken'; - case ts.SyntaxKind.FirstTemplateToken: - return 'FirstTemplateToken'; - case ts.SyntaxKind.LastTemplateToken: - return 'LastTemplateToken'; - case ts.SyntaxKind.FirstBinaryOperator: - return 'FirstBinaryOperator'; - case ts.SyntaxKind.LastBinaryOperator: - return 'LastBinaryOperator'; - case ts.SyntaxKind.FirstNode: - return 'FirstNode'; - default: - return 'Unknown'; - } } \ No newline at end of file diff --git a/src/parser.ts b/src/parser.ts deleted file mode 100644 index 0cbfbfd1..00000000 --- a/src/parser.ts +++ /dev/null @@ -1,183 +0,0 @@ -import * as ts from 'typescript'; -import { navigate, getFlatChildren } from './nodeUtils'; - - -const defaultOptions: ts.CompilerOptions = { - target: ts.ScriptTarget.Latest, - module: ts.ModuleKind.CommonJS -}; - -export interface ClassDoc { - name: string; - extends: string; - propInterface: string; - comment: string; -} - -export interface InterfaceDoc { - name: string; - members: MemberDoc[]; - comment: string; -} - -export interface MemberDoc { - name: string; - text: string; - type: string; - values?: string[]; - isRequired: boolean; - comment: string; -} - -export interface FileDoc { - classes: ClassDoc[]; - interfaces: InterfaceDoc[]; -} -/** Generate documention for all classes in a set of .ts files */ -export function getDocumentation(fileName: string, options: ts.CompilerOptions = defaultOptions): FileDoc { - - let program = ts.createProgram([fileName], options); - //noinspection TypeScriptUnresolvedFunction - let checker = program.getTypeChecker(); - - const classes: ClassDoc[] = []; - const interfaces: InterfaceDoc[] = []; - - //noinspection TypeScriptUnresolvedFunction - const sourceFile = program.getSourceFile(fileName); - ts.forEachChild(sourceFile, visit); - - /** visit nodes finding exported classes */ - function visit(node: ts.Node) { - // Only consider exported nodes - if (!isNodeExported(node)) { - return; - } - - if (node.kind === ts.SyntaxKind.VariableStatement) { - const classNode: any = (node as ts.VariableStatement).declarationList.declarations[0]; - - if (!classNode.initializer.parameters || !classNode.initializer.parameters[0].type.typeName) { - return; - } - - const symbol = classNode.symbol; - const intf = classNode.initializer.parameters[0].type.typeName.getText(); - const classObj = { - name: symbol.name, - comment: ts.displayPartsToString(symbol.getDocumentationComment()), - extends: 'StatelessComponent', - propInterface: intf, - }; - classes.push(classObj); - } - if (node.kind === ts.SyntaxKind.ClassDeclaration) { - const classNode = node as ts.ClassDeclaration; - const symbol = checker.getSymbolAtLocation(classNode.name); - - const typeArguments = navigate(classNode, - ts.SyntaxKind.HeritageClause, - ts.SyntaxKind.ExpressionWithTypeArguments); - - const list = getFlatChildren(typeArguments) - .filter(i => i.kind === ts.SyntaxKind.Identifier) - .map((i: ts.Identifier) => i.text); - - const componentIndex = list.indexOf('Component'); - let propsIndex = -1; - let extendsClass; - - if (componentIndex > -1) { - propsIndex = componentIndex + 1; - extendsClass = 'Component'; - } else { - const pureIndex = list.indexOf('PureComponent'); - - propsIndex = pureIndex === -1 ? -1 : (pureIndex + 1); - extendsClass = pureIndex === -1 ? null : 'PureComponent'; - } - - classes.push({ - name: symbol.name, - comment: ts.displayPartsToString(symbol.getDocumentationComment()), - extends: list.length > 0 ? extendsClass : null, - propInterface: list.length > propsIndex ? list[propsIndex] : null, - }); - } - - if (node.kind === ts.SyntaxKind.InterfaceDeclaration) { - const interfaceDeclaration = node as ts.InterfaceDeclaration; - if (interfaceDeclaration.parent === sourceFile) { - - const symbol = checker.getSymbolAtLocation(interfaceDeclaration.name); - const type = checker.getTypeAtLocation(interfaceDeclaration.name); - - const members = type.getProperties() - .filter(i => { - const s = i as any; - return s.parent && s.parent.name && symbol.name === s.parent.name; - }) - .map(i => { - const symbol = checker.getSymbolAtLocation(i.valueDeclaration.name); - const prop = i.valueDeclaration as ts.PropertySignature; - const typeInfo = getType(prop); - return { - name: i.getName(), - text: i.valueDeclaration.getText(), - type: typeInfo.type, - values: typeInfo.values, - isRequired: !prop.questionToken, - comment: ts.displayPartsToString(symbol.getDocumentationComment()).trim(), - }; - }); - - const interfaceDoc: InterfaceDoc = { - name: symbol.getName(), - comment: ts.displayPartsToString(symbol.getDocumentationComment()).trim(), - members: members, - }; - interfaces.push(interfaceDoc); - } - } - else if (node.kind === ts.SyntaxKind.ModuleDeclaration) { - // This is a namespace, visit its children - ts.forEachChild(node, visit); - } - } - - /** True if this is visible outside this file, false otherwise */ - function isNodeExported(node: ts.Node): boolean { - // Parse the modifier array for the export keyword. If it is found - // and the node.parent is a sourcefile, return true - // This only returns top level exports - const {modifiers} = node; - if (modifiers) { - for (let i = 0; i < (modifiers as Array).length; i++) { - if (modifiers[i].kind === ts.SyntaxKind.ExportKeyword) { - return node.parent.kind === ts.SyntaxKind.SourceFile - } - } - } - return false; - } - - return { - classes, - interfaces, - } -} - -function getType(prop: ts.PropertySignature): {type: string, values?: string[]} { - const unionType = prop.type as ts.UnionTypeNode; - if (unionType && unionType.types) { - return { - type: 'string', - values: (unionType.types as Array).map(i => i.getText()), - } - } - //noinspection TypeScriptUnresolvedFunction - return { - type: prop.type.getText(), - } -} - \ No newline at end of file diff --git a/src/print.ts b/src/print.ts new file mode 100644 index 00000000..932ae231 --- /dev/null +++ b/src/print.ts @@ -0,0 +1,16 @@ +import * as ts from 'typescript'; +import { simplePrint } from './printUtils'; +const args = process.argv.splice(2); +const fileName = args[0]; +console.log('Priting file: ', fileName, ' args: ', args); + +const defaultOptions: ts.CompilerOptions = { + target: ts.ScriptTarget.Latest, + module: ts.ModuleKind.CommonJS +}; + +const program = ts.createProgram([fileName], defaultOptions); +const sourceFile = program.getSourceFile(fileName); +const checker = program.getTypeChecker(); + +simplePrint(checker, sourceFile); diff --git a/src/printUtils.ts b/src/printUtils.ts new file mode 100644 index 00000000..0d604faf --- /dev/null +++ b/src/printUtils.ts @@ -0,0 +1,149 @@ +import * as ts from 'typescript'; + +const removeList = [ + 'parent', + '_children', + 'statements', + 'pos', + 'end', + 'modifierFlagsCache', + 'transformFlags', + 'flowNode', + 'parent', +]; + +export function syntaxKindToName(kind: ts.SyntaxKind) { + return (ts).SyntaxKind[kind]; +} + +export function flagsToText(kind: ts.TypeFlags) { + return (ts).TypeFlags[kind]; +} + +/** True if this is visible outside this file, false otherwise */ +function isNodeExported(node: ts.Node): boolean { + // Parse the modifier array for the export keyword. If it is found + // and the node.parent is a sourcefile, return true + // This only returns top level exports + const { modifiers } = node; + if (modifiers) { + for (let i = 0; i < (modifiers as Array).length; i++) { + if (modifiers[i].kind === ts.SyntaxKind.ExportKeyword) { + return node.parent.kind === ts.SyntaxKind.SourceFile + } + } + } + return false; +} + +function symbolMapToString(map: ts.Map): string { + const values = []; + map.forEach((value, key) => { + values.push(value.name); + }); + return values.join('; '); +} + +export function simplePrint(checker: ts.TypeChecker, node: ts.Node, indent = 0) { + const indentText = Array(indent * 4).join(' '); + const info: string[] = []; + + function addSymbol(info: string[], node: ts.Node, prefix: string = '') { + const s = checker.getSymbolAtLocation(node); + if (!s) { + return; + } + + if (s.exports && s.exports.size > 0) { + info.push(prefix + 'exports: ' + symbolMapToString(s.exports)); + } + + if (s.globalExports && s.globalExports.size > 0) { + info.push(prefix + 'globalExports: ' + symbolMapToString(s.exports)); + } + + info.push(prefix + 'name: ' + s.name); + const type = checker.getTypeOfSymbolAtLocation(s, node); + if (type && type.symbol) { + info.push(prefix + 'type: ' + type.symbol.name); + } + const comments = s.getDocumentationComment(); + if (comments.length > 0) { + info.push(prefix + 'comment: \'' + comments.map(i => i.text).join('; ') + '\''); + } + } + + if (node.kind === ts.SyntaxKind.FunctionDeclaration) { + const d = node as ts.FunctionDeclaration; + info.push('name: ' + d.name.text); + info.push('type: ' + d.type.getText()); + } + + if (node.kind === ts.SyntaxKind.ClassDeclaration) { + const d = node as ts.ClassDeclaration; + info.push('name: ' + d.name.text); + info.push('members: [' + d.members.map(i => i.name.getText()).join(', ') + ']'); + } + + if (node.kind === ts.SyntaxKind.Identifier) { + const d = node as ts.Identifier; + addSymbol(info, node); + } + + if (node.kind === ts.SyntaxKind.InterfaceDeclaration) { + const d = node as ts.InterfaceDeclaration; + addSymbol(info, node); + addSymbol(info, d.name, 'name-'); + } + + if (node.kind === ts.SyntaxKind.ExportDeclaration) { + const d = node as ts.ExportDeclaration; + info.push('name: ' + d.name); + addSymbol(info, node); + } + + if (node.kind === ts.SyntaxKind.ExportAssignment) { + const d = node as ts.ExportAssignment; + info.push('name: ' + d.name); + addSymbol(info, node); + } + + if (node.kind === ts.SyntaxKind.VariableDeclaration) { + const d = node as ts.VariableDeclaration; + addSymbol(info, d.name, 'name-'); + addSymbol(info, node); + } + + if (node.kind === ts.SyntaxKind.VariableStatement) { + const d = node as ts.VariableStatement; + if (d.declarationList && d.declarationList.declarations.length === 1) { + const dec = d.declarationList.declarations[0]; + info.push('declarations: ' + `(kind: ${syntaxKindToName(dec.kind)}, name: ${dec.name.getText()}, name.kind: ${syntaxKindToName(dec.name.kind)})`); + if (dec.type) { + info.push('declaration.type: ' + syntaxKindToName(dec.type.kind)); + } + if (dec.initializer) { + info.push('declaration.initializer: ' + syntaxKindToName(dec.initializer.kind)); + } + } + addSymbol(info, node); + } + + if (node.kind === ts.SyntaxKind.CallExpression) { + const d = node as ts.CallExpression; + info.push('arguments: ' + d.arguments.map(i => i.getText()).join(';')); + if (d.contextualType) { + info.push('contextualType: ' + d.contextualType.symbol.name); + } + } + + if (isNodeExported(node)) { + info.push('exported: true'); + } + + const infoText = info.length === 0 ? '' : `(${info.join(', ')})` + console.log(`${indentText}${syntaxKindToName(node.kind)} ${infoText}`); + ts.forEachChild(node, child => { + simplePrint(checker, child, indent + 1); + }); +} \ No newline at end of file diff --git a/src/propTypesParser.ts b/src/propTypesParser.ts index 59b9dda0..df0d9a42 100644 --- a/src/propTypesParser.ts +++ b/src/propTypesParser.ts @@ -1,10 +1,32 @@ -import { getDocumentation } from './parser'; -import { convertToDocgen, StyleguidistComponent } from './docgenConverter'; +import { convertToDocgen } from './docgenConverter'; +import { getFileDocumentation } from './getFileDocumentation'; + +export interface StyleguidistComponent { + displayName: string; + description: string; + props: StyleguidistProps; +} + +export interface StyleguidistProps { + [key: string]: PropItem; +} + +export interface PropItem { + required: boolean; + type: PropItemType; + description: string; + defaultValue: any; +} + +export interface PropItemType { + name: string; + value?: any; +} /** * Parser given file and return documentation in format compatibe with react-docgen. */ export function parse(filePath: string): StyleguidistComponent { - const doc = getDocumentation(filePath); + const doc = getFileDocumentation(filePath); return convertToDocgen(doc); } \ No newline at end of file diff --git a/src/transformAST.ts b/src/transformAST.ts new file mode 100644 index 00000000..f77af1bf --- /dev/null +++ b/src/transformAST.ts @@ -0,0 +1,181 @@ +import * as ts from 'typescript'; +import { navigate } from './nodeUtils'; +import { + MemberType, + VariableEntry, + VeriableKind, + InterfaceEntry, + ClassEntry, + PropertyEntry +} from './model'; + +/** + * Checks if the node is exported. + */ +function isNodeExported(node: ts.Node): boolean { + // Parse the modifier array for the export keyword. If it is found + // and the node.parent is a sourcefile, return true + // This only returns top level exports + const { modifiers } = node; + if (modifiers) { + for (let i = 0; i < (modifiers as Array).length; i++) { + if (modifiers[i].kind === ts.SyntaxKind.ExportKeyword) { + return node.parent.kind === ts.SyntaxKind.SourceFile + } + } + } + return false; +} + +function getType(prop: ts.PropertySignature): MemberType { + const unionType = prop.type as ts.UnionTypeNode; + if (unionType && unionType.types) { + return { + type: 'string', + values: (unionType.types as Array).map(i => i.getText()), + } + } + //noinspection TypeScriptUnresolvedFunction + return { + type: prop.type.getText(), + } +} + +function getMethods(checker: ts.TypeChecker, type: ts.Type, classDeclaratinNode: ts.ClassDeclaration) { + return classDeclaratinNode.members + .map(i => ({ name: i.name.getText() })); +} + +function getProperties(checker: ts.TypeChecker, type: ts.Type, interfaceDeclaratinNode: ts.InterfaceDeclaration): PropertyEntry[] { + + return type.getProperties() + .filter(i => i.valueDeclaration.parent === interfaceDeclaratinNode) + .map(i => { + const symbol = checker.getSymbolAtLocation(i.valueDeclaration.name); + const prop = i.valueDeclaration as ts.PropertySignature; + const typeInfo = getType(prop); + return { + name: i.getName(), + text: i.valueDeclaration.getText(), + type: typeInfo.type, + values: typeInfo.values || [], + isRequired: !prop.questionToken, + comment: ts.displayPartsToString(symbol.getDocumentationComment()).trim(), + }; + }); +} + +function findAllNodes(rootNode: ts.Node, result: ts.Node[]) { + result.push(rootNode); + ts.forEachChild(rootNode, (node) => { + findAllNodes(node, result); + }); +} + +/** + * Transform source file AST (abstract syntax tree) to our + * model (classes, interfaces, variables, methods). + */ +export function transformAST(sourceFile: ts.SourceFile, checker: ts.TypeChecker) { + const nodes = []; + findAllNodes(sourceFile, nodes); + + const variables: VariableEntry[] = nodes + .filter(i => i.kind === ts.SyntaxKind.VariableStatement) + .map(i => i as ts.VariableStatement) + .filter(i => i.declarationList.declarations + && i.declarationList.declarations.length === 1 + && i.declarationList.declarations[0].name.kind === ts.SyntaxKind.Identifier) + .map(i => { + const d = i.declarationList.declarations[0] as ts.VariableDeclaration; + const identifier = d.name as ts.Identifier; + const symbol = checker.getSymbolAtLocation(identifier); + let arrowFunctionType: string = null; + let literalFlags: ts.TypeFlags = null; + let kind: VeriableKind = 'unknown'; + const varType = checker.getTypeAtLocation(d); + + const initializerType = checker.getTypeAtLocation(d.initializer); + const initializerFlags = initializerType.flags; + let arrowFunctionParams = []; + let callExpressionArguments = []; + if (d.initializer.kind === ts.SyntaxKind.ArrowFunction) { + const arrowFunc = d.initializer as ts.ArrowFunction; + if (arrowFunc.parameters) { + arrowFunctionParams = arrowFunc.parameters.map(i => i.type.getText()) + } + arrowFunctionType = arrowFunc.type ? arrowFunc.type.getText() : 'undefined'; + kind = 'arrowFunction' + } else if (d.initializer.kind === ts.SyntaxKind.FirstLiteralToken) { + const literal = d.initializer as ts.LiteralExpression; + kind = 'literal'; + } else if (d.initializer.kind === ts.SyntaxKind.CallExpression) { + kind = 'callExpression'; + const callExpresson = d.initializer as ts.CallExpression; + if (callExpresson.arguments) { + callExpressionArguments = callExpresson.arguments.map(i => i.getText()); + } + } + + return { + name: identifier.text, + exported: isNodeExported(i), + comment: symbol ? ts.displayPartsToString(symbol.getDocumentationComment()).trim() : '', + kind, + type: varType.symbol ? varType.symbol.getName() : null, + arrowFunctionType, + arrowFunctionParams, + callExpressionArguments, + literalFlags, + initializerFlags, + }; + }); + + const interfaces: InterfaceEntry[] = nodes + .filter(i => i.kind === ts.SyntaxKind.InterfaceDeclaration) + .map(i => i as ts.InterfaceDeclaration) + .map(i => { + const symbol = checker.getSymbolAtLocation(i.name); + const type = checker.getTypeAtLocation(i.name); + return { + name: symbol.name, + comment: ts.displayPartsToString(symbol.getDocumentationComment()).trim(), + exported: isNodeExported(i), + properties: getProperties(checker, type, i), + }; + }); + + const classes: ClassEntry[] = nodes + .filter(i => i.kind === ts.SyntaxKind.ClassDeclaration) + .map(i => i as ts.ClassDeclaration) + .map(i => { + const symbol = checker.getSymbolAtLocation(i.name); + const type = checker.getTypeAtLocation(i.name); + const baseTypes = type.getBaseTypes(); + let baseType = null; + if (baseTypes.length) { + const t = baseTypes[0]; + const typeArguments = navigate(i, + ts.SyntaxKind.HeritageClause, + ts.SyntaxKind.ExpressionWithTypeArguments) as ts.ExpressionWithTypeArguments; + baseType = { + name: t.symbol.getName(), + typeArguments: typeArguments ? typeArguments.typeArguments.map(t => t.getText()) : [] + }; + } + + return { + name: symbol.name, + exported: isNodeExported(i), + baseType: baseType, + comment: ts.displayPartsToString(symbol.getDocumentationComment()).trim(), + methods: getMethods(checker, type, i), + }; + }); + + return { + classes, + interfaces, + variables, + } +}