Repository: asyncapi/modelina (ts)

Issue:
language specific keywords are rendered as property names
#### Describe the bug
There are certain keywords that are reserved. This is different between languages, we need to make sure we dont render such properties as-is.

#### How to Reproduce
For example, in Java, you cannot have a property named `enum`. 

#### Expected behavior
I expected the library to handle any property names. 


Pull request:
fix: ensure reserved keywords can never be rendered as properties for Java class
**Description**
This PR ensures that reserved keywords can never be rendered for the Java class.

I actually struggled quite a bit with how this functionality should work in cohesion with the name convention option. I could not come up with any other way of solving this problem other than this way. In case you see another please let me know 🙏 

**Related issue(s)**
partly solves #176 

Patch:
diff --git a/docs/generators.md b/docs/generators.md
index c2946121..c48facb7 100644
--- a/docs/generators.md
+++ b/docs/generators.md
@@ -48,7 +48,7 @@ Below is a list of additional options available for a given generator.
 | `collectionType` | String | It indicates with which signature should be rendered the `array` type. Its value can be either `List` (`List<{type}>`) or `Array` (`{type}[]`). | `List` |
 | `namingConvention` | Object | Options for naming conventions. | - |
 | `namingConvention.type` | Function | A function that returns the format of the type. | _Returns pascal cased name_ |
-| `namingConvention.property` | Function | A function that returns the format of the property. | _Returns camel cased name_ |
+| `namingConvention.property` | Function | A function that returns the format of the property. | _Returns camel cased name, and ensures that names of properties does not clash against reserved keywords_ |
 
 ### [JavaScript](../src/generators/javascript/JavaScriptGenerator.ts)
 
diff --git a/src/generators/java/JavaGenerator.ts b/src/generators/java/JavaGenerator.ts
index 3e3b3984..42344320 100644
--- a/src/generators/java/JavaGenerator.ts
+++ b/src/generators/java/JavaGenerator.ts
@@ -12,6 +12,7 @@ export interface JavaOptions extends CommonGeneratorOptions<JavaPreset> {
   collectionType?: 'List' | 'Array';
   namingConvention?: CommonNamingConvention;
 }
