Skip to content

Commit

Permalink
fix: ensure reserved keywords can never be rendered as properties for…
Browse files Browse the repository at this point in the history
… Java class (#269)
  • Loading branch information
jonaslagoni authored Jul 27, 2021
1 parent 53b3f34 commit 13d71e9
Show file tree
Hide file tree
Showing 9 changed files with 189 additions and 17 deletions.
2 changes: 1 addition & 1 deletion docs/generators.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)

Expand Down
1 change: 1 addition & 0 deletions src/generators/java/JavaGenerator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
63 changes: 61 additions & 2 deletions src/generators/java/JavaRenderer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
*
Expand All @@ -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.
Expand All @@ -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 || '';
}

Expand All @@ -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 || '';
}

Expand Down
14 changes: 7 additions & 7 deletions src/generators/java/renderers/ClassRenderer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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))}
}`;
Expand Down Expand Up @@ -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});
}
}

Expand All @@ -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}; }`;
}
};
64 changes: 64 additions & 0 deletions src/generators/typescript/TypeScriptRenderer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
*
Expand Down
28 changes: 23 additions & 5 deletions src/helpers/NameHelpers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}
};
2 changes: 1 addition & 1 deletion src/models/Preset.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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> {
Expand Down
30 changes: 30 additions & 0 deletions test/generators/java/JavaGenerator.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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',
Expand Down
2 changes: 1 addition & 1 deletion test/helpers/NameHelpers.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ describe('NameHelpers', () => {

const additionalPropertiesName = getUniquePropertyName(model, DefaultPropertyNames.additionalProperties);

expect(additionalPropertiesName).toEqual('_additionalProperties');
expect(additionalPropertiesName).toEqual('reserved_additionalProperties');
});
});

Expand Down

0 comments on commit 13d71e9

Please sign in to comment.