Repository: lukeautry/tsoa (ts)

Issue:
Method return type problem when dealing with arrays
If you define a request handler and want to return an array of some type you will face a type naming issue in the OpenAPI specification when writing
```
public testGet(): T[] {}
```
instead of
```
public testGet(): Array<T> {}
```


If you then try to generate an API client using the Swagger codegen executable you will face the issue of return types like this (Dart):
```
static List<T[]> listFromJson(List<dynamic> json) {}
```
Expected behavior:
```
static List<T> listFromJson(List<dynamic> json) {}
```

Pull request description:
fixes #398 and adds more tests to prove #399
fixes #398 and adds more tests to prove #399

Patch:
diff --git a/src/metadataGeneration/typeResolver.ts b/src/metadataGeneration/typeResolver.ts
index 7af45cf9..ba419ab2 100644
--- a/src/metadataGeneration/typeResolver.ts
+++ b/src/metadataGeneration/typeResolver.ts
@@ -121,6 +121,20 @@ export class TypeResolver {
     }
 
     this.current.AddReferenceType(referenceType);
+
+    // We do a hard assert in the test mode so we can catch bad ref names (https://github.com/lukeautry/tsoa/issues/398).
+    //   The goal is to avoid producing these names before the code is ever merged to master (via extensive test coverage)
+    //   and therefore this validation does not have to run for the users
+    if (process.env.NODE_ENV === 'tsoa_test') {
+      // This regex allows underscore, hyphen, and period since those are valid in SwaggerEditor
+      const symbolsRegex = /[!$%^&*()+|~=`{}\[\]:";'<>?,\/]/;
+      if (symbolsRegex.test(referenceType.refName)) {
+        throw new Error(`Problem with creating refName ${referenceType.refName} since we should not allow symbols in ref names ` +
+          `because it would cause invalid swagger.yaml to be created. This is due to the swagger rule ` +
+          `"ref values must be RFC3986-compliant percent-encoded URIs."`);
+      }
+    }
+
     return referenceType;
   }
 
@@ -312,13 +326,19 @@ export class TypeResolver {
 
     const resolvedName = genericTypes.reduce((acc, generic) => {
       if (ts.isTypeReferenceNode(generic) && generic.typeArguments && generic.typeArguments.length > 0) {
-        return [...acc, this.getTypeName(generic.typeName.getText(), generic.typeArguments)];
+        const typeNameSection = this.getTypeName(generic.typeName.getText(), generic.typeArguments);
+        acc.push(typeNameSection);
+        return acc;
       } else {
-        return [...acc, this.getAnyTypeName(generic)];
+        const typeNameSection = this.getAnyTypeName(generic);
+        acc.push(typeNameSection);
+        return acc;
       }
     }, [] as string[]);
 
-    return typeName + resolvedName.join('');
+    const finalName = typeName + resolvedName.join('');
+
+    return finalName;
   }
 
   private getAnyTypeName(typeNode: ts.TypeNode): string {
@@ -329,7 +349,7 @@ export class TypeResolver {
 
     if (typeNode.kind === ts.SyntaxKind.ArrayType) {
       const arrayType = typeNode as ts.ArrayTypeNode;
-      return this.getAnyTypeName(arrayType.elementType) + '[]';
+      return this.getAnyTypeName(arrayType.elementType) + 'Array';
     }
 
     if (typeNode.kind === ts.SyntaxKind.UnionType) {


Test patch:
diff --git a/tests/fixtures/testModel.ts b/tests/fixtures/testModel.ts
index 889ba397..a81c39ac 100644
--- a/tests/fixtures/testModel.ts
+++ b/tests/fixtures/testModel.ts
@@ -64,10 +64,13 @@ export interface TestModel extends Model {
   modelsEnumIndirect?: TestSubEnumModelContainer;
   typeAliasCase1?: TypeAliasModelCase1;
   TypeAliasCase2?: TypeAliasModelCase2;
-  // tslint:disable-next-line
-  genericNested?: GenericRequest<Array<TypeAliasModel1>>;
-  // tslint:disable-next-line
-  genericNested2?: GenericRequest<Array<TypeAliasModel2>>;
+  genericMultiNested?: GenericRequest<GenericRequest<TypeAliasModel1>>;
+  // tslint:disable-next-line: array-type
+  genericNestedArrayKeyword1?: GenericRequest<Array<TypeAliasModel1>>;
+  genericNestedArrayCharacter1?: GenericRequest<TypeAliasModel1[]>;
+  // tslint:disable-next-line: array-type
+  genericNestedArrayKeyword2?: GenericRequest<Array<TypeAliasModel2>>;
+  genericNestedArrayCharacter2?: GenericRequest<TypeAliasModel2[]>;
 }
 
 export interface TypeAliasModel1 {
diff --git a/tests/unit/swagger/definitionsGeneration/definitions.spec.ts b/tests/unit/swagger/definitionsGeneration/definitions.spec.ts
index 4bdc38b9..5f723161 100644
--- a/tests/unit/swagger/definitionsGeneration/definitions.spec.ts
+++ b/tests/unit/swagger/definitionsGeneration/definitions.spec.ts
@@ -318,11 +318,20 @@ describe('Definition generation', () => {
                     expect(propertySchema).to.not.haveOwnProperty('additionalProperties', `for property ${propertyName}`);
                     expect(propertySchema['x-nullable']).to.eq(true, `for property ${propertyName}[x-nullable]`);
                 },
-                genericNested: (propertyName, propertySchema) => {
-                  expect(propertySchema.$ref).to.eq('#/definitions/GenericRequestArrayTypeAliasModel1');
+                genericMultiNested: (propertyName, propertySchema) => {
+                    expect(propertySchema.$ref).to.eq('#/definitions/GenericRequestGenericRequestTypeAliasModel1', `for property ${propertyName}.$ref`);
                 },
-                genericNested2: (propertyName, propertySchema) => {
-                  expect(propertySchema.$ref).to.eq('#/definitions/GenericRequestArrayTypeAliasModel2');
+                genericNestedArrayKeyword1: (propertyName, propertySchema) => {
+                    expect(propertySchema.$ref).to.eq('#/definitions/GenericRequestArrayTypeAliasModel1', `for property ${propertyName}.$ref`);
+                },
+                genericNestedArrayCharacter1: (propertyName, propertySchema) => {
+                    expect(propertySchema.$ref).to.eq('#/definitions/GenericRequestTypeAliasModel1Array', `for property ${propertyName}.$ref`);
+                },
+                genericNestedArrayKeyword2: (propertyName, propertySchema) => {
+                    expect(propertySchema.$ref).to.eq('#/definitions/GenericRequestArrayTypeAliasModel2', `for property ${propertyName}.$ref`);
+                },
+                genericNestedArrayCharacter2: (propertyName, propertySchema) => {
+                    expect(propertySchema.$ref).to.eq('#/definitions/GenericRequestTypeAliasModel2Array', `for property ${propertyName}.$ref`);
                 },
             };
 
@@ -537,11 +546,11 @@ describe('Definition generation', () => {
         });
 
         it('should generate different definitions for a generic model', () => {
-          const definition = getValidatedDefinition('GenericModelTestModel', currentSpec).properties;
+          const genericDefinition = getValidatedDefinition('GenericModelTestModel', currentSpec).properties;
 
-          if (!definition) { throw new Error(`There were no properties on model.`); }
+          if (!genericDefinition) { throw new Error(`There were no properties on model.`); }
 
-          const property = definition.result;
+          const property = genericDefinition.result;
 
           expect(property).not.to.haveOwnProperty('additionalProperties', 'since swagger does not support setting that property on $refs');
 
@@ -549,7 +558,7 @@ describe('Definition generation', () => {
           expect(property.$ref).to.equal('#/definitions/TestModel');
         });
         it('should generate different definitions for a generic model array', () => {
-          const definition = getValidatedDefinition('GenericModelTestModel[]', currentSpec).properties;
+          const definition = getValidatedDefinition('GenericModelTestModelArray', currentSpec).properties;
 
           if (!definition) { throw new Error(`There were no properties on model.`); }
 
@@ -576,7 +585,7 @@ describe('Definition generation', () => {
           expect(property.type).to.equal('string');
         });
         it('should generate different definitions for a generic primitive array', () => {
-          const definition = getValidatedDefinition('GenericModelstring[]', currentSpec).properties;
+          const definition = getValidatedDefinition('GenericModelstringArray', currentSpec).properties;
 
           if (!definition) { throw new Error(`There were no properties on model.`); }
 
diff --git a/tests/unit/swagger/schemaDetails3.spec.ts b/tests/unit/swagger/schemaDetails3.spec.ts
index f5495d97..3989037f 100644
--- a/tests/unit/swagger/schemaDetails3.spec.ts
+++ b/tests/unit/swagger/schemaDetails3.spec.ts
@@ -359,11 +359,20 @@ describe('Definition generation for OpenAPI 3.0.0', () => {
                     expect(propertySchema).to.not.haveOwnProperty('additionalProperties', `for property ${propertyName}`);
                     expect(propertySchema.nullable).to.eq(true, `for property ${propertyName}.nullable`);
                 },
-                genericNested: (propertyName, propertySchema) => {
-                    expect(propertySchema.$ref).to.eq('#/components/schemas/GenericRequestArrayTypeAliasModel1');
+                genericMultiNested: (propertyName, propertySchema) => {
+                    expect(propertySchema.$ref).to.eq('#/components/schemas/GenericRequestGenericRequestTypeAliasModel1', `for property ${propertyName}.$ref`);
                 },
-                genericNested2: (propertyName, propertySchema) => {
-                    expect(propertySchema.$ref).to.eq('#/components/schemas/GenericRequestArrayTypeAliasModel2');
+                genericNestedArrayKeyword1: (propertyName, propertySchema) => {
+                    expect(propertySchema.$ref).to.eq('#/components/schemas/GenericRequestArrayTypeAliasModel1', `for property ${propertyName}.$ref`);
+                },
+                genericNestedArrayCharacter1: (propertyName, propertySchema) => {
+                    expect(propertySchema.$ref).to.eq('#/components/schemas/GenericRequestTypeAliasModel1Array', `for property ${propertyName}.$ref`);
+                },
+                genericNestedArrayKeyword2: (propertyName, propertySchema) => {
+                    expect(propertySchema.$ref).to.eq('#/components/schemas/GenericRequestArrayTypeAliasModel2', `for property ${propertyName}.$ref`);
+                },
+                genericNestedArrayCharacter2: (propertyName, propertySchema) => {
+                    expect(propertySchema.$ref).to.eq('#/components/schemas/GenericRequestTypeAliasModel2Array', `for property ${propertyName}.$ref`);
                 },
             };