+
 export class JavaGenerator extends AbstractGenerator<JavaOptions> {
   static defaultOptions: JavaOptions = {
     ...defaultGeneratorOptions,
diff --git a/src/generators/java/JavaRenderer.ts b/src/generators/java/JavaRenderer.ts
index 6ba48e9d..f8ddb110 100644
--- a/src/generators/java/JavaRenderer.ts
+++ b/src/generators/java/JavaRenderer.ts
@@ -3,6 +3,61 @@ import { JavaGenerator, JavaOptions } from './JavaGenerator';
 import { CommonModel, CommonInputModel, Preset } from '../../models';
 import { FormatHelpers, ModelKind, TypeHelpers } from '../../helpers';
 
+/**
+ * List of reserved java keywords that may not be rendered as is.
+ */
+export const ReservedJavaKeywordList = [
+  'abstract', 
+  'continue', 
+  'for', 
+  'new', 
+  'switch  assert', 
+  'default', 
+  'goto', 
+  'package', 
+  'synchronized', 
+  'boolean', 
+  'do', 
+  'if', 
+  'private', 
+  'this', 
+  'break', 
+  'double', 
+  'implements', 
+  'protected', 
+  'throw', 
+  'byte', 
+  'else', 
+  'import', 
+  'public', 
+  'throws', 
+  'case', 
+  'enum', 
+  'instanceof', 
+  'return', 
+  'transient', 
+  'catch', 
+  'extends', 
+  'int', 
+  'short', 
+  'try', 
+  'char', 
+  'final', 
+  'interface', 
+  'static', 
+  'void', 
+  'class', 
+  'finally', 
+  'long', 
+  'strictfp', 
+  'volatile', 
+  'const', 
+  'float', 
+  'native', 
+  'super', 
+  'while'
+];
+
 /**
  * Common renderer for Java types
  * 
@@ -18,6 +73,10 @@ export abstract class JavaRenderer extends AbstractRenderer<JavaOptions, JavaGen
   ) {
     super(options, generator, presets, model, inputModel);
   }
+  
+  static isReservedJavaKeyword(word: string): boolean {
+    return ReservedJavaKeywordList.includes(word);
+  }
 
   /**
    * Renders the name of a type based on provided generator option naming convention type function.
@@ -29,7 +88,7 @@ export abstract class JavaRenderer extends AbstractRenderer<JavaOptions, JavaGen
    */
   nameType(name: string | undefined, model?: CommonModel): string {
     return this.options?.namingConvention?.type 
-      ? this.options.namingConvention.type(name, { model: model || this.model, inputModel: this.inputModel })
+      ? this.options.namingConvention.type(name, { model: model || this.model, inputModel: this.inputModel, isReservedKeyword: JavaRenderer.isReservedJavaKeyword(`${name}`) })
       : name || '';
   }
 
@@ -41,7 +100,7 @@ export abstract class JavaRenderer extends AbstractRenderer<JavaOptions, JavaGen
    */
   nameProperty(propertyName: string | undefined, property?: CommonModel): string {
     return this.options?.namingConvention?.property 
-      ? this.options.namingConvention.property(propertyName, { model: this.model, inputModel: this.inputModel, property })
+      ? this.options.namingConvention.property(propertyName, { model: this.model, inputModel: this.inputModel, property, isReservedKeyword: JavaRenderer.isReservedJavaKeyword(`${propertyName}`) })
       : propertyName || '';
   }
   
diff --git a/src/generators/java/renderers/ClassRenderer.ts b/src/generators/java/renderers/ClassRenderer.ts
index 00404abd..11954ce8 100644
--- a/src/generators/java/renderers/ClassRenderer.ts
+++ b/src/generators/java/renderers/ClassRenderer.ts
@@ -24,7 +24,7 @@ export class ClassRenderer extends JavaRenderer {
       this.addDependency('import java.util.Map;');
     }
     
-    const formattedName = this.nameType(this.model.$id);
+    const formattedName = this.nameType(`${this.model.$id}`);
     return `public class ${formattedName} {
 ${this.indent(this.renderBlock(content, 2))}
 }`;
@@ -94,11 +94,11 @@ ${this.indent(this.renderBlock(content, 2))}
   }
 
   runGetterPreset(propertyName: string, property: CommonModel, type: PropertyType = PropertyType.property): Promise<string> {
-    return this.runPreset('getter', { propertyName, property, type });
+    return this.runPreset('getter', { propertyName, property, type});
   }
 
   runSetterPreset(propertyName: string, property: CommonModel, type: PropertyType = PropertyType.property): Promise<string> {
-    return this.runPreset('setter', { propertyName, property, type });
+    return this.runPreset('setter', { propertyName, property, type});
   }
 }
 
@@ -115,21 +115,21 @@ export const JAVA_DEFAULT_CLASS_PRESET: ClassPreset<ClassRenderer> = {
     return `private ${propertyType} ${propertyName};`;
   },
   getter({ renderer, propertyName, property, type }) {
-    propertyName = renderer.nameProperty(propertyName, property);
+    const formattedPropertyName = renderer.nameProperty(propertyName, property);
     const getterName = `get${FormatHelpers.toPascalCase(propertyName)}`;
     let getterType = renderer.renderType(property);
     if (type === PropertyType.additionalProperty || type === PropertyType.patternProperties) {
       getterType = `Map<String, ${getterType}>`;
     }
-    return `public ${getterType} ${getterName}() { return this.${propertyName}; }`;
+    return `public ${getterType} ${getterName}() { return this.${formattedPropertyName}; }`;
   },
   setter({ renderer, propertyName, property, type }) {
-    propertyName = renderer.nameProperty(propertyName, property);
+    const formattedPropertyName = renderer.nameProperty(propertyName, property);
     const setterName = FormatHelpers.toPascalCase(propertyName);
     let setterType = renderer.renderType(property);
     if (type === PropertyType.additionalProperty || type === PropertyType.patternProperties) {
       setterType = `Map<String, ${setterType}>`;
     }
-    return `public void set${setterName}(${setterType} ${propertyName}) { this.${propertyName} = ${propertyName}; }`;
+    return `public void set${setterName}(${setterType} ${formattedPropertyName}) { this.${formattedPropertyName} = ${formattedPropertyName}; }`;
   }
 };
