From 13d71e9636120e64de539bf8a24321983a28f686 Mon Sep 17 00:00:00 2001 From: Jonas Lagoni Date: Tue, 27 Jul 2021 18:00:42 +0200 Subject: [PATCH] fix: ensure reserved keywords can never be rendered as properties for Java class (#269) --- docs/generators.md | 2 +- src/generators/java/JavaGenerator.ts | 1 + src/generators/java/JavaRenderer.ts | 63 +++++++++++++++++- .../java/renderers/ClassRenderer.ts | 14 ++-- .../typescript/TypeScriptRenderer.ts | 64 +++++++++++++++++++ src/helpers/NameHelpers.ts | 28 ++++++-- src/models/Preset.ts | 2 +- test/generators/java/JavaGenerator.spec.ts | 30 +++++++++ test/helpers/NameHelpers.spec.ts | 2 +- 9 files changed, 189 insertions(+), 17 deletions(-) diff --git a/docs/generators.md b/docs/generators.md index c294612183..c48facb71d 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 3e3b39840b..42344320a9 100644 --- a/src/generators/java/JavaGenerator.ts +++ b/src/generators/java/JavaGenerator.ts @@ -12,6 +12,7 @@ export interface JavaOptions extends CommonGeneratorOptions { collectionType?: 'List' | 'Array'; namingConvention?: CommonNamingConvention; } + export class JavaGenerator extends AbstractGenerator { static defaultOptions: JavaOptions = { ...defaultGeneratorOptions, diff --git a/src/generators/java/JavaRenderer.ts b/src/generators/java/JavaRenderer.ts index 6ba48e9d61..f8ddb1103a 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 { - return this.runPreset('getter', { propertyName, property, type }); + return this.runPreset('getter', { propertyName, property, type}); } runSetterPreset(propertyName: string, property: CommonModel, type: PropertyType = PropertyType.property): Promise { - 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 = { 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`; } - 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`; } - 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 55f2dd000f..468b3aea7a 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 77e344d7c1..121db5eb0b 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 de3257c124..f50969668e 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 extends CommonPreset { diff --git a/test/generators/java/JavaGenerator.spec.ts b/test/generators/java/JavaGenerator.spec.ts index 5539aea172..79b7e60169 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 7bf3fc1bc7..faafbd9740 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'); }); });