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>
This commit is contained in:
Sarah Fortune
2025-07-27 22:02:12 -04:00
committed by GitHub
co-authored by ellipsis-dev[bot]
parent 586d804a01
commit 56e388c90f
5 changed files with 80 additions and 16 deletions
+55 -3
View File
@@ -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)
+8 -6
View File
@@ -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)
})
}
+8 -2
View File
@@ -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)
})
}
+8 -4
View File
@@ -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 = {}