Skip to content

Commit 13d71e9

Browse files
authored
fix: ensure reserved keywords can never be rendered as properties for Java class (#269)
1 parent 53b3f34 commit 13d71e9

File tree

9 files changed

+189
-17
lines changed

9 files changed

+189
-17
lines changed

docs/generators.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -48,7 +48,7 @@ Below is a list of additional options available for a given generator.
4848
| `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` |
4949
| `namingConvention` | Object | Options for naming conventions. | - |
5050
| `namingConvention.type` | Function | A function that returns the format of the type. | _Returns pascal cased name_ |
51-
| `namingConvention.property` | Function | A function that returns the format of the property. | _Returns camel cased name_ |
51+
| `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_ |
5252

5353
### [JavaScript](../src/generators/javascript/JavaScriptGenerator.ts)
5454

src/generators/java/JavaGenerator.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ export interface JavaOptions extends CommonGeneratorOptions<JavaPreset> {
1212
collectionType?: 'List' | 'Array';
1313
namingConvention?: CommonNamingConvention;
1414
}
15+
1516
export class JavaGenerator extends AbstractGenerator<JavaOptions> {
1617
static defaultOptions: JavaOptions = {
1718
...defaultGeneratorOptions,

src/generators/java/JavaRenderer.ts

Lines changed: 61 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,61 @@ import { JavaGenerator, JavaOptions } from './JavaGenerator';
33
import { CommonModel, CommonInputModel, Preset } from '../../models';
44
import { FormatHelpers, ModelKind, TypeHelpers } from '../../helpers';
55

6+
/**
7+
* List of reserved java keywords that may not be rendered as is.
8+
*/
9+
export const ReservedJavaKeywordList = [
10+
'abstract',
11+
'continue',
12+
'for',
13+
'new',
14+
'switch assert',
15+
'default',
16+
'goto',
17+
'package',
18+
'synchronized',
19+
'boolean',
20+
'do',
21+
'if',
22+
'private',
23+
'this',
24+
'break',
25+
'double',
26+
'implements',
27+
'protected',
28+
'throw',
29+
'byte',
30+
'else',
31+
'import',
32+
'public',
33+
'throws',
34+
'case',
35+
'enum',
36+
'instanceof',
37+
'return',
38+
'transient',
39+
'catch',
40+
'extends',
41+
'int',
42+
'short',
43+
'try',
44+
'char',
45+
'final',
46+
'interface',
47+
'static',
48+
'void',
49+
'class',
50+
'finally',
51+
'long',
52+
'strictfp',
53+
'volatile',
54+
'const',
55+
'float',
56+
'native',
57+
'super',
58+
'while'
59+
];
60+
661
/**
762
* Common renderer for Java types
863
*
@@ -18,6 +73,10 @@ export abstract class JavaRenderer extends AbstractRenderer<JavaOptions, JavaGen
1873
) {
1974
super(options, generator, presets, model, inputModel);
2075
}
76+
77+
static isReservedJavaKeyword(word: string): boolean {
78+
return ReservedJavaKeywordList.includes(word);
79+
}
2180

2281
/**
2382
* 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
2988
*/
3089
nameType(name: string | undefined, model?: CommonModel): string {
3190
return this.options?.namingConvention?.type
32-
? this.options.namingConvention.type(name, { model: model || this.model, inputModel: this.inputModel })
91+
? this.options.namingConvention.type(name, { model: model || this.model, inputModel: this.inputModel, isReservedKeyword: JavaRenderer.isReservedJavaKeyword(`${name}`) })
3392
: name || '';
3493
}
3594

@@ -41,7 +100,7 @@ export abstract class JavaRenderer extends AbstractRenderer<JavaOptions, JavaGen
41100
*/
42101
nameProperty(propertyName: string | undefined, property?: CommonModel): string {
43102
return this.options?.namingConvention?.property
44-
? this.options.namingConvention.property(propertyName, { model: this.model, inputModel: this.inputModel, property })
103+
? this.options.namingConvention.property(propertyName, { model: this.model, inputModel: this.inputModel, property, isReservedKeyword: JavaRenderer.isReservedJavaKeyword(`${propertyName}`) })
45104
: propertyName || '';
46105
}
47106

src/generators/java/renderers/ClassRenderer.ts

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@ export class ClassRenderer extends JavaRenderer {
2424
this.addDependency('import java.util.Map;');
2525
}
2626

27-
const formattedName = this.nameType(this.model.$id);
27+
const formattedName = this.nameType(`${this.model.$id}`);
2828
return `public class ${formattedName} {
2929
${this.indent(this.renderBlock(content, 2))}
3030
}`;
@@ -94,11 +94,11 @@ ${this.indent(this.renderBlock(content, 2))}
9494
}
9595

9696
runGetterPreset(propertyName: string, property: CommonModel, type: PropertyType = PropertyType.property): Promise<string> {
97-
return this.runPreset('getter', { propertyName, property, type });
97+
return this.runPreset('getter', { propertyName, property, type});
9898
}
9999

100100
runSetterPreset(propertyName: string, property: CommonModel, type: PropertyType = PropertyType.property): Promise<string> {
101-
return this.runPreset('setter', { propertyName, property, type });
101+
return this.runPreset('setter', { propertyName, property, type});
102102
}
103103
}
104104

@@ -115,21 +115,21 @@ export const JAVA_DEFAULT_CLASS_PRESET: ClassPreset<ClassRenderer> = {
115115
return `private ${propertyType} ${propertyName};`;
116116
},
117117
getter({ renderer, propertyName, property, type }) {
118-
propertyName = renderer.nameProperty(propertyName, property);
118+
const formattedPropertyName = renderer.nameProperty(propertyName, property);
119119
const getterName = `get${FormatHelpers.toPascalCase(propertyName)}`;
120120
let getterType = renderer.renderType(property);
121121
if (type === PropertyType.additionalProperty || type === PropertyType.patternProperties) {
122122
getterType = `Map<String, ${getterType}>`;
123123
}
124-
return `public ${getterType} ${getterName}() { return this.${propertyName}; }`;
124+
return `public ${getterType} ${getterName}() { return this.${formattedPropertyName}; }`;
125125
},
126126
setter({ renderer, propertyName, property, type }) {
127-
propertyName = renderer.nameProperty(propertyName, property);
127+
const formattedPropertyName = renderer.nameProperty(propertyName, property);
128128
const setterName = FormatHelpers.toPascalCase(propertyName);
129129
let setterType = renderer.renderType(property);
130130
if (type === PropertyType.additionalProperty || type === PropertyType.patternProperties) {
131131
setterType = `Map<String, ${setterType}>`;
132132
}
133-
return `public void set${setterName}(${setterType} ${propertyName}) { this.${propertyName} = ${propertyName}; }`;
133+
return `public void set${setterName}(${setterType} ${formattedPropertyName}) { this.${formattedPropertyName} = ${formattedPropertyName}; }`;
134134
}
135135
};

src/generators/typescript/TypeScriptRenderer.ts

Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,70 @@ import { FormatHelpers } from '../../helpers';
55
import { CommonModel, CommonInputModel, Preset, PropertyType } from '../../models';
66
import { DefaultPropertyNames, getUniquePropertyName } from '../../helpers/NameHelpers';
77

8+
export const ReservedTypeScriptKeywords = [
9+
'break',
10+
'case',
11+
'catch',
12+
'class',
13+
'const',
14+
'continue',
15+
'debugger',
16+
'default',
17+
'delete',
18+
'do',
19+
'else',
20+
'enum',
21+
'export',
22+
'extends',
23+
'false',
24+
'finally',
25+
'for',
26+
'function',
27+
'if',
28+
'import',
29+
'in',
30+
'instanceof',
31+
'new',
32+
'null',
33+
'return',
34+
'super',
35+
'switch',
36+
'this',
37+
'throw',
38+
'true',
39+
'try',
40+
'typeof',
41+
'var',
42+
'void',
43+
'while',
44+
'with',
45+
'any',
46+
'boolean',
47+
'constructor',
48+
'declare',
49+
'get',
50+
'module',
51+
'require',
52+
'number',
53+
'set',
54+
'string',
55+
'symbol',
56+
'type',
57+
'from',
58+
'of',
59+
// Strict mode reserved words
60+
'as',
61+
'implements',
62+
'interface',
63+
'let',
64+
'package',
65+
'private',
66+
'protected',
67+
'public',
68+
'static',
69+
'yield'
70+
];
71+
872
/**
973
* Common renderer for TypeScript types
1074
*

src/helpers/NameHelpers.ts

Lines changed: 23 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -19,29 +19,47 @@ export enum DefaultPropertyNames {
1919
*/
2020
export function getUniquePropertyName(rootModel: CommonModel, propertyName: string): string {
2121
if (Object.keys(rootModel.properties || {}).includes(propertyName)) {
22-
return getUniquePropertyName(rootModel, `_${propertyName}`);
22+
return getUniquePropertyName(rootModel, `reserved_${propertyName}`);
2323
}
2424
return propertyName;
2525
}
2626

27+
/**
28+
* The common naming convention context type.
29+
*/
30+
export type CommonTypeNamingConventionCtx = { model: CommonModel, inputModel: CommonInputModel, isReservedKeyword?: boolean};
31+
export type CommonPropertyNamingConventionCtx = { model: CommonModel, inputModel: CommonInputModel, property?: CommonModel, isReservedKeyword?: boolean};
32+
2733
/**
2834
* The common naming convention type shared between generators for different languages.
2935
*/
3036
export type CommonNamingConvention = {
31-
type?: (name: string | undefined, ctx: { model: CommonModel, inputModel: CommonInputModel }) => string;
32-
property?: (name: string | undefined, ctx: { model: CommonModel, inputModel: CommonInputModel, property?: CommonModel }) => string;
37+
type?: (name: string | undefined, ctx: CommonTypeNamingConventionCtx) => string;
38+
property?: (name: string | undefined, ctx: CommonPropertyNamingConventionCtx) => string;
3339
};
3440

3541
/**
3642
* A CommonNamingConvention implementation shared between generators for different languages.
3743
*/
3844
export const CommonNamingConventionImplementation: CommonNamingConvention = {
39-
type: (name: string | undefined) => {
45+
type: (name, ctx) => {
4046
if (!name) {return '';}
47+
if (ctx.isReservedKeyword) {
48+
name = `reserved_${name}`;
49+
}
4150
return FormatHelpers.toPascalCase(name);
4251
},
43-
property: (name: string | undefined) => {
52+
property: (name, ctx) => {
4453
if (!name) {return '';}
54+
if (ctx.isReservedKeyword) {
55+
// If name is considered reserved, make sure we rename it appropriately
56+
// and make sure no clashes occur.
57+
name = FormatHelpers.toCamelCase(`reserved_${name}`);
58+
if (Object.keys(ctx.model.properties || {}).includes(name)) {
59+
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
60+
return CommonNamingConventionImplementation.property!(name, ctx);
61+
}
62+
}
4563
return FormatHelpers.toCamelCase(name);
4664
}
4765
};

src/models/Preset.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@ export enum PropertyType {
2424
export interface PropertyArgs {
2525
propertyName: string;
2626
property: CommonModel;
27-
type: PropertyType
27+
type: PropertyType;
2828
}
2929

3030
export interface ClassPreset<R extends AbstractRenderer, O extends object = any> extends CommonPreset<R, O> {

test/generators/java/JavaGenerator.spec.ts

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,36 @@ describe('JavaGenerator', () => {
66
generator = new JavaGenerator();
77
});
88

9+
test('should not render reserved keyword', async () => {
10+
const doc = {
11+
$id: 'Address',
12+
type: 'object',
13+
properties: {
14+
enum: { type: 'string' },
15+
reservedEnum: { type: 'string' }
16+
},
17+
additionalProperties: false
18+
};
19+
const expected = `public class Address {
20+
private String reservedReservedEnum;
21+
private String reservedEnum;
22+
23+
public String getEnum() { return this.reservedReservedEnum; }
24+
public void setEnum(String reservedReservedEnum) { this.reservedReservedEnum = reservedReservedEnum; }
25+
26+
public String getReservedEnum() { return this.reservedEnum; }
27+
public void setReservedEnum(String reservedEnum) { this.reservedEnum = reservedEnum; }
28+
}`;
29+
30+
const inputModel = await generator.process(doc);
31+
const model = inputModel.models['Address'];
32+
33+
let classModel = await generator.renderClass(model, inputModel);
34+
expect(classModel.result).toEqual(expected);
35+
36+
classModel = await generator.render(model, inputModel);
37+
expect(classModel.result).toEqual(expected);
38+
});
939
test('should render `class` type', async () => {
1040
const doc = {
1141
$id: 'Address',

test/helpers/NameHelpers.spec.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ describe('NameHelpers', () => {
1515

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

18-
expect(additionalPropertiesName).toEqual('_additionalProperties');
18+
expect(additionalPropertiesName).toEqual('reserved_additionalProperties');
1919
});
2020
});
2121

0 commit comments

Comments
 (0)