From 56e388c90fb92edfa7883709318108c340d18677 Mon Sep 17 00:00:00 2001 From: Sarah Fortune Date: Sun, 27 Jul 2025 22:02:12 -0400 Subject: [PATCH] Add a check to the proto scripts to warn about using int64 types. (#5174) * Add a check to the proto scripts to warn about using int64 types. Javascript cannot represent the full range of int64. So, when the protos are deserialized from JSON int64's are converted to strings. The typescript code is expecting a number and not a string, and this causes errors. This was noticed before now because in the vscode protobus and hostbridge, the proto messages are not serialized and deserialized, they are just passed around as JS objects. However, in IntelliJ the protos are serialized when they are sent through the ProtoBus. When the response messages contains and int64, it is deserialized to a string instead of a number for safety. This is causes parts of Cline to fail in IntelliJ, e.g. the task history view won't load because `Task.getTotalTasksSize()` returns a string when it is expecting a number. * Make checkProtos shorter * Update scripts/build-proto.mjs Co-authored-by: ellipsis-dev[bot] <65095814+ellipsis-dev[bot]@users.noreply.github.com> * Update scripts/build-proto.mjs Co-authored-by: ellipsis-dev[bot] <65095814+ellipsis-dev[bot]@users.noreply.github.com> * Update scripts/build-proto.mjs Co-authored-by: ellipsis-dev[bot] <65095814+ellipsis-dev[bot]@users.noreply.github.com> * Fix typo * Fix typo * Fix bad merge --------- Co-authored-by: ellipsis-dev[bot] <65095814+ellipsis-dev[bot]@users.noreply.github.com> --- package.json | 2 +- scripts/build-proto.mjs | 58 +++++++++++++++++++++++-- scripts/generate-host-bridge-client.mjs | 14 +++--- scripts/generate-protobus-setup.mjs | 10 ++++- scripts/proto-utils.mjs | 12 +++-- 5 files changed, 80 insertions(+), 16 deletions(-) diff --git a/package.json b/package.json index d6b6c51af3..6a0509e2f8 100644 --- a/package.json +++ b/package.json @@ -345,7 +345,7 @@ "watch:esbuild": "node esbuild.mjs --watch", "watch:tsc": "tsc --noEmit --watch --project tsconfig.json", "package": "npm run check-types && npm run build:webview && npm run lint && node esbuild.mjs --production", - "protos": "node scripts/build-proto.mjs && node scripts/generate-protobus-setup.mjs && node scripts/generate-host-bridge-client.mjs", + "protos": "node scripts/build-proto.mjs", "postprotos": "prettier src/shared/proto src/core/controller src/hosts/ webview-ui/src/services src/generated --write --log-level warn", "clean": "rimraf dist dist-standalone webview-ui/build src/generated out/", "compile-tests": "node ./scripts/build-tests.js", diff --git a/scripts/build-proto.mjs b/scripts/build-proto.mjs index 1e1c188e81..93160d0cb8 100755 --- a/scripts/build-proto.mjs +++ b/scripts/build-proto.mjs @@ -7,8 +7,10 @@ import { globby } from "globby" import { createRequire } from "module" import os from "os" import * as path from "path" -import { fileURLToPath } from "url" import { rmrf } from "./file-utils.mjs" +import { main as generateProtoBusSetup } from "./generate-protobus-setup.mjs" +import { main as generateHostBridgeClient } from "./generate-host-bridge-client.mjs" +import { loadProtoDescriptorSet } from "./proto-utils.mjs" const require = createRequire(import.meta.url) const PROTOC = path.join(require.resolve("grpc-tools"), "../bin/protoc") @@ -34,9 +36,14 @@ const TS_PROTO_OPTIONS = [ ] async function main() { - console.log(chalk.bold.blue("Compiling Protocol Buffers...")) - await cleanup() + await compileProtos() + await checkProtos() + await generateProtoBusSetup() + await generateHostBridgeClient() +} +async function compileProtos() { + console.log(chalk.bold.blue("Compiling Protocol Buffers...")) // Check for Apple Silicon compatibility before proceeding checkAppleSiliconCompatibility() @@ -180,6 +187,51 @@ function checkAppleSiliconCompatibility() { } } +const int64TypeNames = ["TYPE_INT64", "TYPE_UINT64", "TYPE_SINT64", "TYPE_FIXED64", "TYPE_SFIXED64"] + +async function checkProtos() { + const proto = await loadProtoDescriptorSet() + const int64Fields = [] + + for (const [packageName, packageDef] of Object.entries(proto)) { + for (const [messageName, def] of Object.entries(packageDef)) { + // Skip service definitions + if (def && typeof def === "object" && "service" in def) { + continue + } + // Check message fields + if (def && def.type && def.type.field) { + for (const field of def.type.field) { + if (int64TypeNames.includes(field.type)) { + const name = `${packageName}.${messageName}.${field.name}` + int64Fields.push({ + name: name, + type: field.type, + }) + } + } + } + } + } + + if (int64Fields.length > 0) { + console.log(chalk.yellow(`\nWarning: Found ${int64Fields.length} fields using 64-bit integer types`)) + for (const field of int64Fields) { + const typeNames = { + TYPE_INT64: "int64", + TYPE_UINT64: "uint64", + TYPE_SINT64: "sint64", + TYPE_FIXED64: "fixed64", + TYPE_SFIXED64: "sfixed64", + } + log_verbose(chalk.yellow(` - ${field.name} (${typeNames[field.type]})`)) + } + log_verbose(chalk.yellow("\nWARNING: 64-bit integer fields detected in proto definitions")) + log_verbose(chalk.yellow("JavaScript cannot safely represent integers larger than 2^53-1 (Number.MAX_SAFE_INTEGER).")) + log_verbose(chalk.yellow("Consider using string representation for large numbers or implementing BigInt support.\n")) + } +} + function log_verbose(s) { if (process.argv.includes("-v") || process.argv.includes("--verbose")) { console.log(s) diff --git a/scripts/generate-host-bridge-client.mjs b/scripts/generate-host-bridge-client.mjs index d2c2e4cb73..00004cc556 100755 --- a/scripts/generate-host-bridge-client.mjs +++ b/scripts/generate-host-bridge-client.mjs @@ -15,7 +15,7 @@ const VSCODE_CLIENT_FILE = path.resolve("src/generated/hosts/vscode/hostbridge-g /** * Main function to generate the host bridge client */ -async function main() { +export async function main() { const { hostServices } = await loadServicesFromProtoDescriptor() await generateTypesFile(hostServices) @@ -234,8 +234,10 @@ const ${name}ServiceRegistry = createServiceRegistry("${name}") ${methods}` } -// Run the main function -main().catch((error) => { - console.error(chalk.red("Error:"), error) - process.exit(1) -}) +// Only run main if this script is executed directly +if (import.meta.url === `file://${process.argv[1]}`) { + main().catch((error) => { + console.error(chalk.red("Error:"), error) + process.exit(1) + }) +} diff --git a/scripts/generate-protobus-setup.mjs b/scripts/generate-protobus-setup.mjs index f22e7c0bbb..6dce1c2576 100755 --- a/scripts/generate-protobus-setup.mjs +++ b/scripts/generate-protobus-setup.mjs @@ -12,7 +12,7 @@ const STANDALONE_SERVER_SETUP_FILE = path.resolve("src/generated/hosts/standalon const SCRIPT_NAME = path.relative(process.cwd(), fileURLToPath(import.meta.url)) -async function main() { +export async function main() { const { protobusServices } = await loadServicesFromProtoDescriptor() await generateWebviewProtobusClients(protobusServices) await generateVscodeServiceTypes(protobusServices) @@ -205,4 +205,10 @@ function getDirName(serviceName) { return domain.charAt(0).toLowerCase() + domain.slice(1) } -main() +// Only run main if this script is executed directly +if (import.meta.url === `file://${process.argv[1]}`) { + main().catch((error) => { + console.error(chalk.red("Error:"), error) + process.exit(1) + }) +} diff --git a/scripts/proto-utils.mjs b/scripts/proto-utils.mjs index 52ff0f6453..906d1413ed 100755 --- a/scripts/proto-utils.mjs +++ b/scripts/proto-utils.mjs @@ -10,7 +10,7 @@ const DESCRIPTOR_SET = path.resolve("dist-standalone/proto/descriptor_set.pb") const typeNameToFQN = new Map() function addTypeNameToFqn(name, fqn) { - if (typeNameToFQN.has(name)) { + if (typeNameToFQN.has(name) && typeNameToFQN.get(name) !== fqn) { throw new Error(`Proto type ${name} redefined (${fqn}).`) } typeNameToFQN.set(name, fqn) @@ -23,11 +23,15 @@ export function getFqn(name) { return typeNameToFQN.get(name) } -export async function loadServicesFromProtoDescriptor() { - // Load service definitions from descriptor set +export async function loadProtoDescriptorSet() { const descriptorBuffer = await fs.readFile(DESCRIPTOR_SET) const packageDefinition = protoLoader.loadFileDescriptorSetFromBuffer(descriptorBuffer) - const proto = grpc.loadPackageDefinition(packageDefinition) + return grpc.loadPackageDefinition(packageDefinition) +} + +export async function loadServicesFromProtoDescriptor() { + // Load service definitions from descriptor set + const proto = await loadProtoDescriptorSet() // Extract host services and proto messages from the proto definition const hostServices = {}