diff --git a/src/generators/typescript/TypeScriptRenderer.ts b/src/generators/typescript/TypeScriptRenderer.ts
index 55f2dd00..468b3aea 100644
--- a/src/generators/typescript/TypeScriptRenderer.ts
+++ b/src/generators/typescript/TypeScriptRenderer.ts
@@ -5,6 +5,70 @@ import { FormatHelpers } from '../../helpers';
 import { CommonModel, CommonInputModel, Preset, PropertyType } from '../../models';
 import { DefaultPropertyNames, getUniquePropertyName } from '../../helpers/NameHelpers';
 
+export const ReservedTypeScriptKeywords = [
+  'break',
+  'case',
+  'catch',
+  'class',
+  'const',
+  'continue',
+  'debugger',
+  'default',
+  'delete',
+  'do',
+  'else',
+  'enum',
+  'export',
+  'extends',
+  'false',
+  'finally',
+  'for',
+  'function',
+  'if',
+  'import',
+  'in',
+  'instanceof',
+  'new',
+  'null',
+  'return',
+  'super',
+  'switch',
+  'this',
+  'throw',
+  'true',
+  'try',
+  'typeof',
+  'var',
+  'void',
+  'while',
+  'with',
+  'any',
+  'boolean',
+  'constructor',
+  'declare',
+  'get',
+  'module',
+  'require',
+  'number',
+  'set',
+  'string',
+  'symbol',
+  'type',
+  'from',
+  'of',
+  // Strict mode reserved words
+  'as',
+  'implements',
+  'interface',
+  'let',
+  'package',
+  'private',
+  'protected',
+  'public',
+  'static',
+  'yield'
+];
+
 /**
  * Common renderer for TypeScript types
  * 
diff --git a/src/helpers/NameHelpers.ts b/src/helpers/NameHelpers.ts
index 77e344d7..121db5eb 100644
--- a/src/helpers/NameHelpers.ts
+++ b/src/helpers/NameHelpers.ts
@@ -19,29 +19,47 @@ export enum DefaultPropertyNames {
  */
 export function getUniquePropertyName(rootModel: CommonModel, propertyName: string): string {
   if (Object.keys(rootModel.properties || {}).includes(propertyName)) {
-    return getUniquePropertyName(rootModel, `_${propertyName}`);
+    return getUniquePropertyName(rootModel, `reserved_${propertyName}`);
   }
   return propertyName;
 }
 
