From eb86847a0359acd85d50d970b6b4eb2a7b4e2eb2 Mon Sep 17 00:00:00 2001 From: Dimitri Mitropoulos Date: Mon, 13 Feb 2023 13:10:27 -0500 Subject: [PATCH] handles JavaScript reserved words --- .../generated-react/dist/descriptorset.bin | Bin 6261 -> 6398 bytes .../dist/example-TodoService_connectquery.ts | 17 +++ .../dist/example_connectweb.ts | 9 ++ packages/generated-react/example.proto | 1 + .../src/generateDts.ts | 5 +- .../src/generateTs.ts | 6 +- .../src/utils.test.ts | 39 +++++++ .../protoc-gen-connect-query/src/utils.ts | 106 ++++++++++++++++++ 8 files changed, 178 insertions(+), 5 deletions(-) create mode 100644 packages/protoc-gen-connect-query/src/utils.test.ts diff --git a/packages/generated-react/dist/descriptorset.bin b/packages/generated-react/dist/descriptorset.bin index 64469183b7ef3fd4a612032e45e22811b8435975..b80257d743239904a1d8e138fbbc8caded0fefbc 100644 GIT binary patch delta 134 zcmexr@Xv5VkO(M8Kov45MS!f#m&XS#=yiR#4O3A!o|bI3gR#Vd2Bo|9!QQ?2*zUu@?@2g U*b0jBOY)5`ePd?fV31${09=3>?EnA( delta 60 zcmV-C0K@ method.methodKind === MethodKind.Unary) .forEach((method, index, filteredMethods) => { - // TODO idempotency f.print(makeJsDoc(method)); f.print( - `export const ${localName(method)} = `, + `export const ${sanitizeIdentifiers(localName(method))} = `, f.import('createQueryService', '@bufbuild/connect-query'), `({`, ); diff --git a/packages/protoc-gen-connect-query/src/utils.test.ts b/packages/protoc-gen-connect-query/src/utils.test.ts new file mode 100644 index 00000000..6fe1a547 --- /dev/null +++ b/packages/protoc-gen-connect-query/src/utils.test.ts @@ -0,0 +1,39 @@ +// Copyright 2021-2022 Buf Technologies, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import { describe, expect, it } from "@jest/globals"; + +import { isReservedWord, sanitizeIdentifiers } from "./utils"; + +describe('isReservedWord', () => { + it('returns true for reserved words', () => { + expect(isReservedWord('delete')).toStrictEqual(true) + }); + + it('returns false for non reserved words', () => { + expect(isReservedWord('deleteRPC')).toStrictEqual(false) + }); +}); + +describe('sanitizeIdentifiers', () => { + it('sanitizes reserved words', () => { + const actual = sanitizeIdentifiers('delete'); + expect(actual).toStrictEqual('delete_RPC') + }); + + it('does nothing to non reserved words', () => { + const actual = sanitizeIdentifiers('deleteRPC'); + expect(actual).toStrictEqual('deleteRPC') + }); +}); diff --git a/packages/protoc-gen-connect-query/src/utils.ts b/packages/protoc-gen-connect-query/src/utils.ts index 514af0e1..483d01e9 100644 --- a/packages/protoc-gen-connect-query/src/utils.ts +++ b/packages/protoc-gen-connect-query/src/utils.ts @@ -18,3 +18,109 @@ import type { createEcmaScriptPlugin } from '@bufbuild/protoplugin'; * Extracts the type of PluginInit from @bufbuild/protoplugin */ export type PluginInit = Required[0]>; + +/** + * a list of reserved words that cannot be used as identifiers. + */ +const reservedWords = [ + 'abstract', + 'as', + 'arguments', + 'await', + 'boolean', + 'break', + 'byte', + 'case', + 'catch', + 'char', + 'class', + 'const', + 'continue', + 'debugger', + 'default', + 'delete', + 'do', + 'double', + 'else', + 'enum', + 'eval', + 'export', + 'extends', + 'false', + 'final', + 'finally', + 'float', + 'for', + 'function', + 'goto', + 'if', + 'implements', + 'import', + 'in', + 'instanceof', + 'int', + 'interface', + 'let', + 'long', + 'native', + 'new', + 'null', + 'package', + 'private', + 'protected', + 'public', + 'return', + 'short', + 'static', + 'super', + 'switch', + 'synchronized', + 'this', + 'throw', + 'throws', + 'transient', + 'true', + 'try', + 'typeof', + 'var', + 'void', + 'volatile', + 'while', + 'with', + 'yield', +] as const; + +type ReservedWord = (typeof reservedWords)[number]; + +/** + * Predicate for indicating whether a given identifier is a reserved word in JavaScript + */ +export const isReservedWord = (word: ReservedWord | T): word is ReservedWord => ( + reservedWords.includes(word as ReservedWord) +); + +/** + * When JavaScript identifiers are formed from the names of Service RPCs, it is possible to hit a situation where the names collide. + * + * For example, `delete` is a reserved word in JavaScript. Therefore the input protofile: + * ```proto + * service MyService { + * rpc Delete(empty) returns (Empty) + * } + * ``` + * + * would otherwise be serialized to: + * ```ts + * const delete = .... + * ``` + * + * which is a syntax error. + * + * This function fixes that by appending `_RPC` to the end of the identifier. + */ +export const sanitizeIdentifiers = (word: T) => { + if (isReservedWord(word)) { + return `${word}_RPC` as const; + } + return word; +} \ No newline at end of file