Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions .vscode/extensions.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
{
"recommendations": [
"bierner.comment-tagged-templates",
"dbaeumer.vscode-eslint",
"esbenp.prettier-vscode"
]
}
83 changes: 82 additions & 1 deletion packages/cli/src/metadataGeneration/typeResolver.ts
Original file line number Diff line number Diff line change
Expand Up @@ -303,6 +303,87 @@ export class TypeResolver {
return new TypeResolver(this.typeNode.type, this.current, this.typeNode, this.context, this.referencer).resolve();
}

if (ts.isTypeQueryNode(this.typeNode)) {
const isIntrinsicType = (type: ts.Type): boolean => {
const flags = ts.TypeFlags;
return (
this.hasFlag(type, flags.Number) ||
this.hasFlag(type, flags.String) ||
this.hasFlag(type, flags.Boolean) ||
this.hasFlag(type, flags.BigInt) ||
this.hasFlag(type, flags.ESSymbol) ||
this.hasFlag(type, flags.Undefined) ||
this.hasFlag(type, flags.Null)
);
};
const symbol = this.current.typeChecker.getSymbolAtLocation(this.typeNode.exprName);
if (symbol) {
// Access the first declaration of the symbol
const declaration = symbol.declarations?.[0];
throwUnless(declaration, new GenerateMetadataError(`Could not find declaration for symbol: ${symbol.name}`, this.typeNode));

if (ts.isVariableDeclaration(declaration)) {
const initializer = declaration.initializer;
const declarationType = this.current.typeChecker.getTypeAtLocation(declaration);
if (isIntrinsicType(declarationType)) {
return { dataType: this.current.typeChecker.typeToString(declarationType) as Tsoa.PrimitiveType['dataType'] };
}
if (initializer && (ts.isStringLiteral(initializer) || ts.isNumericLiteral(initializer) || ts.isBigIntLiteral(initializer))) {
return { dataType: 'enum', enums: [initializer.text] };
} else if (initializer && ts.isObjectLiteralExpression(initializer)) {
const getOneOrigDeclaration = (prop: ts.Symbol): ts.Declaration | undefined => {
if (prop.declarations) {
return prop.declarations[0];
}
const syntheticOrigin: ts.Symbol = (prop as any).links?.syntheticOrigin;
if (syntheticOrigin && syntheticOrigin.name === prop.name) {
return syntheticOrigin.declarations?.[0];
}
return undefined;
};

const isIgnored = (prop: ts.Symbol) => {
const declaration = getOneOrigDeclaration(prop);
return declaration !== undefined && getJSDocTagNames(declaration).some(tag => tag === 'ignore') && !ts.isPropertyAssignment(declaration);
};

const typeProperties: ts.Symbol[] = this.current.typeChecker.getPropertiesOfType(this.current.typeChecker.getTypeAtLocation(initializer));
const properties: Tsoa.Property[] = typeProperties
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

A lot of the property management should already exist in checker based resolutions.

Ideally we can merge them together.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can you elaborate on what you mean? Do you mean putting this code in a location that can be shared across other places where we do property management in this file?

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is taken from lines 194ff, right?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, parts of it are taken from another section. Do you want me to extract that to a shared method is my question

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Tes, that extraction was what I was hinting at.

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@pclements12 Not trying to rush you, but are you still working on this?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I haven't had time to spend on it lately. Happy for anyone else to contribute to clean up on this pr to get it across the line. It's functionally complete, but needs a little organizing

.filter(property => isIgnored(property) === false)
.map(property => {
const propertyType = this.current.typeChecker.getTypeOfSymbolAtLocation(property, this.typeNode);
const typeNode = this.current.typeChecker.typeToTypeNode(propertyType, undefined, ts.NodeBuilderFlags.NoTruncation)!;
const parent = getOneOrigDeclaration(property);
const type = new TypeResolver(typeNode, this.current, parent, this.context, propertyType).resolve();

const required = !this.hasFlag(property, ts.SymbolFlags.Optional);
const comments = property.getDocumentationComment(this.current.typeChecker);
const description = comments.length ? ts.displayPartsToString(comments) : undefined;

return {
name: property.getName(),
required,
deprecated: parent ? isExistJSDocTag(parent, tag => tag.tagName.text === 'deprecated') || isDecorator(parent, identifier => identifier.text === 'Deprecated') : false,
type,
default: undefined,
validators: (parent ? getPropertyValidators(parent) : {}) || {},
description,
format: parent ? this.getNodeFormat(parent) : undefined,
example: parent ? this.getNodeExample(parent) : undefined,
extensions: parent ? this.getNodeExtension(parent) : undefined,
};
});

const objectLiteral: Tsoa.NestedObjectLiteralType = {
dataType: 'nestedObjectLiteral',
properties,
};
return objectLiteral;
}
}
}
}