+/**
+ * The common naming convention context type.
+ */
+export type CommonTypeNamingConventionCtx = { model: CommonModel, inputModel: CommonInputModel, isReservedKeyword?: boolean};
+export type CommonPropertyNamingConventionCtx = { model: CommonModel, inputModel: CommonInputModel, property?: CommonModel, isReservedKeyword?: boolean};
+
 /**
  * The common naming convention type shared between generators for different languages.
  */
 export type CommonNamingConvention = {
-  type?: (name: string | undefined, ctx: { model: CommonModel, inputModel: CommonInputModel }) => string;
-  property?: (name: string | undefined, ctx: { model: CommonModel, inputModel: CommonInputModel, property?: CommonModel }) => string;
+  type?: (name: string | undefined, ctx: CommonTypeNamingConventionCtx) => string;
+  property?: (name: string | undefined, ctx: CommonPropertyNamingConventionCtx) => string;
 };
 
 /**
  * A CommonNamingConvention implementation shared between generators for different languages.
  */
 export const CommonNamingConventionImplementation: CommonNamingConvention = {
-  type: (name: string | undefined) => {
+  type: (name, ctx) => {
     if (!name) {return '';}
+    if (ctx.isReservedKeyword) { 
+      name = `reserved_${name}`;
+    }
     return FormatHelpers.toPascalCase(name);
   },
-  property: (name: string | undefined) => {
+  property: (name, ctx) => {
     if (!name) {return '';}
+    if (ctx.isReservedKeyword) { 
+      // If name is considered reserved, make sure we rename it appropriately
+      // and make sure no clashes occur.
+      name = FormatHelpers.toCamelCase(`reserved_${name}`);
+      if (Object.keys(ctx.model.properties || {}).includes(name)) {
+        // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
+        return CommonNamingConventionImplementation.property!(name, ctx);
+      }
+    }
     return FormatHelpers.toCamelCase(name);
   }
 };
diff --git a/src/models/Preset.ts b/src/models/Preset.ts
index de3257c1..f5096966 100644
--- a/src/models/Preset.ts
+++ b/src/models/Preset.ts
@@ -24,7 +24,7 @@ export enum PropertyType {
 export interface PropertyArgs {
   propertyName: string;
   property: CommonModel;
-  type: PropertyType
+  type: PropertyType;
 }
 
 export interface ClassPreset<R extends AbstractRenderer, O extends object = any> extends CommonPreset<R, O> {


Test patch:
diff --git a/test/generators/java/JavaGenerator.spec.ts b/test/generators/java/JavaGenerator.spec.ts
index 5539aea1..79b7e601 100644
--- a/test/generators/java/JavaGenerator.spec.ts
+++ b/test/generators/java/JavaGenerator.spec.ts
@@ -6,6 +6,36 @@ describe('JavaGenerator', () => {
     generator = new JavaGenerator();
   });
 
+  test('should not render reserved keyword', async () => {
+    const doc = {
+      $id: 'Address',
+      type: 'object',
+      properties: {
+        enum: { type: 'string' },
+        reservedEnum: { type: 'string' }
+      },
+      additionalProperties: false
+    };
+    const expected = `public class Address {
+  private String reservedReservedEnum;
+  private String reservedEnum;
+
+  public String getEnum() { return this.reservedReservedEnum; }
+  public void setEnum(String reservedReservedEnum) { this.reservedReservedEnum = reservedReservedEnum; }
+
+  public String getReservedEnum() { return this.reservedEnum; }
+  public void setReservedEnum(String reservedEnum) { this.reservedEnum = reservedEnum; }
+}`;
+
+    const inputModel = await generator.process(doc);
+    const model = inputModel.models['Address'];
+
+    let classModel = await generator.renderClass(model, inputModel);
+    expect(classModel.result).toEqual(expected);
+
+    classModel = await generator.render(model, inputModel);
+    expect(classModel.result).toEqual(expected);
+  });
   test('should render `class` type', async () => {
     const doc = {
       $id: 'Address',
diff --git a/test/helpers/NameHelpers.spec.ts b/test/helpers/NameHelpers.spec.ts
index 7bf3fc1b..faafbd97 100644
--- a/test/helpers/NameHelpers.spec.ts
+++ b/test/helpers/NameHelpers.spec.ts
@@ -15,7 +15,7 @@ describe('NameHelpers', () => {
 
       const additionalPropertiesName = getUniquePropertyName(model, DefaultPropertyNames.additionalProperties);
       
-      expect(additionalPropertiesName).toEqual('_additionalProperties');
+      expect(additionalPropertiesName).toEqual('reserved_additionalProperties');
     });
   });