Filter out strings and numbers
This commit is contained in:
parent
3ba279a0ab
commit
d1072031d6
@ -9,6 +9,27 @@ function isTypeReference(type: ts.ObjectType): type is ts.TypeReference {
|
|||||||
return !!(type.objectFlags & ts.ObjectFlags.Reference)
|
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() {
|
export function typecheck() {
|
||||||
/** Generate interfaces for all exported classes in a set of .ts files */
|
/** Generate interfaces for all exported classes in a set of .ts files */
|
||||||
function generateInterfaces(
|
function generateInterfaces(
|
||||||
@ -21,33 +42,62 @@ export function typecheck() {
|
|||||||
// Get the checker, we will use it to find more about classes
|
// Get the checker, we will use it to find more about classes
|
||||||
const checker = program.getTypeChecker()
|
const checker = program.getTypeChecker()
|
||||||
|
|
||||||
// Visit every sourceFile in the program
|
const classDefs: IClassDefinition[] = []
|
||||||
for (const sourceFile of program.getSourceFiles()) {
|
|
||||||
if (!sourceFile.isDeclarationFile) {
|
|
||||||
// Walk the tree to search for classes
|
|
||||||
ts.forEachChild(sourceFile, visit)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return
|
|
||||||
|
|
||||||
function typeToString(type: ts.Type): string {
|
function typeToString(type: ts.Type): string {
|
||||||
return checker.typeToString(type)
|
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 {
|
function filterGlobalTypes(type: ts.Type): boolean {
|
||||||
|
console.log('filterGlobalTypes', typeToString(type))
|
||||||
const symbol = type.getSymbol()
|
const symbol = type.getSymbol()
|
||||||
|
if (!symbol) {
|
||||||
|
console.log(' no symbol')
|
||||||
|
return false
|
||||||
|
}
|
||||||
if (symbol && !((symbol as any).parent)) {
|
if (symbol && !((symbol as any).parent)) {
|
||||||
|
console.log(' no parent')
|
||||||
// e.g. Array symbol has no parent
|
// e.g. Array symbol has no parent
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
if (type.isLiteral()) {
|
||||||
|
console.log(' is literal')
|
||||||
|
return false
|
||||||
|
}
|
||||||
if (type.isUnionOrIntersection()) {
|
if (type.isUnionOrIntersection()) {
|
||||||
|
console.log(' is union or intersection')
|
||||||
// union type params should have already been extracted
|
// union type params should have already been extracted
|
||||||
return false
|
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
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Converts a generic type to the target of the type reference.
|
||||||
|
*/
|
||||||
function mapGenericTypes(type: ts.Type): ts.Type {
|
function mapGenericTypes(type: ts.Type): ts.Type {
|
||||||
if (isObjectType(type) && isTypeReference(type)) {
|
if (isObjectType(type) && isTypeReference(type)) {
|
||||||
return type.target
|
return type.target
|
||||||
@ -55,11 +105,17 @@ export function typecheck() {
|
|||||||
return type
|
return type
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Removes duplicates from an array of types
|
||||||
|
*/
|
||||||
function filterDuplicates(type: ts.Type, i: number, arr: ts.Type[]) {
|
function filterDuplicates(type: ts.Type, i: number, arr: ts.Type[]) {
|
||||||
// TODO improve performance of this method
|
// TODO improve performance of this method
|
||||||
return i === arr.indexOf(type)
|
return i === arr.indexOf(type)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Recursively retrieves a list of all type parameters.
|
||||||
|
*/
|
||||||
function getAllTypeParameters(type: ts.Type): ts.Type[] {
|
function getAllTypeParameters(type: ts.Type): ts.Type[] {
|
||||||
if (isObjectType(type) && isTypeReference(type)) {
|
if (isObjectType(type) && isTypeReference(type)) {
|
||||||
const types: ts.Type[] = [type]
|
const types: ts.Type[] = [type]
|
||||||
@ -99,7 +155,20 @@ export function typecheck() {
|
|||||||
return [type]
|
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) {
|
function visit(node: ts.Node) {
|
||||||
// Only consider exported nodes
|
// Only consider exported nodes
|
||||||
if (!isNodeExported(node)) {
|
if (!isNodeExported(node)) {
|
||||||
@ -110,74 +179,102 @@ export function typecheck() {
|
|||||||
// This is a top level class, get its symbol
|
// This is a top level class, get its symbol
|
||||||
const symbol = checker.getSymbolAtLocation(node.name)
|
const symbol = checker.getSymbolAtLocation(node.name)
|
||||||
|
|
||||||
const typeParameters: ts.Type[] = []
|
const typeParameters: ts.TypeParameter[] = []
|
||||||
|
const expandedTypeParameters: ts.Type[] = []
|
||||||
|
const allRelevantTypes: ts.Type[] = []
|
||||||
if (symbol) {
|
if (symbol) {
|
||||||
console.log('===')
|
// console.log('===')
|
||||||
// console.log('text', node.getText(node.getSourceFile()))
|
// console.log('text', node.getText(node.getSourceFile()))
|
||||||
console.log('class', symbol.getName())
|
// console.log('class', symbol.getName())
|
||||||
const type = checker.getDeclaredTypeOfSymbol(symbol)
|
const type = checker.getDeclaredTypeOfSymbol(symbol)
|
||||||
|
|
||||||
if (type.isClassOrInterface() && type.typeParameters) {
|
if (type.isClassOrInterface() && type.typeParameters) {
|
||||||
type.typeParameters.forEach(tp => {
|
type.typeParameters.forEach(tp => {
|
||||||
console.log(' tp.symbol.name', tp.symbol.name)
|
// console.log(' tp.symbol.name', tp.symbol.name)
|
||||||
const constraint = tp.getConstraint()
|
const constraint = tp.getConstraint()
|
||||||
if (constraint) {
|
if (constraint) {
|
||||||
// TODO call getAllTypeParameters here...
|
expandedTypeParameters.push(...getAllTypeParameters(tp))
|
||||||
console.log(' tp.constraint',
|
|
||||||
checker.typeToString(constraint))
|
|
||||||
}
|
}
|
||||||
const def = tp.getDefault()
|
const def = tp.getDefault()
|
||||||
if (def) {
|
if (def) {
|
||||||
// TODO call getAllTypeParameters here...
|
expandedTypeParameters.push(...getAllTypeParameters(tp))
|
||||||
console.log(' tp.default', checker.typeToString(def))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
typeParameters.push(tp)
|
typeParameters.push(tp)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
// const properties = checker.getPropertiesOfType(type)
|
|
||||||
const properties = type.getApparentProperties()
|
const properties = type.getApparentProperties()
|
||||||
|
|
||||||
console.log(' %o', properties
|
const filterClassTypeParameters =
|
||||||
.filter(p => {
|
(t: ts.Type) => typeParameters.every(tp => tp !== t)
|
||||||
const flags = ts.getCombinedModifierFlags(p.valueDeclaration)
|
|
||||||
return !(flags & ts.ModifierFlags.NonPublicAccessibilityModifier)
|
const classProperties: IClassProperty[] = properties
|
||||||
})
|
.filter(filterInvisibleProperties)
|
||||||
.map(p => {
|
.map(p => {
|
||||||
const vd = p.valueDeclaration
|
const vd = p.valueDeclaration
|
||||||
const questionToken =
|
const optional = ts.isPropertyDeclaration(vd) && !!vd.questionToken
|
||||||
ts.isPropertyDeclaration(vd) && !!vd.questionToken
|
|
||||||
|
|
||||||
const propType = checker
|
const propType = checker.getTypeOfSymbolAtLocation(p, vd)
|
||||||
.getTypeOfSymbolAtLocation(p, p.valueDeclaration)
|
|
||||||
|
|
||||||
const typeParams = getAllTypeParameters(propType)
|
const typeParams = getAllTypeParameters(propType)
|
||||||
return {
|
|
||||||
name: p.getName(),
|
const relevantTypes = typeParams
|
||||||
type: checker.typeToString(propType),
|
|
||||||
questionToken,
|
|
||||||
typeParams: typeParams.map(typeToString),
|
|
||||||
filteredTypeParams: typeParams
|
|
||||||
.filter(filterGlobalTypes)
|
.filter(filterGlobalTypes)
|
||||||
// filter class type parameters
|
.filter(filterClassTypeParameters)
|
||||||
.filter(t => typeParameters.every(tp => tp !== t))
|
|
||||||
.map(mapGenericTypes)
|
.map(mapGenericTypes)
|
||||||
.filter(filterDuplicates)
|
.filter(filterDuplicates)
|
||||||
.map(typeToString),
|
|
||||||
|
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 */
|
// Visit every sourceFile in the program
|
||||||
function isNodeExported(node: ts.Node): boolean {
|
for (const sourceFile of program.getSourceFiles()) {
|
||||||
return (
|
if (!sourceFile.isDeclarationFile) {
|
||||||
(ts.getCombinedModifierFlags(node as any) &
|
// Walk the tree to search for classes
|
||||||
ts.ModifierFlags.Export) !== 0 ||
|
ts.forEachChild(sourceFile, visit)
|
||||||
(!!node.parent && node.parent.kind === ts.SyntaxKind.SourceFile)
|
}
|
||||||
)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user