diff --git a/packages/scripts/src/commands/typecheck.ts b/packages/scripts/src/commands/typecheck.ts index 9843a68..7fde1c5 100644 --- a/packages/scripts/src/commands/typecheck.ts +++ b/packages/scripts/src/commands/typecheck.ts @@ -9,6 +9,27 @@ function isTypeReference(type: ts.ObjectType): type is ts.TypeReference { return !!(type.objectFlags & ts.ObjectFlags.Reference) } +function filterInvisibleProperties(type: ts.Symbol): boolean { + const flags = ts.getCombinedModifierFlags(type.valueDeclaration) + return !(flags & ts.ModifierFlags.NonPublicAccessibilityModifier) +} + +interface IClassProperty { + name: string + type: ts.Type + relevantTypes: ts.Type[] + typeString: string + optional: boolean +} + +interface IClassDefinition { + name: string + typeParameters: ts.TypeParameter[] + relevantTypeParameters: ts.Type[] + allRelevantTypes: ts.Type[] + properties: IClassProperty[] +} + export function typecheck() { /** Generate interfaces for all exported classes in a set of .ts files */ function generateInterfaces( @@ -21,33 +42,62 @@ export function typecheck() { // Get the checker, we will use it to find more about classes const checker = program.getTypeChecker() - // Visit every sourceFile in the program - for (const sourceFile of program.getSourceFiles()) { - if (!sourceFile.isDeclarationFile) { - // Walk the tree to search for classes - ts.forEachChild(sourceFile, visit) - } - } - - return + const classDefs: IClassDefinition[] = [] function typeToString(type: ts.Type): string { return checker.typeToString(type) } + /** + * Can be used to filters out global types like Array or string from a list + * of types. For example: types.filter(filterGlobalTypes) + */ function filterGlobalTypes(type: ts.Type): boolean { + console.log('filterGlobalTypes', typeToString(type)) const symbol = type.getSymbol() + if (!symbol) { + console.log(' no symbol') + return false + } if (symbol && !((symbol as any).parent)) { + console.log(' no parent') // e.g. Array symbol has no parent return false } + if (type.isLiteral()) { + console.log(' is literal') + return false + } if (type.isUnionOrIntersection()) { + console.log(' is union or intersection') // union type params should have already been extracted return false } + if (isObjectType(type) && isTypeReference(type)) { + console.log(' is reference') + if (isObjectType(type.target) + && type.target.objectFlags & ts.ObjectFlags.Tuple) { + return false + } + } + // if (isObjectType(type) && type.objectFlags & ts.ObjectFlags.Tuple) { + // console.log(' is tuple') + // // tuple params should have already been extracted + // return false + // } + + console.log(' ', + type.flags, + (type as any).objectFlags, + !!symbol, + symbol && !!(symbol as any).parent, + ) return true } + /** + * Converts a generic type to the target of the type reference. + */ function mapGenericTypes(type: ts.Type): ts.Type { if (isObjectType(type) && isTypeReference(type)) { return type.target @@ -55,11 +105,17 @@ export function typecheck() { return type } + /** + * Removes duplicates from an array of types + */ function filterDuplicates(type: ts.Type, i: number, arr: ts.Type[]) { // TODO improve performance of this method return i === arr.indexOf(type) } + /** + * Recursively retrieves a list of all type parameters. + */ function getAllTypeParameters(type: ts.Type): ts.Type[] { if (isObjectType(type) && isTypeReference(type)) { const types: ts.Type[] = [type] @@ -99,7 +155,20 @@ export function typecheck() { return [type] } - /** visit nodes finding exported classes */ + /** + * True if this is visible outside this file, false otherwise + */ + function isNodeExported(node: ts.Node): boolean { + return ( + (ts.getCombinedModifierFlags(node as any) & + ts.ModifierFlags.Export) !== 0 || + (!!node.parent && node.parent.kind === ts.SyntaxKind.SourceFile) + ) + } + + /** + * Visit nodes finding exported classes + */ function visit(node: ts.Node) { // Only consider exported nodes if (!isNodeExported(node)) { @@ -110,74 +179,102 @@ export function typecheck() { // This is a top level class, get its symbol const symbol = checker.getSymbolAtLocation(node.name) - const typeParameters: ts.Type[] = [] + const typeParameters: ts.TypeParameter[] = [] + const expandedTypeParameters: ts.Type[] = [] + const allRelevantTypes: ts.Type[] = [] if (symbol) { - console.log('===') + // console.log('===') // console.log('text', node.getText(node.getSourceFile())) - console.log('class', symbol.getName()) + // console.log('class', symbol.getName()) const type = checker.getDeclaredTypeOfSymbol(symbol) if (type.isClassOrInterface() && type.typeParameters) { type.typeParameters.forEach(tp => { - console.log(' tp.symbol.name', tp.symbol.name) + // console.log(' tp.symbol.name', tp.symbol.name) const constraint = tp.getConstraint() if (constraint) { - // TODO call getAllTypeParameters here... - console.log(' tp.constraint', - checker.typeToString(constraint)) + expandedTypeParameters.push(...getAllTypeParameters(tp)) } const def = tp.getDefault() if (def) { - // TODO call getAllTypeParameters here... - console.log(' tp.default', checker.typeToString(def)) + expandedTypeParameters.push(...getAllTypeParameters(tp)) } typeParameters.push(tp) }) } - // const properties = checker.getPropertiesOfType(type) const properties = type.getApparentProperties() - console.log(' %o', properties - .filter(p => { - const flags = ts.getCombinedModifierFlags(p.valueDeclaration) - return !(flags & ts.ModifierFlags.NonPublicAccessibilityModifier) - }) - .map(p => { - const vd = p.valueDeclaration - const questionToken = - ts.isPropertyDeclaration(vd) && !!vd.questionToken + const filterClassTypeParameters = + (t: ts.Type) => typeParameters.every(tp => tp !== t) - const propType = checker - .getTypeOfSymbolAtLocation(p, p.valueDeclaration) + const classProperties: IClassProperty[] = properties + .filter(filterInvisibleProperties) + .map(p => { + const vd = p.valueDeclaration + const optional = ts.isPropertyDeclaration(vd) && !!vd.questionToken - const typeParams = getAllTypeParameters(propType) - return { - name: p.getName(), - type: checker.typeToString(propType), - questionToken, - typeParams: typeParams.map(typeToString), - filteredTypeParams: typeParams - .filter(filterGlobalTypes) - // filter class type parameters - .filter(t => typeParameters.every(tp => tp !== t)) - .map(mapGenericTypes) - .filter(filterDuplicates) - .map(typeToString), - } - })) + const propType = checker.getTypeOfSymbolAtLocation(p, vd) + + const typeParams = getAllTypeParameters(propType) + + const relevantTypes = typeParams + .filter(filterGlobalTypes) + .filter(filterClassTypeParameters) + .map(mapGenericTypes) + .filter(filterDuplicates) + + allRelevantTypes.push(...relevantTypes) + + return { + name: p.getName(), + type: propType, + relevantTypes, + typeString: typeToString(type), + optional, + } + }) + + const relevantTypeParameters = expandedTypeParameters + .filter(filterGlobalTypes) + .filter(mapGenericTypes) + .filter(filterDuplicates) + + allRelevantTypes.push(...relevantTypeParameters) + + const classDef: IClassDefinition = { + name: symbol.getName(), + typeParameters, + allRelevantTypes: allRelevantTypes + .filter(filterClassTypeParameters) + .filter(filterDuplicates), + relevantTypeParameters, + properties: classProperties, + } + + console.log(classDef.name) + console.log(' ', + classDef.properties + .map(p => p.name + ': ' + typeToString(p.type) + ' {' + + p.relevantTypes.map(typeToString) + '}') + .join('\n '), + ) + console.log('\n allRelevantTypes:\n ', + classDef.allRelevantTypes.map(typeToString).join('\n ')) + console.log('\n') + + classDefs.push(classDef) } } } - /** True if this is visible outside this file, false otherwise */ - function isNodeExported(node: ts.Node): boolean { - return ( - (ts.getCombinedModifierFlags(node as any) & - ts.ModifierFlags.Export) !== 0 || - (!!node.parent && node.parent.kind === ts.SyntaxKind.SourceFile) - ) + // Visit every sourceFile in the program + for (const sourceFile of program.getSourceFiles()) { + if (!sourceFile.isDeclarationFile) { + // Walk the tree to search for classes + ts.forEachChild(sourceFile, visit) + } } }