Skip to content

Commit 95d211c

Browse files
authored
fix(typescript): Handle invalid type names (#317)
* chore(typescript): Use tsutils isValidIdentifier * fix(typescript): Handle invalid type names
1 parent bf87c14 commit 95d211c

File tree

3 files changed

+62
-30
lines changed

3 files changed

+62
-30
lines changed

plugins/typescript/src/core/schemaToEnumDeclaration.ts

Lines changed: 1 addition & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -1,33 +1,10 @@
11
import { pascal } from "case";
22
import { SchemaObject } from "openapi3-ts/oas30";
33
import ts, { factory as f } from "typescript";
4+
import { isValidIdentifier } from "tsutils";
45
import { convertNumberToWord } from "../utils/getEnumProperties";
56
import { Context, getJSDocComment } from "./schemaToTypeAliasDeclaration";
67

7-
/**
8-
* Function to check if a string is a valid TypeScript identifier
9-
*
10-
* @param name Name to check
11-
*/
12-
function isValidIdentifier(name: string): boolean {
13-
if (name.length === 0) {
14-
return false;
15-
}
16-
17-
const firstChar = name.charCodeAt(0);
18-
if (!ts.isIdentifierStart(firstChar, ts.ScriptTarget.Latest)) {
19-
return false;
20-
}
21-
22-
for (let i = 1; i < name.length; i++) {
23-
if (!ts.isIdentifierPart(name.charCodeAt(i), ts.ScriptTarget.Latest)) {
24-
return false;
25-
}
26-
}
27-
28-
return true;
29-
}
30-
318
/**
329
* Add Enum support when transforming an OpenAPI Schema Object to Typescript Nodes.
3310
*

plugins/typescript/src/core/schemaToTypeAliasDeclaration.test.ts

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -955,6 +955,30 @@ describe("schemaToTypeAliasDeclaration", () => {
955955
`);
956956
});
957957

958+
it("should generate valid identifier from number name", () => {
959+
const schema: SchemaObject = {};
960+
961+
expect(printSchema(schema, "200")).toMatchInlineSnapshot(`
962+
"export type TwoHundred = void;"
963+
`);
964+
});
965+
966+
it("should generate valid identifier from symbol name", () => {
967+
const schema: SchemaObject = {};
968+
969+
expect(printSchema(schema, "-")).toMatchInlineSnapshot(`
970+
"export type _ = void;"
971+
`);
972+
});
973+
974+
it("should generate valid identifier from invalid name", () => {
975+
const schema: SchemaObject = {};
976+
977+
expect(printSchema(schema, "🙂")).toMatchInlineSnapshot(`
978+
"export type _ = void;"
979+
`);
980+
});
981+
958982
it("should generate a `never` if the combined type is broken", () => {
959983
const schema: SchemaObject = {
960984
allOf: [{ type: "string" }, { type: "number" }],

plugins/typescript/src/core/schemaToTypeAliasDeclaration.ts

Lines changed: 37 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,12 @@ import {
1111
} from "openapi3-ts/oas30";
1212
import { singular } from "pluralize";
1313
import { isValidIdentifier } from "tsutils";
14-
import ts, { factory as f } from "typescript";
14+
import ts, {
15+
factory as f,
16+
isIdentifierPart,
17+
isIdentifierStart,
18+
} from "typescript";
19+
import { convertNumberToWord } from "../utils/getEnumProperties";
1520
import { getReferenceSchema } from "./getReference";
1621

1722
type RemoveIndex<T> = {
@@ -56,11 +61,37 @@ export const schemaToTypeAliasDeclaration = (
5661
const jsDocNode = isSchemaObject(schema)
5762
? getJSDocComment(schema, context)
5863
: undefined;
64+
65+
let identifier = pascal(name);
66+
67+
if (identifier.length > 0 && !isNaN(Number(identifier))) {
68+
// If the identifier can be cast to a number, convert it to a word.
69+
identifier = pascal(convertNumberToWord(Number(identifier)));
70+
} else {
71+
// If the identifier does not start with a valid character but valid identifier part, prefix it with an underscore.
72+
if (
73+
!isIdentifierStart(identifier.charCodeAt(0), ts.ScriptTarget.Latest) &&
74+
isIdentifierPart(identifier.charCodeAt(0), ts.ScriptTarget.Latest)
75+
) {
76+
identifier = `_${identifier}`;
77+
}
78+
79+
// If the identifier is still not valid, remove invalid characters.
80+
if (!isValidIdentifier(identifier)) {
81+
identifier = identifier.replace(/[^a-zA-Z0-9_]/g, "");
82+
}
83+
84+
// If the identifier is now empty, set it to "_".
85+
if (identifier.length === 0) {
86+
identifier = "_";
87+
}
88+
}
89+
5990
const declarationNode = f.createTypeAliasDeclaration(
6091
[f.createModifier(ts.SyntaxKind.ExportKeyword)],
61-
pascal(name),
92+
identifier,
6293
undefined,
63-
getType(schema, context, name)
94+
getType(schema, context, identifier)
6495
);
6596

6697
return jsDocNode ? [jsDocNode, declarationNode] : [declarationNode];
@@ -89,10 +120,10 @@ export const getType = (
89120

90121
let refNode: ts.TypeNode = f.createTypeReferenceNode(
91122
namespace === context.currentComponent
92-
? f.createIdentifier(pascal(name))
123+
? f.createIdentifier(name)
93124
: f.createQualifiedName(
94125
f.createIdentifier(pascal(namespace)),
95-
f.createIdentifier(pascal(name))
126+
f.createIdentifier(name)
96127
)
97128
);
98129

@@ -174,7 +205,7 @@ export const getType = (
174205
if (schema.enum) {
175206
if (isNodeEnum) {
176207
return f.createUnionTypeNode([
177-
f.createTypeReferenceNode(f.createIdentifier(pascal(name || ""))),
208+
f.createTypeReferenceNode(f.createIdentifier(name || "")),
178209
...(schema.nullable ? [f.createLiteralTypeNode(f.createNull())] : []),
179210
]);
180211
}

0 commit comments

Comments
 (0)