throwUnless(this.typeNode.kind === ts.SyntaxKind.TypeReference, new GenerateMetadataError(`Unknown type: ${ts.SyntaxKind[this.typeNode.kind]}`, this.typeNode));

return this.resolveTypeReferenceNode(this.typeNode as ts.TypeReferenceNode, this.current, this.context, this.parentNode);
Expand Down Expand Up @@ -573,7 +654,7 @@ export class TypeResolver {
if (this.context[name]) {
//resolve name only interesting if entity is not qualifiedName
name = this.context[name].name; //Not needed to check unicity, because generic parameters are checked previously
} else {
} else if (type.parent && !ts.isTypeQueryNode(type.parent)) {
const declarations = this.getModelTypeDeclarations(type);

//Two possible solutions for recognizing different types:
Expand Down
7 changes: 6 additions & 1 deletion tests/fixtures/controllers/parameterController.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { Body, BodyProp, Get, Header, Path, Post, Query, Request, Route, Res, TsoaResponse, Deprecated, Queries, RequestProp, FormField } from '@tsoa/runtime';
import { Gender, ParameterTestModel } from '../testModel';
import { Gender, ParameterTestModel, ValueType } from '../testModel';

@Route('ParameterTest')
export class ParameterController {
Expand Down Expand Up @@ -402,4 +402,9 @@ export class ParameterController {
public async inline1(@Body() body: { requestString: string; requestNumber: number }): Promise<{ resultString: string; responseNumber: number }> {
return { resultString: 'a', responseNumber: 1 };
}

@Post('TypeInference')
public async typeInference(@Body() body: ValueType): Promise<ValueType> {
return { a: 'a', b: 1 };
}
}
4 changes: 4 additions & 0 deletions tests/fixtures/testModel.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1289,3 +1289,7 @@ type OrderDirection = 'asc' | 'desc';
type OrderOptions<E> = `${keyof E & string}:${OrderDirection}`;

type TemplateLiteralString = OrderOptions<ParameterTestModel>;

const value = { a: 'a', b: 1 };
type Infer<T> = T;
export type ValueType = Infer<typeof value>;
17 changes: 17 additions & 0 deletions tests/unit/swagger/definitionsGeneration/metadata.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -825,6 +825,23 @@ describe('Metadata generation', () => {
const deprecatedParam2 = method.parameters[2];
expect(deprecatedParam2.deprecated).to.be.true;
});

it('should handle type inference params', () => {
const method = controller.methods.find(m => m.name === 'typeInference');
if (!method) {
throw new Error('Method typeInference not defined!');
}
const parameter = method.parameters.find(param => param.parameterName === 'body');
if (!parameter) {
throw new Error('Parameter firstname not defined!');
}

expect(method.parameters.length).to.equal(1);
expect(parameter.in).to.equal('body');
expect(parameter.name).to.equal('body');
expect(parameter.parameterName).to.equal('body');
expect(parameter.required).to.be.true;
});
});

describe('HiddenMethodGenerator', () => {
Expand Down
Loading
Loading