I am trying to parse a class and create default values of the property declarations that does not have values on the declaration.
For example from this:
class MyClass {
name = 'My Class';
id: string;
}
functionCall<MyClass>() // <-- I get the hold of the type class here
I want to generate something like this:
{
name: 'My Class',
id: 'I am generated'
}
I get MyClass
from functionCall<MyClass>
as type: ts.InterfaceType
node and from there I parse the properties:
const typeChecker = program.getTypeChecker();
const type = typeChecker.getTypeAtLocation(callExpression); // This is the node where `functioCall<MyClass>()` is
type.getProperties().forEach(symbol => {
// I am stuck here
});
The problem I have there, is that some symbols
have symbol.valueDeclaration
as undefined and I do not know what to use to know
- The type of the property
- If an assignment is done.
I though check if a symbol.valueDeclaration
exist and if their symbol.valueDeclaration.kind
is some keyword (like SyntaxKind.StringKeyword
) but it would not work for something like
class SomeClass {
noKeyword; // probably any?
}
Any help or pointers would be appreciated.
CodePudding user response:
I was almost there, I needed to check if in the children of the valueDeclaration
a literal was defined
const propertyDeclaration = symbol.valueDeclaration;
if (ts.isPropertyDeclaration(propertyDeclaration)) {
const children = propertyDeclaration.getChildren();
const assignationLiteral = children.find(child => {
ts.isLiteralTypeNode;
return [
ts.SyntaxKind.NumericLiteral,
ts.SyntaxKind.StringLiteral,
ts.SyntaxKind.ObjectLiteralExpression,
].includes(child.kind);
});
if (assignationLiteral) {
// A value es defined!
} {
// I have a keyword, I have to check which one is it :)
}
}
CodePudding user response:
This is from a script to get run time types as objects:
/*
Used to handle types from:
InterfaceDeclaration,
ClassDeclaration,
TypeLiteral
*/
function getTypeDescriptor(
property: ts.PropertyDeclaration | ts.PropertySignature,
typeChecker: ts.TypeChecker,
): ts.Expression {
const { type } = property;
if (type !== undefined) {
if (property.initializer) {
switch (property.initializer.kind) {
case ts.SyntaxKind.StringLiteral:
return ts.createLiteral('string');
case ts.SyntaxKind.FirstLiteralToken:
return ts.createLiteral('number');
}
}
return ts.createLiteral('any');
}
return getDescriptor(type, typeChecker);
}
// Create expression based on objects or property types
export function getDescriptor(type: ts.Node | undefined, typeChecker: ts.TypeChecker): ts.Expression {
if (!type) {
return ts.createLiteral('unknown');
}
switch (type.kind) {
case ts.SyntaxKind.PropertySignature:
return getDescriptor((type as ts.PropertySignature).type, typeChecker);
case ts.SyntaxKind.StringKeyword:
return ts.createLiteral('string');
case ts.SyntaxKind.NumberKeyword:
return ts.createLiteral('number');
case ts.SyntaxKind.BooleanKeyword:
return ts.createLiteral('boolean');
case ts.SyntaxKind.AnyKeyword:
return ts.createLiteral('any');
case ts.SyntaxKind.TypeAliasDeclaration:
case ts.SyntaxKind.TypeReference:
const symbol = typeChecker.getSymbolAtLocation((type as ts.TypeReferenceNode).typeName);
const declaration = ((symbol && symbol.declarations) || [])[0];
return getDescriptor(declaration, typeChecker);
case ts.SyntaxKind.ArrayType:
return ts.createArrayLiteral([getDescriptor((type as ts.ArrayTypeNode).elementType, typeChecker)]);
case ts.SyntaxKind.InterfaceDeclaration:
case ts.SyntaxKind.ClassDeclaration:
case ts.SyntaxKind.TypeLiteral:
return ts.createObjectLiteral(
(type as ts.TypeLiteralNode).members.map(member =>
ts.createPropertyAssignment(
member.name || '',
getTypeDescriptor(member as ts.PropertySignature, typeChecker),
),
),
);
default:
throw new Error('Unknown type ' ts.SyntaxKind[type.kind]);
}
}
Example
With this class:
class X {
public id: number = 1;
}
The property initializer would look like this:
<ref *2> TokenObject {
parent: <ref *1> NodeObject {
parent: NodeObject {
parent: [SourceFileObject],
kind: SyntaxKind.ClassDeclaration,
name: [IdentifierObject],
typeParameters: undefined,
heritageClauses: undefined,
members: [Array],
symbol: [SymbolObject]
},
kind: SyntaxKind.PropertyDeclaration,
decorators: undefined,
modifiers: [ [TokenObject], pos: 154, end: 162, transformFlags: 536870913 ],
name: IdentifierObject {},
questionToken: undefined,
type: TokenObject {
kind: SyntaxKind.NumberKeyword
},
initializer: [Circular *2],
symbol: SymbolObject {
escapedName: 'id',
declarations: [Array],
valueDeclaration: [Circular *1],
parent: [SymbolObject],
}
},
kind: SyntaxKind.NumericLiteral,
text: '1',
}
With text: '1'
being value of the property, kind: SyntaxKind.NumericLiteral
being the type of the text
and parent.type.kind: SyntaxKind.NumberKeyword
being the actual defined type of the property.
P.S.: I know this aint much, but i hope you find it somewhat helpful
EDIT: Adding how I get the types using the transformer:
export function transformer(program: ts.Program, opts?: TransformerOptions) {
function visitor(ctx: ts.TransformationContext, sourcefile: ts.SourceFile, result: { seen: boolean }) {
const typeChecker = program.getTypeChecker();
const _visitor: ts.Visitor = (node: ts.Node) => {
if (ts.isCallExpression(node) && node.typeArguments && node.expression.getText(sourcefile) == 'generateRtti') {
const [type] = node.typeArguments;
const [argument] = node.arguments;
const fn = ts.createIdentifier(nameOfGenerateFunction);
const typeName = type.getText();
const typeSource = getDescriptor(type, typeChecker);
result.seen = true;
return ts.createCall(fn, undefined, [argument || ts.createStringLiteral(typeName), typeSource]);
}
return ts.visitEachChild(node, _visitor, ctx);
};
return _visitor;
}
return (ctx: ts.TransformationContext) => {
return (sourcefile: ts.SourceFile) => {
const result = { seen: false };
const newSourcefile = ts.visitNode(sourcefile, visitor(ctx, sourcefile, result));
if (result.seen) {
const generated_function = createGenerateFunction();
const statements: Array<ts.Statement> = [generated_function];
for (const statement of newSourcefile.statements) {
if (ts.isImportDeclaration(statement))
statements.splice(statements.length - 1, 0, statement);
else
statements.push(statement);
}
return ts.updateSourceFileNode(newSourcefile, statements);
}
return newSourcefile;
};
};
}