Repository: lukeautry/tsoa (ts)

Issue:
Generic type inside extends don't resolve
Following the addition of generic type support (awesome addition! 👍) I've made some tests with it with the hope of simplifying my project, and one of my scenario is failing when I use extends over a generic class where the type used in the controller, is not generic itself.

## Sorting

* **I'm submitting a ...**
  - [X] bug report
  - [X] feature request
  - [ ] support request

* I confirm that I
  - [X] used the [search](https://github.com/lukeautry/tsoa/search?type=Issues) to make sure that a similar issue hasn't already been submit

## Expected Behavior
Theses lines should works.

```ts
export interface ThingContainerWithTitle extends GenericContainer<TheThing> {
    title: string;
}

export interface GenericContainer<T> {
    id: string;
    list: T[];
}
```

While using `ThingContainerWithTitle` as a property/return type of a controller.

## Current Behavior
But I got

> There was a problem resolving type of 'T'.
> There was a problem resolving type of 'GenContainer'.
> There was a problem resolving type of 'ThingContainerWithTitle'.

## Investigation
It work if I create a whole chain of generic types:

```ts
export interface ThingContainerWithTitle<T> extends GenericContainer<TheThing> {
    title: string;
}

export interface GenericContainer<T> {
    id: string;
    list: T[];
}
```

Then using `ThingContainerWithTitle<TheThing>`. But it's just wrong.

## Context (Environment)
Version of the library: 2.5.1
Version of NodeJS: 10.14.1
* Confirm you were using yarn not npm: [X]

## Breaking change?
No breaking change: it would add a new case where the generic can be used, but should not affect existing ones.

Pull request description:
Resolve Generic type inside heritage clauses
### All Submissions:

* [x] Have you followed the guidelines in our [Contributing](https://github.com/lukeautry/tsoa/tree/master/docs/CONTRIBUTING.md) document?
* [x] Have you checked to ensure there aren't other open [Pull Requests](https://github.com/lukeautry/tsoa/pulls) for the same update/change?
* [x] Have you written unit tests?
* [x] Have you written unit tests that cover the negative cases (i.e.: if bad data is submitted, does the library respond properly)?
* [x] This PR is associated with an existing issue?

**Closing issues**

Closes #467

### If this is a new feature submission:

* [x] Has the issue had a maintainer respond to the issue and clarify that the feature is something that aligns with the [goals](https://github.com/lukeautry/tsoa#goal) and [philosophy](https://github.com/lukeautry/tsoa#philosophy) of the project?

Patch:
diff --git a/README.MD b/README.MD
index f1a20c6b..c8a46d9b 100644
--- a/README.MD
+++ b/README.MD
@@ -34,7 +34,6 @@ Pronounced so·uh
     - [Security](#security)
     - [Tags](#tags)
     - [OperationId](#operationid)
-    - [Deprecated](#deprecated)
   * [Command Line Interface](#command-line-interface)
     - [Swagger spec generation](#swagger-spec-generation)
     - [Route generation](#route-generation)
@@ -777,19 +776,6 @@ public async find(): Promise<any> {
 }
 ```
 
-### Deprecated
-
-Declares this endpoint to be deprecated. Useful for when you are migrating endpoints and wants to keep a outdated
-version live until all consumers migrate to use the new endpoint version. 
-
-```ts
-@Get()
-@Deprecated()
-public async find(): Promise<any> {
-
-}
-```
-
 ## Command Line Interface
 
 For information on the configuration object (tsoa.json), check out the following:
diff --git a/package.json b/package.json
index 699ccdfa..959cf173 100644
--- a/package.json
+++ b/package.json
@@ -1,7 +1,7 @@
 {
   "name": "tsoa",
   "description": "Build swagger-compliant REST APIs using TypeScript and Node",
-  "version": "2.5.2",
+  "version": "2.4.11",
   "main": "./dist/index.js",
   "typings": "./dist/index.d.ts",
   "keywords": [
diff --git a/src/decorators/deprecated.ts b/src/decorators/deprecated.ts
deleted file mode 100644
index fba9ecc8..00000000
--- a/src/decorators/deprecated.ts
+++ /dev/null
@@ -1,8 +0,0 @@
-/**
- * used to show a method as deprecated on swagger documentation
- */
-export function Deprecated(): Function {
-  return () => {
-    return;
-  };
-}
diff --git a/src/metadataGeneration/methodGenerator.ts b/src/metadataGeneration/methodGenerator.ts
index ca79450b..30efc29c 100644
--- a/src/metadataGeneration/methodGenerator.ts
+++ b/src/metadataGeneration/methodGenerator.ts
@@ -1,6 +1,6 @@
 import * as ts from 'typescript';
 import { getDecorators } from './../utils/decoratorUtils';
-import {getJSDocComment, getJSDocDescription, isExistJSDocTag} from './../utils/jsDocUtils';
+import { getJSDocComment, getJSDocDescription, isExistJSDocTag } from './../utils/jsDocUtils';
 import { GenerateMetadataError } from './exceptions';
 import { getInitializerValue } from './initializer-value';
 import { MetadataGenerator } from './metadataGenerator';
@@ -38,7 +38,7 @@ export class MethodGenerator {
     responses.push(this.getMethodSuccessResponse(type));
 
     return {
-      deprecated: this.getIsDeprecated(),
+      deprecated: isExistJSDocTag(this.node, tag => tag.tagName.text === 'deprecated'),
       description: getJSDocDescription(this.node),
       isHidden: this.getIsHidden(),
       method: this.method,
@@ -200,21 +200,6 @@ export class MethodGenerator {
     return example;
   }
 
-  private getIsDeprecated() {
-    if (isExistJSDocTag(this.node, tag => tag.tagName.text === 'deprecated')) {
-      return true;
-    }
-    const depDecorators = this.getDecoratorsByIdentifier(this.node, 'Deprecated');
-    if (!depDecorators || !depDecorators.length) {
-      return false;
-    }
-    if (depDecorators.length > 1) {
-      throw new GenerateMetadataError(`Only one Deprecated decorator allowed in '${this.getCurrentLocation}' method.`);
-    }
-
-    return true;
-  }
-
   private getOperationId() {
     const opDecorators = this.getDecoratorsByIdentifier(this.node, 'OperationId');
     if (!opDecorators || !opDecorators.length) {
diff --git a/src/metadataGeneration/typeResolver.ts b/src/metadataGeneration/typeResolver.ts
index 6a4a87b4..be815da5 100644
--- a/src/metadataGeneration/typeResolver.ts
+++ b/src/metadataGeneration/typeResolver.ts
@@ -16,6 +16,9 @@ const localReferenceTypeCache: { [typeName: string]: Tsoa.ReferenceType } = {};
 const inProgressTypes: { [typeName: string]: boolean } = {};
 
 type UsableDeclaration = ts.InterfaceDeclaration | ts.ClassDeclaration | ts.TypeAliasDeclaration | ts.PropertySignature;
+interface Context {
+  [name: string]: ts.TypeReferenceNode | ts.TypeNode;
+}
 
 export class TypeResolver {
   constructor(
@@ -23,7 +26,7 @@ export class TypeResolver {
     private readonly current: MetadataGenerator,
     private readonly parentNode?: ts.Node,
     private readonly extractEnum = true,
-    private context: { [name: string]: ts.TypeReferenceNode | ts.TypeNode } = {},
+    private context: Context = {},
   ) {}
 
   public static clearCache() {
@@ -184,15 +187,7 @@ export class TypeResolver {
 
     let referenceType: Tsoa.ReferenceType;
     if (typeReference.typeArguments && typeReference.typeArguments.length > 0) {
-      const typeParameters = this.getModelTypeDeclaration(typeReference.typeName).typeParameters;
-      if (typeParameters) {
-        for (let index = 0; index < typeParameters.length; index++) {
-          const typeParameter = typeParameters[index];
-          this.context = { [typeParameter.name.text]: typeReference.typeArguments[index], ...this.context };
-        }
-      }
-    } else {
-      this.context = {};
+      this.typeArgumentsToContext(typeReference, typeReference.typeName, this.context);
     }
 
     referenceType = this.getReferenceType(typeReference.typeName as ts.EntityName, this.extractEnum, typeReference.typeArguments);
@@ -718,6 +713,35 @@ export class TypeResolver {
     return undefined;
   }
 
+  private typeArgumentsToContext(type: ts.TypeReferenceNode | ts.ExpressionWithTypeArguments, targetEntitiy: ts.EntityName, context: Context): Context {
+    this.context = {};
+
+    if (type.typeArguments && type.typeArguments.length > 0) {
+      const typeParameters = this.getModelTypeDeclaration(targetEntitiy).typeParameters;
+
+      if (typeParameters) {
+        for (let index = 0; index < typeParameters.length; index++) {
+          const typeParameter = typeParameters[index];
+          const typeArg = type.typeArguments[index];
+          let resolvedType: ts.TypeNode;
+
+          // Argument may be a forward reference from context
+          if (ts.isTypeReferenceNode(typeArg) && ts.isIdentifier(typeArg.typeName) && context[typeArg.typeName.text]) {
+            resolvedType = context[typeParameter.name.text];
+          } else {
+            resolvedType = type.typeArguments[index];
+          }
+
+          this.context = {
+            ...this.context,
+            [typeParameter.name.text]: resolvedType,
+          };
+        }
+      }
+    }
+    return context;
+  }
+
   private getModelInheritedProperties(modelTypeDeclaration: Exclude<UsableDeclaration, ts.PropertySignature>): Tsoa.Property[] {
     const properties = [] as Tsoa.Property[];
     if (modelTypeDeclaration.kind === ts.SyntaxKind.TypeAliasDeclaration) {
@@ -735,10 +759,20 @@ export class TypeResolver {
 
       clause.types.forEach(t => {
         const baseEntityName = t.expression as ts.EntityName;
+
+        // create subContext
+        let resetCtx = this.context;
+        if (t.typeArguments && t.typeArguments.length > 0) {
+          resetCtx = this.typeArgumentsToContext(t, baseEntityName, this.context);
+        }
+
         const referenceType = this.getReferenceType(baseEntityName);
         if (referenceType.properties) {
           referenceType.properties.forEach(property => properties.push(property));
         }
+
+        // reset subContext
+        this.context = resetCtx;
       });
     });
 
diff --git a/src/swagger/specGenerator.ts b/src/swagger/specGenerator.ts
index 0a95e263..65175d00 100644
--- a/src/swagger/specGenerator.ts
+++ b/src/swagger/specGenerator.ts
@@ -89,7 +89,6 @@ export abstract class SpecGenerator {
   protected abstract getSwaggerTypeForUnionType(type: Tsoa.UnionType);
 
   protected abstract getSwaggerTypeForIntersectionType(type: Tsoa.IntersectionType);
-
   public getSwaggerTypeForObjectLiteral(objectLiteral: Tsoa.ObjectLiteralType): Swagger.Schema {
     const properties = objectLiteral.properties.reduce((acc, property: Tsoa.Property) => {
       return {
@@ -98,15 +97,12 @@ export abstract class SpecGenerator {
       };
     }, {});
 
-    const additionalProperties = objectLiteral.additionalProperties && this.getSwaggerType(objectLiteral.additionalProperties);
-
     const required = objectLiteral.properties.filter(prop => prop.required).map(prop => prop.name);
 
     // An empty list required: [] is not valid.
     // If all properties are optional, do not specify the required keyword.
     return {
       properties,
-      ...(additionalProperties && { additionalProperties }),
       ...(required && required.length && { required }),
       type: 'object',
     };
diff --git a/tslint.json b/tslint.json
index 77f2536e..cd0264bc 100644
--- a/tslint.json
+++ b/tslint.json
@@ -24,7 +24,6 @@
     "no-namespace": [
       false
     ],
-    "object-literal-key-quotes": false,
     "ban-types": false
   },
   "linterOptions": {


Test patch:
diff --git a/.github/workflows/runTestsOnPush.yml b/.github/workflows/runTestsOnPush.yml
index 9886a97a..6bcc7da1 100644
--- a/.github/workflows/runTestsOnPush.yml
+++ b/.github/workflows/runTestsOnPush.yml
@@ -10,10 +10,6 @@ jobs:
       uses: Borales/actions-yarn@master
       with:
         args: install
-    - name: Lint
-      uses: Borales/actions-yarn@master
-      with:
-        args: lint
     - name: Test
       uses: Borales/actions-yarn@master
       with:
diff --git a/tests/fixtures/controllers/deprecatedController.ts b/tests/fixtures/controllers/deprecatedController.ts
deleted file mode 100644
index d8745267..00000000
--- a/tests/fixtures/controllers/deprecatedController.ts
+++ /dev/null
@@ -1,18 +0,0 @@
-import { Controller, Get, Route } from '../../../src';
-import { Deprecated } from '../../../src/decorators/deprecated';
-import { TestModel } from '../../fixtures/testModel';
-import { ModelService } from '../services/modelService';
-
-@Route('Controller')
-export class DeprecatedController extends Controller {
-  @Get('normalGetMethod')
-  public async normalGetMethod(): Promise<TestModel> {
-    return Promise.resolve(new ModelService().getModel());
-  }
-
-  @Get('deprecatedGetMethod')
-  @Deprecated()
-  public async deprecatedGetMethod(): Promise<TestModel> {
-    return Promise.resolve(new ModelService().getModel());
-  }
-}
diff --git a/tests/fixtures/testModel.ts b/tests/fixtures/testModel.ts
index a1f79f43..fe51788c 100644
--- a/tests/fixtures/testModel.ts
+++ b/tests/fixtures/testModel.ts
@@ -440,9 +440,37 @@ export interface GenericModel<T> {
   result: T;
   union?: T | string;
   nested?: GenericRequest<T>;
+  heritageCheck?: ThingContainerWithTitle<T>;
 }
 
 export interface GenericRequest<T> {
   name: string;
   value: T;
 }
+
+interface ThingContainerWithTitle<T> extends GenericContainer<number, number> {
+  // T is TestModel[] here
+  t: T;
+  title: string;
+}
+
+interface GenericContainer<T, TSameNameDifferentValue> {
+  id: string;
+  // T is number here
+  list: T[];
+  dangling: DanglingContext<T>;
+}
+
+/**
+ * This should only be used inside GenericContainer to check it's
+ * type argument T gets propagated while TSameNameDifferentValue does not
+ * and instead, the interface {@link TSameNameDifferentValue} is used.
+ */
+interface DanglingContext<T> {
+  number: T;
+  shouldBeString: TSameNameDifferentValue;
+}
+
+interface TSameNameDifferentValue {
+  str: string;
+}
diff --git a/tests/unit/swagger/definitionsGeneration/definitions.spec.ts b/tests/unit/swagger/definitionsGeneration/definitions.spec.ts
index 527bbb42..0888a39e 100644
--- a/tests/unit/swagger/definitionsGeneration/definitions.spec.ts
+++ b/tests/unit/swagger/definitionsGeneration/definitions.spec.ts
@@ -430,13 +430,7 @@ describe('Definition generation', () => {
                 name: { type: 'string' },
                 nested: {
                   properties: {
-                    additionals: {
-                      properties: {},
-                      type: 'object',
-                      additionalProperties: {
-                        $ref: '#/definitions/TypeAliasModel1',
-                      },
-                    },
+                    additionals: { properties: {}, type: 'object' },
                     allNestedOptional: {
                       properties: { one: { type: 'string' }, two: { type: 'string' } },
                       type: 'object',
@@ -468,6 +462,11 @@ describe('Definition generation', () => {
           Object.keys(definition.properties!).length,
           `because the swagger spec (${currentSpec.specName}) should only produce property schemas for properties that live on the TypeScript interface.`,
         );
+
+        expect(Object.keys(assertionsPerProperty)).to.length(
+          Object.keys(definition.properties!).length,
+          `because the swagger spec (${currentSpec.specName}) should only produce property schemas for properties that live on the TypeScript interface.`,
+        );
       });
     });
 
@@ -762,6 +761,43 @@ describe('Definition generation', () => {
           expect(ref!.name).to.deep.equal({ type: 'string', description: undefined, format: undefined, default: undefined });
           expect(ref!.value).to.deep.equal({ items: { $ref: '#/definitions/TestModel' }, type: 'array', description: undefined, format: undefined, default: undefined });
         });
+        it('should not propagate dangling context', () => {
+          const definition = getValidatedDefinition('DanglingContextnumber', currentSpec).properties;
+
+          expect(definition!.number).to.deep.equal({ type: 'number', format: 'double', description: undefined, default: undefined });
+          expect(definition!.shouldBeString!.$ref).to.deep.equal('#/definitions/TSameNameDifferentValue');
+        });
+        it('should check heritage clauses for type args', () => {
+          const definition = getValidatedDefinition('GenericModelTestModelArray', currentSpec).properties;
+
+          expect(definition!.heritageCheck).to.deep.equal({
+            $ref: '#/definitions/ThingContainerWithTitleTestModelArray',
+            description: undefined,
+            format: undefined,
+            'x-nullable': true,
+          });
+
+          const ref = getValidatedDefinition('ThingContainerWithTitleTestModelArray', currentSpec).properties;
+          expect(ref!.title).to.deep.equal({ type: 'string', description: undefined, format: undefined, default: undefined });
+          expect(ref!.t).to.deep.equal({
+            default: undefined,
+            description: undefined,
+            format: undefined,
+            items: {
+              $ref: '#/definitions/TestModel',
+            },
+            type: 'array',
+          });
+
+          expect(ref!.id).to.deep.equal({ type: 'string', description: undefined, format: undefined, default: undefined });
+          expect(ref!.list).to.deep.equal({
+            items: { type: 'number', format: 'double' },
+            type: 'array',
+            description: undefined,
+            format: undefined,
+            default: undefined,
+          });
+        });
       });
     });
   });
diff --git a/tests/unit/swagger/definitionsGeneration/metadata.spec.ts b/tests/unit/swagger/definitionsGeneration/metadata.spec.ts
index dd9d083c..cbdc87f6 100644
--- a/tests/unit/swagger/definitionsGeneration/metadata.spec.ts
+++ b/tests/unit/swagger/definitionsGeneration/metadata.spec.ts
@@ -502,31 +502,4 @@ describe('Metadata generation', () => {
       expect(method.isHidden).to.equal(true);
     });
   });
-
-  describe('DeprecatedMethodGenerator', () => {
-    const parameterMetadata = new MetadataGenerator('./tests/fixtures/controllers/deprecatedController.ts').Generate();
-    const controller = parameterMetadata.controllers[0];
-
-    it('should generate normal method', () => {
-      const method = controller.methods.find(m => m.name === 'normalGetMethod');
-      if (!method) {
-        throw new Error('Method normalGetMethod not defined!');
-      }
-
-      expect(method.method).to.equal('get');
-      expect(method.path).to.equal('normalGetMethod');
-      expect(method.deprecated).to.equal(false);
-    });
-
-    it('should generate deprecated method', () => {
-      const method = controller.methods.find(m => m.name === 'deprecatedGetMethod');
-      if (!method) {
-        throw new Error('Method deprecatedGetMethod not defined!');
-      }
-
-      expect(method.method).to.equal('get');
-      expect(method.path).to.equal('deprecatedGetMethod');
-      expect(method.deprecated).to.equal(true);
-    });
-  });
 });
diff --git a/tests/unit/swagger/schemaDetails3.spec.ts b/tests/unit/swagger/schemaDetails3.spec.ts
index b0e110d9..6a079e60 100644
--- a/tests/unit/swagger/schemaDetails3.spec.ts
+++ b/tests/unit/swagger/schemaDetails3.spec.ts
@@ -225,9 +225,6 @@ describe('Definition generation for OpenAPI 3.0.0', () => {
                     additionals: {
                       properties: {},
                       type: 'object',
-                      additionalProperties: {
-                        $ref: '#/components/schemas/TypeAliasModel1',
-                      },
                     },
                     allNestedOptional: {
                       properties: { one: { type: 'string' }, two: { type: 'string' } },