From 540f2d35a8e959d3b2f1cb5cc924204d2a132174 Mon Sep 17 00:00:00 2001 From: Daniil Sedov Date: Fri, 12 Jul 2024 15:33:49 +0300 Subject: [PATCH] fix: getter name collisions in TS wrappers (#556) Exports a TS mapping from Tact getter names to their TS analogues --- CHANGELOG.md | 1 + cspell.json | 1 + src/bindings/writeTypescript.ts | 24 +- .../getter-names-conflict.spec.ts.snap | 299 ++++++++++++++++++ .../contracts/getter-names-conflict.tact | 16 + .../getter-names-conflict.spec.ts | 30 ++ tact.config.json | 5 + 7 files changed, 375 insertions(+), 1 deletion(-) create mode 100644 src/test/e2e-emulated/__snapshots__/getter-names-conflict.spec.ts.snap create mode 100644 src/test/e2e-emulated/contracts/getter-names-conflict.tact create mode 100644 src/test/e2e-emulated/getter-names-conflict.spec.ts diff --git a/CHANGELOG.md b/CHANGELOG.md index 4e8822ff0..2be3061f2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -37,6 +37,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Usage of `initOf` inside of `init()` does not cause error `135` anymore: PR [#521](https://github.com/tact-lang/tact/issues/521) - Usage of `newAddress` with hash parts shorter than 64 hexadecimal digits does not cause constant evaluation error `Invalid address hash length` anymore: PR [#525](https://github.com/tact-lang/tact/pull/525) - Introduced a streamlined error logger for compilation pipeline to support third-party tools: PR [#509](https://github.com/tact-lang/tact/pull/509) +- Collisions of PascalCase getter names in generated wrappers are now checked: PR [#556](https://github.com/tact-lang/tact/pull/556) ## [1.4.0] - 2024-06-21 diff --git a/cspell.json b/cspell.json index 6df46b2af..aeb676014 100644 --- a/cspell.json +++ b/cspell.json @@ -100,6 +100,7 @@ "src/test/e2e-emulated/contracts/intrinsics.tact", "src/test/e2e-emulated/contracts/strings.tact", "src/test/compilation-fail/fail-const-eval.spec.ts", + "src/test/e2e-emulated/getter-names-conflict.spec.ts", "stdlib/stdlib.fc" ] } diff --git a/src/bindings/writeTypescript.ts b/src/bindings/writeTypescript.ts index 092d1b49f..1ff535a72 100644 --- a/src/bindings/writeTypescript.ts +++ b/src/bindings/writeTypescript.ts @@ -214,18 +214,40 @@ export function writeTypescript( w.append(`]`); w.append(); + const getterNames: Map = new Map(); + // Getters w.append(`const ${abi.name}_getters: ABIGetter[] = [`); w.inIndent(() => { if (abi.getters) { for (const t of abi.getters) { w.append(JSON.stringify(t) + ","); + + let getterName = changeCase.pascalCase(t.name); + if (Array.from(getterNames.values()).includes(getterName)) { + getterName = t.name; + } + getterNames.set(t.name, getterName); } } }); w.append(`]`); w.append(); + // Getter mapping + w.append( + `export const ${abi.name}_getterMapping: { [key: string]: string } = {`, + ); + w.inIndent(() => { + if (abi.getters) { + for (const t of abi.getters) { + w.append(`'${t.name}': 'get${getterNames.get(t.name)}',`); + } + } + }); + w.append(`}`); + w.append(); + // Receivers w.append(`const ${abi.name}_receivers: ABIReceiver[] = [`); w.inIndent(() => { @@ -564,7 +586,7 @@ export function writeTypescript( if (abi.getters) { for (const g of abi.getters) { w.append( - `async get${changeCase.pascalCase(g.name)}(${["provider: ContractProvider", ...writeArguments(g.arguments ? g.arguments : [])].join(", ")}) {`, + `async get${getterNames.get(g.name)}(${["provider: ContractProvider", ...writeArguments(g.arguments ? g.arguments : [])].join(", ")}) {`, ); w.inIndent(() => { w.append(`let builder = new TupleBuilder();`); diff --git a/src/test/e2e-emulated/__snapshots__/getter-names-conflict.spec.ts.snap b/src/test/e2e-emulated/__snapshots__/getter-names-conflict.spec.ts.snap new file mode 100644 index 000000000..02f9615bf --- /dev/null +++ b/src/test/e2e-emulated/__snapshots__/getter-names-conflict.spec.ts.snap @@ -0,0 +1,299 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`getter-names-conflict should handle conflicts in getter names correctly 1`] = ` +Test { + "abi": { + "errors": { + "10": { + "message": "Dictionary error", + }, + "128": { + "message": "Null reference exception", + }, + "129": { + "message": "Invalid serialization prefix", + }, + "13": { + "message": "Out of gas error", + }, + "130": { + "message": "Invalid incoming message", + }, + "131": { + "message": "Constraints error", + }, + "132": { + "message": "Access denied", + }, + "133": { + "message": "Contract stopped", + }, + "134": { + "message": "Invalid argument", + }, + "135": { + "message": "Code of a contract was not found", + }, + "136": { + "message": "Invalid address", + }, + "137": { + "message": "Masterchain support is not enabled for this contract", + }, + "2": { + "message": "Stack underflow", + }, + "3": { + "message": "Stack overflow", + }, + "32": { + "message": "Method ID not found", + }, + "34": { + "message": "Action is invalid or not supported", + }, + "37": { + "message": "Not enough TON", + }, + "38": { + "message": "Not enough extra-currencies", + }, + "4": { + "message": "Integer overflow", + }, + "5": { + "message": "Integer out of expected range", + }, + "6": { + "message": "Invalid opcode", + }, + "7": { + "message": "Type check error", + }, + "8": { + "message": "Cell overflow", + }, + "9": { + "message": "Cell underflow", + }, + }, + "getters": [ + { + "arguments": [], + "name": "testGetter", + "returnType": { + "format": 257, + "kind": "simple", + "optional": false, + "type": "int", + }, + }, + { + "arguments": [], + "name": "test_getter", + "returnType": { + "format": 257, + "kind": "simple", + "optional": false, + "type": "int", + }, + }, + { + "arguments": [], + "name": "Test_getter", + "returnType": { + "format": 257, + "kind": "simple", + "optional": false, + "type": "int", + }, + }, + ], + "receivers": [ + { + "message": { + "kind": "empty", + }, + "receiver": "internal", + }, + ], + "types": [ + { + "fields": [ + { + "name": "code", + "type": { + "kind": "simple", + "optional": false, + "type": "cell", + }, + }, + { + "name": "data", + "type": { + "kind": "simple", + "optional": false, + "type": "cell", + }, + }, + ], + "header": null, + "name": "StateInit", + }, + { + "fields": [ + { + "name": "bounced", + "type": { + "kind": "simple", + "optional": false, + "type": "bool", + }, + }, + { + "name": "sender", + "type": { + "kind": "simple", + "optional": false, + "type": "address", + }, + }, + { + "name": "value", + "type": { + "format": 257, + "kind": "simple", + "optional": false, + "type": "int", + }, + }, + { + "name": "raw", + "type": { + "kind": "simple", + "optional": false, + "type": "slice", + }, + }, + ], + "header": null, + "name": "Context", + }, + { + "fields": [ + { + "name": "bounce", + "type": { + "kind": "simple", + "optional": false, + "type": "bool", + }, + }, + { + "name": "to", + "type": { + "kind": "simple", + "optional": false, + "type": "address", + }, + }, + { + "name": "value", + "type": { + "format": 257, + "kind": "simple", + "optional": false, + "type": "int", + }, + }, + { + "name": "mode", + "type": { + "format": 257, + "kind": "simple", + "optional": false, + "type": "int", + }, + }, + { + "name": "body", + "type": { + "kind": "simple", + "optional": true, + "type": "cell", + }, + }, + { + "name": "code", + "type": { + "kind": "simple", + "optional": true, + "type": "cell", + }, + }, + { + "name": "data", + "type": { + "kind": "simple", + "optional": true, + "type": "cell", + }, + }, + ], + "header": null, + "name": "SendParameters", + }, + ], + }, + "address": kQD80WcWxBhERIUXYb8hyvNv5hK-iToKR4Q5dEGUZ2NWl9Mx, + "init": { + "code": x{FF00F4A413F4BCF2C80B} + x{62_} + x{D001D0D3030171B0A301FA400120D74981010BBAF2E08820D70B0A208104FFBAF2D0898309BAF2E088545053036F04F86102F862DB3C59DB3CF2E08230C8F84301CC7F01CA00C9ED54} + x{ED44D0D401F863D20030916DE0F828D70B0A8309BAF2E089DB3C} + x{6D} + x{0192307FE07021D749C21F953020D70B1FDEC00001D749C121B0917FE070} + x{2_} + x{2_} + x{B9BDCDB3CDB3C31} + x{ED44D0D401F863D20030916DE0F828D70B0A8309BAF2E089DB3C} + x{6D} + x{73} + x{BBE1FDB3CDB3C31} + x{ED44D0D401F863D20030916DE0F828D70B0A8309BAF2E089DB3C} + x{6D} + x{71} + x{2_} + x{BB1A6DB3CDB3C31} + x{ED44D0D401F863D20030916DE0F828D70B0A8309BAF2E089DB3C} + x{6D} + x{72} + x{B82BEED44D0D20001}, + "data": x{4_} + x{C_} + x{A15891_} + x{FF00F4A413F4BCF2C80B} + x{62_} + x{D001D0D3030171B0A301FA400120D74981010BBAF2E08820D70B0A208104FFBAF2D0898309BAF2E088545053036F04F86102F862DB3C59DB3CF2E08230C8F84301CC7F01CA00C9ED54} + x{ED44D0D401F863D20030916DE0F828D70B0A8309BAF2E089DB3C} + x{6D} + x{0192307FE07021D749C21F953020D70B1FDEC00001D749C121B0917FE070} + x{2_} + x{2_} + x{B9BDCDB3CDB3C31} + x{ED44D0D401F863D20030916DE0F828D70B0A8309BAF2E089DB3C} + x{6D} + x{73} + x{BBE1FDB3CDB3C31} + x{ED44D0D401F863D20030916DE0F828D70B0A8309BAF2E089DB3C} + x{6D} + x{71} + x{2_} + x{BB1A6DB3CDB3C31} + x{ED44D0D401F863D20030916DE0F828D70B0A8309BAF2E089DB3C} + x{6D} + x{72} + x{B82BEED44D0D20001}, + }, +} +`; diff --git a/src/test/e2e-emulated/contracts/getter-names-conflict.tact b/src/test/e2e-emulated/contracts/getter-names-conflict.tact new file mode 100644 index 000000000..bcd694034 --- /dev/null +++ b/src/test/e2e-emulated/contracts/getter-names-conflict.tact @@ -0,0 +1,16 @@ +contract Test { + receive () {} + + get fun testGetter(): Int { + return 1; + } + + get fun test_getter(): Int { + return 2; + } + + get fun Test_getter(): Int { + return 3; + } +} + diff --git a/src/test/e2e-emulated/getter-names-conflict.spec.ts b/src/test/e2e-emulated/getter-names-conflict.spec.ts new file mode 100644 index 000000000..9eda79803 --- /dev/null +++ b/src/test/e2e-emulated/getter-names-conflict.spec.ts @@ -0,0 +1,30 @@ +import { toNano } from "@ton/core"; +import { ContractSystem } from "@tact-lang/emulator"; +import { __DANGER_resetNodeId } from "../../grammar/ast"; +import { + Test, + Test_getterMapping, +} from "./contracts/output/getter-names-conflict_Test"; + +describe("getter-names-conflict", () => { + beforeEach(() => { + __DANGER_resetNodeId(); + }); + it("should handle conflicts in getter names correctly", async () => { + // Init + const system = await ContractSystem.create(); + const treasure = system.treasure("treasure"); + const contract = system.open(await Test.fromInit()); + await contract.send(treasure, { value: toNano("10") }, null); + await system.run(); + expect(contract).toMatchSnapshot(); + + expect(await contract.getTestGetter()).toBe(1n); + expect(await contract.gettest_getter()).toBe(2n); + expect(await contract.getTest_getter()).toBe(3n); + + expect(Test_getterMapping["testGetter"]).toBe("getTestGetter"); + expect(Test_getterMapping["test_getter"]).toBe("gettest_getter"); + expect(Test_getterMapping["Test_getter"]).toBe("getTest_getter"); + }); +}); diff --git a/tact.config.json b/tact.config.json index 62d7a3055..205786a5c 100644 --- a/tact.config.json +++ b/tact.config.json @@ -365,6 +365,11 @@ "options": { "debug": true } + }, + { + "name": "getter-names-conflict", + "path": "./src/test/e2e-emulated/contracts/getter-names-conflict.tact", + "output": "./src/test/e2e-emulated/contracts/output" } ] }