|
| 1 | +/** |
| 2 | + * This is a Gradle script for proto linting. |
| 3 | + * |
| 4 | + * Implementation: |
| 5 | + * 1. Integrates the 'buf' CLI tool to compile .proto files and generate a JSON AST (Abstract Syntax Tree) image. |
| 6 | + * 2. Uses Groovy's JsonSlurper to parse the AST image. |
| 7 | + * 3. Traverses all Enum definitions and validates them against preset rules. |
| 8 | + * |
| 9 | + * Current Validation: |
| 10 | + * Enforces the java-tron API evolution standard (see https://github.com/tronprotocol/java-tron/issues/6515). |
| 11 | + * Except for legacy enums in the 'legacyEnums' whitelist, all newly defined Enums MUST reserve index 0 for a field starting with 'UNKNOWN_'. |
| 12 | + * This ensures robust forward/backward compatibility during proto3 JSON serialization. |
| 13 | + */ |
| 14 | +import groovy.json.JsonSlurper |
| 15 | +import org.gradle.internal.os.OperatingSystem |
| 16 | + |
| 17 | +// Define the required buf CLI version |
| 18 | +def bufVersion = "1.61.0" |
| 19 | +def currentOs = OperatingSystem.current() |
| 20 | +def platform = currentOs.isMacOsX() ? "osx" : (currentOs.isWindows() ? "windows" : "linux") |
| 21 | +def machine = rootProject.archInfo.isArm64 ? "aarch_64" : "x86_64" |
| 22 | + |
| 23 | +// Create a custom configuration for the buf CLI tool to keep it isolated from the classpath |
| 24 | +configurations { |
| 25 | + bufTool |
| 26 | +} |
| 27 | + |
| 28 | +// Depend on the buf executable published on Maven Central |
| 29 | +dependencies { |
| 30 | + bufTool "build.buf:buf:${bufVersion}:${platform}-${machine}@exe" |
| 31 | +} |
| 32 | + |
| 33 | +task protoLint { |
| 34 | + group = "verification" |
| 35 | + description = "Validate Protobuf Enums using buf generated JSON AST. Enforces 'UNKNOWN_' prefix for index 0 to ensure JSON serialization backward compatibility." |
| 36 | + |
| 37 | + // Explicitly declare dependency to avoid Gradle implicit dependency warnings |
| 38 | + // because generateProto outputs to the parent 'src' directory. |
| 39 | + dependsOn 'generateProto' |
| 40 | + |
| 41 | + // Incremental build support: Only run if proto files or the script itself changes |
| 42 | + inputs.dir('src/main/protos') |
| 43 | + inputs.file('protoLint.gradle') |
| 44 | + |
| 45 | + def markerFile = file("${buildDir}/tmp/protoLint.done") |
| 46 | + outputs.file(markerFile) |
| 47 | + |
| 48 | + doLast { |
| 49 | + def bufExe = configurations.bufTool.singleFile |
| 50 | + if (!bufExe.exists() || !bufExe.canExecute()) { |
| 51 | + bufExe.setExecutable(true) |
| 52 | + } |
| 53 | + |
| 54 | + // 1. Legacy Whitelist |
| 55 | + // Contains enums that existed before the 'UNKNOWN_' standard was enforced. |
| 56 | + // Format: "filename.proto:EnumName" or "filename.proto:MessageName.EnumName" |
| 57 | + def legacyEnums = [ |
| 58 | + "core/contract/common.proto:ResourceCode", |
| 59 | + "core/contract/smart_contract.proto:SmartContract.ABI.Entry.EntryType", |
| 60 | + "core/contract/smart_contract.proto:SmartContract.ABI.Entry.StateMutabilityType", |
| 61 | + "core/Tron.proto:AccountType", |
| 62 | + "core/Tron.proto:ReasonCode", |
| 63 | + "core/Tron.proto:Proposal.State", |
| 64 | + "core/Tron.proto:MarketOrder.State", |
| 65 | + "core/Tron.proto:Permission.PermissionType", |
| 66 | + "core/Tron.proto:Transaction.Contract.ContractType", |
| 67 | + "core/Tron.proto:Transaction.Result.code", |
| 68 | + "core/Tron.proto:Transaction.Result.contractResult", |
| 69 | + "core/Tron.proto:TransactionInfo.code", |
| 70 | + "core/Tron.proto:BlockInventory.Type", |
| 71 | + "core/Tron.proto:Inventory.InventoryType", |
| 72 | + "core/Tron.proto:Items.ItemType", |
| 73 | + "core/Tron.proto:PBFTMessage.MsgType", |
| 74 | + "core/Tron.proto:PBFTMessage.DataType", |
| 75 | + "api/api.proto:Return.response_code", |
| 76 | + "api/api.proto:TransactionSignWeight.Result.response_code", |
| 77 | + "api/api.proto:TransactionApprovedList.Result.response_code", |
| 78 | + "api/zksnark.proto:ZksnarkResponse.Code" |
| 79 | + ].collect { it.toString() } as Set |
| 80 | + |
| 81 | + // 2. Build JSON AST Image using buf CLI |
| 82 | + def imageDir = file("${buildDir}/tmp/buf") |
| 83 | + def imageFile = file("${imageDir}/proto-ast.json") |
| 84 | + imageDir.mkdirs() |
| 85 | + |
| 86 | + println "🔍 Generating Proto AST image using buf CLI..." |
| 87 | + |
| 88 | + def bufConfig = '{"version":"v1beta1","build":{"roots":["src/main/protos","build/extracted-include-protos/main"]}}' |
| 89 | + |
| 90 | + def execResult = exec { |
| 91 | + commandLine bufExe.absolutePath, 'build', '.', '--config', bufConfig, '-o', "${imageFile.absolutePath}#format=json" |
| 92 | + ignoreExitValue = true |
| 93 | + } |
| 94 | + |
| 95 | + if (execResult.exitValue != 0) { |
| 96 | + throw new GradleException("Failed to generate AST image. Ensure your .proto files are valid. Buf exited with code ${execResult.exitValue}") |
| 97 | + } |
| 98 | + |
| 99 | + if (!imageFile.exists()) { |
| 100 | + throw new GradleException("Failed to locate generated buf image at ${imageFile.absolutePath}") |
| 101 | + } |
| 102 | + |
| 103 | + // 3. Parse AST and Validate Enums |
| 104 | + def descriptorSet |
| 105 | + try { |
| 106 | + descriptorSet = new JsonSlurper().parse(imageFile) |
| 107 | + } catch (Exception e) { |
| 108 | + throw new GradleException("Failed to parse buf generated JSON AST: ${e.message}", e) |
| 109 | + } |
| 110 | + |
| 111 | + def errors = [] |
| 112 | + |
| 113 | + descriptorSet.file?.each { protoFile -> |
| 114 | + // Skip Google's and gRPC's internal protos as they are outside our control |
| 115 | + if (protoFile.name?.startsWith("google/") || protoFile.name?.startsWith("grpc/")) { |
| 116 | + return |
| 117 | + } |
| 118 | + |
| 119 | + // A queue-based (BFS) approach to safely traverse all nested messages and enums |
| 120 | + // without using recursion, ensuring support for any nesting depth. |
| 121 | + def queue = [] |
| 122 | + |
| 123 | + // Initial seed: top-level enums and messages |
| 124 | + protoFile.enumType?.each { queue << [def: it, parentName: ""] } |
| 125 | + protoFile.messageType?.each { queue << [def: it, parentName: ""] } |
| 126 | + |
| 127 | + while (!queue.isEmpty()) { |
| 128 | + def item = queue.remove(0) |
| 129 | + def definition = item.def |
| 130 | + def parentName = item.parentName |
| 131 | + |
| 132 | + if (definition.value != null) { |
| 133 | + // This is an Enum definition |
| 134 | + def fullName = parentName ? "${parentName}.${definition.name}" : definition.name |
| 135 | + def identifier = "${protoFile.name}:${fullName}".toString() |
| 136 | + |
| 137 | + if (!legacyEnums.contains(identifier)) { |
| 138 | + def zeroValue = definition.value?.find { it.number == 0 } |
| 139 | + if (zeroValue && !zeroValue.name?.startsWith("UNKNOWN_")) { |
| 140 | + errors << "[${protoFile.name}] Enum \"${fullName}\" has index 0: \"${zeroValue.name}\". It MUST start with \"UNKNOWN_\"." |
| 141 | + } |
| 142 | + } |
| 143 | + } else { |
| 144 | + // This is a Message definition, look for nested enums and nested messages |
| 145 | + def currentMsgName = parentName ? "${parentName}.${definition.name}" : definition.name |
| 146 | + |
| 147 | + definition.enumType?.each { queue << [def: it, parentName: currentMsgName] } |
| 148 | + definition.nestedType?.each { queue << [def: it, parentName: currentMsgName] } |
| 149 | + } |
| 150 | + } |
| 151 | + } |
| 152 | + |
| 153 | + // 4. Report Results |
| 154 | + if (!errors.isEmpty()) { |
| 155 | + println "\n🚨 [Protocol Design Violation] The following enums violate the java-tron API evolution standard (Issue #6515):" |
| 156 | + errors.each { println " - $it" } |
| 157 | + println "\n💡 Recommendation: To ensure proto3 JSON serialization backward compatibility, newly defined enums MUST reserve index 0 for an 'UNKNOWN_XXX' field.\n" |
| 158 | + throw new GradleException("Proto Enum validation failed. See above for details.") |
| 159 | + } else { |
| 160 | + println "✅ Proto Enum validation passed!" |
| 161 | + // Update marker file for Gradle incremental build cache |
| 162 | + markerFile.text = "Success: " + new Date().toString() |
| 163 | + } |
| 164 | + } |
| 165 | +} |
| 166 | + |
| 167 | +check.dependsOn protoLint |
0 commit comments