@tsonic/emitter 0.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +34 -0
- package/scripts/update-golden-tests.ts +119 -0
- package/src/adapter-generator.ts +112 -0
- package/src/array.test.ts +301 -0
- package/src/constants.ts +32 -0
- package/src/core/exports.ts +36 -0
- package/src/core/imports.test.ts +83 -0
- package/src/core/imports.ts +243 -0
- package/src/core/index.ts +9 -0
- package/src/core/module-emitter/assembly.ts +83 -0
- package/src/core/module-emitter/header.ts +19 -0
- package/src/core/module-emitter/index.ts +17 -0
- package/src/core/module-emitter/namespace.ts +39 -0
- package/src/core/module-emitter/orchestrator.ts +98 -0
- package/src/core/module-emitter/separation.ts +41 -0
- package/src/core/module-emitter/static-container.ts +75 -0
- package/src/core/module-emitter.test.ts +154 -0
- package/src/core/module-emitter.ts +6 -0
- package/src/core/module-map.ts +218 -0
- package/src/core/options.ts +16 -0
- package/src/core/type-params.ts +34 -0
- package/src/emitter-types/context.ts +91 -0
- package/src/emitter-types/core.ts +138 -0
- package/src/emitter-types/csharp-types.ts +42 -0
- package/src/emitter-types/formatting.ts +13 -0
- package/src/emitter-types/fqn.ts +81 -0
- package/src/emitter-types/index.ts +31 -0
- package/src/emitter.ts +107 -0
- package/src/expression-emitter.ts +112 -0
- package/src/expressions/access.ts +104 -0
- package/src/expressions/calls.ts +264 -0
- package/src/expressions/collections.ts +354 -0
- package/src/expressions/functions.ts +71 -0
- package/src/expressions/identifiers.ts +125 -0
- package/src/expressions/index.test.ts +515 -0
- package/src/expressions/index.ts +38 -0
- package/src/expressions/literals.ts +138 -0
- package/src/expressions/operators.ts +211 -0
- package/src/expressions/other.ts +63 -0
- package/src/generator-exchange.ts +120 -0
- package/src/generator.test.ts +193 -0
- package/src/golden-tests/config-parser.ts +67 -0
- package/src/golden-tests/discovery.ts +61 -0
- package/src/golden-tests/index.ts +10 -0
- package/src/golden-tests/registration.ts +26 -0
- package/src/golden-tests/runner.ts +131 -0
- package/src/golden-tests/tree-builder.ts +43 -0
- package/src/golden-tests/types.ts +21 -0
- package/src/golden.test.ts +40 -0
- package/src/hierarchical-bindings.test.ts +258 -0
- package/src/index.ts +14 -0
- package/src/integration.test.ts +303 -0
- package/src/specialization/call-site-rewriting.test.ts +99 -0
- package/src/specialization/collection/expressions.ts +184 -0
- package/src/specialization/collection/index.ts +7 -0
- package/src/specialization/collection/orchestrator.ts +25 -0
- package/src/specialization/collection/statements.ts +91 -0
- package/src/specialization/collection.ts +10 -0
- package/src/specialization/generation.ts +189 -0
- package/src/specialization/generic-classes.test.ts +59 -0
- package/src/specialization/generic-functions.test.ts +292 -0
- package/src/specialization/helpers.ts +39 -0
- package/src/specialization/index.ts +28 -0
- package/src/specialization/interfaces.test.ts +151 -0
- package/src/specialization/naming.ts +34 -0
- package/src/specialization/substitution.ts +186 -0
- package/src/specialization/type-aliases.test.ts +134 -0
- package/src/specialization/types.ts +19 -0
- package/src/specialization-generator.ts +23 -0
- package/src/statement-emitter.ts +117 -0
- package/src/statements/blocks.ts +115 -0
- package/src/statements/classes/helpers.ts +9 -0
- package/src/statements/classes/index.ts +13 -0
- package/src/statements/classes/inline-types.ts +79 -0
- package/src/statements/classes/members/constructors.ts +123 -0
- package/src/statements/classes/members/index.ts +8 -0
- package/src/statements/classes/members/methods.ts +137 -0
- package/src/statements/classes/members/orchestrator.ts +33 -0
- package/src/statements/classes/members/properties.ts +62 -0
- package/src/statements/classes/members.ts +6 -0
- package/src/statements/classes/parameters.ts +69 -0
- package/src/statements/classes/properties.ts +95 -0
- package/src/statements/classes.ts +14 -0
- package/src/statements/control/conditionals.ts +134 -0
- package/src/statements/control/exceptions.ts +59 -0
- package/src/statements/control/index.ts +11 -0
- package/src/statements/control/loops.ts +250 -0
- package/src/statements/control.ts +14 -0
- package/src/statements/declarations/classes.ts +89 -0
- package/src/statements/declarations/enums.ts +32 -0
- package/src/statements/declarations/functions.ts +147 -0
- package/src/statements/declarations/index.ts +10 -0
- package/src/statements/declarations/interfaces.ts +116 -0
- package/src/statements/declarations/structs.test.ts +182 -0
- package/src/statements/declarations/type-aliases.ts +104 -0
- package/src/statements/declarations/variables.ts +159 -0
- package/src/statements/declarations.ts +13 -0
- package/src/statements/index.test.ts +258 -0
- package/src/statements/index.ts +43 -0
- package/src/type-assertion.test.ts +143 -0
- package/src/type-emitter.ts +18 -0
- package/src/types/arrays.ts +21 -0
- package/src/types/dictionaries.ts +52 -0
- package/src/types/emitter.ts +76 -0
- package/src/types/functions.ts +45 -0
- package/src/types/index.test.ts +116 -0
- package/src/types/index.ts +14 -0
- package/src/types/intersections.ts +19 -0
- package/src/types/literals.ts +26 -0
- package/src/types/objects.ts +27 -0
- package/src/types/parameters.test.ts +146 -0
- package/src/types/parameters.ts +95 -0
- package/src/types/primitives.ts +24 -0
- package/src/types/references.ts +187 -0
- package/src/types/unions.test.ts +397 -0
- package/src/types/unions.ts +62 -0
- package/src/types.ts +33 -0
- package/testcases/README.md +213 -0
- package/testcases/arrays/basic/ArrayLiteral.ts +4 -0
- package/testcases/arrays/basic/config.yaml +1 -0
- package/testcases/arrays/destructuring/ArrayDestructure.ts +4 -0
- package/testcases/arrays/destructuring/config.yaml +1 -0
- package/testcases/arrays/methods/ArrayMethods.ts +6 -0
- package/testcases/arrays/methods/config.yaml +1 -0
- package/testcases/arrays/multidimensional/MultiDimensional.ts +10 -0
- package/testcases/arrays/multidimensional/config.yaml +1 -0
- package/testcases/arrays/spread/ArraySpread.ts +3 -0
- package/testcases/arrays/spread/config.yaml +1 -0
- package/testcases/async/basic/AsyncFunction.ts +5 -0
- package/testcases/async/basic/config.yaml +1 -0
- package/testcases/classes/abstract/AbstractClasses.ts +53 -0
- package/testcases/classes/abstract/config.yaml +1 -0
- package/testcases/classes/basic/Person.ts +12 -0
- package/testcases/classes/basic/config.yaml +1 -0
- package/testcases/classes/constructor/User.ts +11 -0
- package/testcases/classes/constructor/config.yaml +1 -0
- package/testcases/classes/field-inference/Counter.ts +11 -0
- package/testcases/classes/field-inference/config.yaml +1 -0
- package/testcases/classes/inheritance/Inheritance.ts +24 -0
- package/testcases/classes/inheritance/config.yaml +1 -0
- package/testcases/classes/static-members/MathHelper.ts +12 -0
- package/testcases/classes/static-members/config.yaml +1 -0
- package/testcases/control-flow/error-handling/ErrorHandling.ts +13 -0
- package/testcases/control-flow/error-handling/config.yaml +1 -0
- package/testcases/control-flow/loops/Loops.ts +21 -0
- package/testcases/control-flow/loops/config.yaml +1 -0
- package/testcases/control-flow/switch/SwitchStatement.ts +15 -0
- package/testcases/control-flow/switch/config.yaml +1 -0
- package/testcases/edge-cases/complex-expressions/ComplexExpressions.ts +10 -0
- package/testcases/edge-cases/complex-expressions/config.yaml +1 -0
- package/testcases/edge-cases/nested-scopes/NestedScopes.ts +10 -0
- package/testcases/edge-cases/nested-scopes/config.yaml +1 -0
- package/testcases/edge-cases/shadowing/Shadowing.ts +16 -0
- package/testcases/edge-cases/shadowing/config.yaml +1 -0
- package/testcases/functions/arrow/ArrowFunction.ts +5 -0
- package/testcases/functions/arrow/config.yaml +1 -0
- package/testcases/functions/basic/Greet.ts +3 -0
- package/testcases/functions/basic/config.yaml +1 -0
- package/testcases/functions/closures/Closures.ts +11 -0
- package/testcases/functions/closures/config.yaml +1 -0
- package/testcases/functions/default-params/DefaultParams.ts +7 -0
- package/testcases/functions/default-params/config.yaml +1 -0
- package/testcases/functions/rest-params/RestParams.ts +7 -0
- package/testcases/functions/rest-params/config.yaml +1 -0
- package/testcases/functions/type-guards/TypeGuards.ts +52 -0
- package/testcases/functions/type-guards/config.yaml +1 -0
- package/testcases/operators/logical/LogicalOperators.ts +11 -0
- package/testcases/operators/logical/config.yaml +1 -0
- package/testcases/operators/nullish-coalescing/NullishCoalescing.ts +7 -0
- package/testcases/operators/nullish-coalescing/config.yaml +1 -0
- package/testcases/operators/optional-chaining/OptionalChaining.ts +15 -0
- package/testcases/operators/optional-chaining/config.yaml +1 -0
- package/testcases/real-world/advanced-generics/advanced-generics.ts +116 -0
- package/testcases/real-world/advanced-generics/config.yaml +1 -0
- package/testcases/real-world/async-ops/async-ops.ts +67 -0
- package/testcases/real-world/async-ops/config.yaml +1 -0
- package/testcases/real-world/business-logic/business-logic.ts +215 -0
- package/testcases/real-world/business-logic/config.yaml +1 -0
- package/testcases/real-world/calculator/calculator.ts +29 -0
- package/testcases/real-world/calculator/config.yaml +1 -0
- package/testcases/real-world/data-structures/config.yaml +1 -0
- package/testcases/real-world/data-structures/data-structures.ts +133 -0
- package/testcases/real-world/functional/config.yaml +1 -0
- package/testcases/real-world/functional/functional.ts +116 -0
- package/testcases/real-world/shapes/config.yaml +1 -0
- package/testcases/real-world/shapes/shapes.ts +87 -0
- package/testcases/real-world/string-utils/config.yaml +1 -0
- package/testcases/real-world/string-utils/string-utils.ts +47 -0
- package/testcases/real-world/todo-list/config.yaml +1 -0
- package/testcases/real-world/todo-list/todo-list.ts +52 -0
- package/testcases/real-world/type-guards/config.yaml +1 -0
- package/testcases/real-world/type-guards/type-guards.ts +71 -0
- package/testcases/structs/basic/Point.ts +9 -0
- package/testcases/structs/basic/config.yaml +1 -0
- package/testcases/types/conditional/ConditionalTypes.ts +35 -0
- package/testcases/types/conditional/config.yaml +1 -0
- package/testcases/types/constants/ModuleConstants.ts +6 -0
- package/testcases/types/constants/config.yaml +1 -0
- package/testcases/types/generics/Generics.ts +15 -0
- package/testcases/types/generics/config.yaml +1 -0
- package/testcases/types/interfaces/Interfaces.ts +14 -0
- package/testcases/types/interfaces/config.yaml +1 -0
- package/testcases/types/mapped/MappedTypes.ts +27 -0
- package/testcases/types/mapped/config.yaml +1 -0
- package/testcases/types/tuples-intersections/TuplesAndIntersections.ts +46 -0
- package/testcases/types/tuples-intersections/config.yaml +1 -0
- package/testcases/types/unions/UnionTypes.ts +11 -0
- package/testcases/types/unions/config.yaml +1 -0
- package/tsconfig.json +14 -0
|
@@ -0,0 +1,397 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tests for union type emission
|
|
3
|
+
* Verifies TypeScript unions map to C# Union<T1, T2>
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { describe, it } from "mocha";
|
|
7
|
+
import { expect } from "chai";
|
|
8
|
+
import { emitModule } from "../emitter.js";
|
|
9
|
+
import { IrModule } from "@tsonic/frontend";
|
|
10
|
+
|
|
11
|
+
describe("Union Type Emission", () => {
|
|
12
|
+
it("should emit nullable type as T?", () => {
|
|
13
|
+
const module: IrModule = {
|
|
14
|
+
kind: "module",
|
|
15
|
+
filePath: "/test/nullable.ts",
|
|
16
|
+
namespace: "Test",
|
|
17
|
+
className: "nullable",
|
|
18
|
+
isStaticContainer: true,
|
|
19
|
+
imports: [],
|
|
20
|
+
exports: [],
|
|
21
|
+
body: [
|
|
22
|
+
{
|
|
23
|
+
kind: "variableDeclaration",
|
|
24
|
+
declarationKind: "const",
|
|
25
|
+
isExported: false,
|
|
26
|
+
declarations: [
|
|
27
|
+
{
|
|
28
|
+
kind: "variableDeclarator",
|
|
29
|
+
name: { kind: "identifierPattern", name: "maybeString" },
|
|
30
|
+
type: {
|
|
31
|
+
kind: "unionType",
|
|
32
|
+
types: [
|
|
33
|
+
{ kind: "primitiveType", name: "string" },
|
|
34
|
+
{ kind: "primitiveType", name: "null" },
|
|
35
|
+
],
|
|
36
|
+
},
|
|
37
|
+
initializer: { kind: "literal", value: null },
|
|
38
|
+
},
|
|
39
|
+
],
|
|
40
|
+
},
|
|
41
|
+
],
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
const code = emitModule(module);
|
|
45
|
+
|
|
46
|
+
// Should use nullable syntax
|
|
47
|
+
expect(code).to.include("string? maybeString");
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
it("should emit two-type union as Union<T1, T2>", () => {
|
|
51
|
+
const module: IrModule = {
|
|
52
|
+
kind: "module",
|
|
53
|
+
filePath: "/test/union.ts",
|
|
54
|
+
namespace: "Test",
|
|
55
|
+
className: "union",
|
|
56
|
+
isStaticContainer: true,
|
|
57
|
+
imports: [],
|
|
58
|
+
exports: [],
|
|
59
|
+
body: [
|
|
60
|
+
{
|
|
61
|
+
kind: "variableDeclaration",
|
|
62
|
+
declarationKind: "const",
|
|
63
|
+
isExported: false,
|
|
64
|
+
declarations: [
|
|
65
|
+
{
|
|
66
|
+
kind: "variableDeclarator",
|
|
67
|
+
name: { kind: "identifierPattern", name: "value" },
|
|
68
|
+
type: {
|
|
69
|
+
kind: "unionType",
|
|
70
|
+
types: [
|
|
71
|
+
{ kind: "primitiveType", name: "string" },
|
|
72
|
+
{ kind: "primitiveType", name: "number" },
|
|
73
|
+
],
|
|
74
|
+
},
|
|
75
|
+
initializer: { kind: "literal", value: "hello" },
|
|
76
|
+
},
|
|
77
|
+
],
|
|
78
|
+
},
|
|
79
|
+
],
|
|
80
|
+
};
|
|
81
|
+
|
|
82
|
+
const code = emitModule(module);
|
|
83
|
+
|
|
84
|
+
// Should use Union<T1, T2>
|
|
85
|
+
expect(code).to.include("Union<string, double> value");
|
|
86
|
+
expect(code).to.include("using Tsonic.Runtime");
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
it("should emit function returning union type", () => {
|
|
90
|
+
const module: IrModule = {
|
|
91
|
+
kind: "module",
|
|
92
|
+
filePath: "/test/unionFunc.ts",
|
|
93
|
+
namespace: "Test",
|
|
94
|
+
className: "unionFunc",
|
|
95
|
+
isStaticContainer: true,
|
|
96
|
+
imports: [],
|
|
97
|
+
exports: [],
|
|
98
|
+
body: [
|
|
99
|
+
{
|
|
100
|
+
kind: "functionDeclaration",
|
|
101
|
+
name: "getValue",
|
|
102
|
+
parameters: [],
|
|
103
|
+
returnType: {
|
|
104
|
+
kind: "unionType",
|
|
105
|
+
types: [
|
|
106
|
+
{ kind: "primitiveType", name: "string" },
|
|
107
|
+
{ kind: "primitiveType", name: "number" },
|
|
108
|
+
],
|
|
109
|
+
},
|
|
110
|
+
body: {
|
|
111
|
+
kind: "blockStatement",
|
|
112
|
+
statements: [
|
|
113
|
+
{
|
|
114
|
+
kind: "returnStatement",
|
|
115
|
+
expression: { kind: "literal", value: "hello" },
|
|
116
|
+
},
|
|
117
|
+
],
|
|
118
|
+
},
|
|
119
|
+
isAsync: false,
|
|
120
|
+
isGenerator: false,
|
|
121
|
+
isExported: true,
|
|
122
|
+
},
|
|
123
|
+
],
|
|
124
|
+
};
|
|
125
|
+
|
|
126
|
+
const code = emitModule(module);
|
|
127
|
+
|
|
128
|
+
// Should return Union<string, double>
|
|
129
|
+
expect(code).to.include("public static Union<string, double> getValue()");
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
it("should emit function parameter with union type", () => {
|
|
133
|
+
const module: IrModule = {
|
|
134
|
+
kind: "module",
|
|
135
|
+
filePath: "/test/unionParam.ts",
|
|
136
|
+
namespace: "Test",
|
|
137
|
+
className: "unionParam",
|
|
138
|
+
isStaticContainer: true,
|
|
139
|
+
imports: [],
|
|
140
|
+
exports: [],
|
|
141
|
+
body: [
|
|
142
|
+
{
|
|
143
|
+
kind: "functionDeclaration",
|
|
144
|
+
name: "process",
|
|
145
|
+
parameters: [
|
|
146
|
+
{
|
|
147
|
+
kind: "parameter",
|
|
148
|
+
pattern: { kind: "identifierPattern", name: "input" },
|
|
149
|
+
type: {
|
|
150
|
+
kind: "unionType",
|
|
151
|
+
types: [
|
|
152
|
+
{ kind: "primitiveType", name: "string" },
|
|
153
|
+
{ kind: "primitiveType", name: "boolean" },
|
|
154
|
+
],
|
|
155
|
+
},
|
|
156
|
+
isOptional: false,
|
|
157
|
+
isRest: false,
|
|
158
|
+
passing: "value",
|
|
159
|
+
},
|
|
160
|
+
],
|
|
161
|
+
returnType: { kind: "voidType" },
|
|
162
|
+
body: {
|
|
163
|
+
kind: "blockStatement",
|
|
164
|
+
statements: [],
|
|
165
|
+
},
|
|
166
|
+
isAsync: false,
|
|
167
|
+
isGenerator: false,
|
|
168
|
+
isExported: true,
|
|
169
|
+
},
|
|
170
|
+
],
|
|
171
|
+
};
|
|
172
|
+
|
|
173
|
+
const code = emitModule(module);
|
|
174
|
+
|
|
175
|
+
// Should accept Union<string, bool> parameter
|
|
176
|
+
expect(code).to.include("process(Union<string, bool> input)");
|
|
177
|
+
});
|
|
178
|
+
|
|
179
|
+
it("should handle union with custom types", () => {
|
|
180
|
+
const module: IrModule = {
|
|
181
|
+
kind: "module",
|
|
182
|
+
filePath: "/test/customUnion.ts",
|
|
183
|
+
namespace: "Test",
|
|
184
|
+
className: "customUnion",
|
|
185
|
+
isStaticContainer: true,
|
|
186
|
+
imports: [],
|
|
187
|
+
exports: [],
|
|
188
|
+
body: [
|
|
189
|
+
{
|
|
190
|
+
kind: "functionDeclaration",
|
|
191
|
+
name: "getResult",
|
|
192
|
+
parameters: [],
|
|
193
|
+
returnType: {
|
|
194
|
+
kind: "unionType",
|
|
195
|
+
types: [
|
|
196
|
+
{ kind: "referenceType", name: "User", typeArguments: [] },
|
|
197
|
+
{ kind: "referenceType", name: "Product", typeArguments: [] },
|
|
198
|
+
],
|
|
199
|
+
},
|
|
200
|
+
body: {
|
|
201
|
+
kind: "blockStatement",
|
|
202
|
+
statements: [
|
|
203
|
+
{
|
|
204
|
+
kind: "returnStatement",
|
|
205
|
+
expression: { kind: "identifier", name: "user" },
|
|
206
|
+
},
|
|
207
|
+
],
|
|
208
|
+
},
|
|
209
|
+
isAsync: false,
|
|
210
|
+
isGenerator: false,
|
|
211
|
+
isExported: true,
|
|
212
|
+
},
|
|
213
|
+
],
|
|
214
|
+
};
|
|
215
|
+
|
|
216
|
+
const code = emitModule(module);
|
|
217
|
+
|
|
218
|
+
// Should use Union<User, Product>
|
|
219
|
+
expect(code).to.include("Union<User, Product> getResult()");
|
|
220
|
+
});
|
|
221
|
+
|
|
222
|
+
it("should emit three-type union as Union<T1, T2, T3>", () => {
|
|
223
|
+
const module: IrModule = {
|
|
224
|
+
kind: "module",
|
|
225
|
+
filePath: "/test/union3.ts",
|
|
226
|
+
namespace: "Test",
|
|
227
|
+
className: "union3",
|
|
228
|
+
isStaticContainer: true,
|
|
229
|
+
imports: [],
|
|
230
|
+
exports: [],
|
|
231
|
+
body: [
|
|
232
|
+
{
|
|
233
|
+
kind: "variableDeclaration",
|
|
234
|
+
declarationKind: "const",
|
|
235
|
+
isExported: false,
|
|
236
|
+
declarations: [
|
|
237
|
+
{
|
|
238
|
+
kind: "variableDeclarator",
|
|
239
|
+
name: { kind: "identifierPattern", name: "value" },
|
|
240
|
+
type: {
|
|
241
|
+
kind: "unionType",
|
|
242
|
+
types: [
|
|
243
|
+
{ kind: "primitiveType", name: "string" },
|
|
244
|
+
{ kind: "primitiveType", name: "number" },
|
|
245
|
+
{ kind: "primitiveType", name: "boolean" },
|
|
246
|
+
],
|
|
247
|
+
},
|
|
248
|
+
initializer: { kind: "literal", value: "hello" },
|
|
249
|
+
},
|
|
250
|
+
],
|
|
251
|
+
},
|
|
252
|
+
],
|
|
253
|
+
};
|
|
254
|
+
|
|
255
|
+
const code = emitModule(module);
|
|
256
|
+
|
|
257
|
+
// Should use Union<T1, T2, T3>
|
|
258
|
+
expect(code).to.include("Union<string, double, bool> value");
|
|
259
|
+
expect(code).to.include("using Tsonic.Runtime");
|
|
260
|
+
});
|
|
261
|
+
|
|
262
|
+
it("should emit four-type union as Union<T1, T2, T3, T4>", () => {
|
|
263
|
+
const module: IrModule = {
|
|
264
|
+
kind: "module",
|
|
265
|
+
filePath: "/test/union4.ts",
|
|
266
|
+
namespace: "Test",
|
|
267
|
+
className: "union4",
|
|
268
|
+
isStaticContainer: true,
|
|
269
|
+
imports: [],
|
|
270
|
+
exports: [],
|
|
271
|
+
body: [
|
|
272
|
+
{
|
|
273
|
+
kind: "functionDeclaration",
|
|
274
|
+
name: "process",
|
|
275
|
+
parameters: [],
|
|
276
|
+
returnType: {
|
|
277
|
+
kind: "unionType",
|
|
278
|
+
types: [
|
|
279
|
+
{ kind: "primitiveType", name: "string" },
|
|
280
|
+
{ kind: "primitiveType", name: "number" },
|
|
281
|
+
{ kind: "primitiveType", name: "boolean" },
|
|
282
|
+
{ kind: "referenceType", name: "Date", typeArguments: [] },
|
|
283
|
+
],
|
|
284
|
+
},
|
|
285
|
+
body: {
|
|
286
|
+
kind: "blockStatement",
|
|
287
|
+
statements: [
|
|
288
|
+
{
|
|
289
|
+
kind: "returnStatement",
|
|
290
|
+
expression: { kind: "literal", value: "hello" },
|
|
291
|
+
},
|
|
292
|
+
],
|
|
293
|
+
},
|
|
294
|
+
isAsync: false,
|
|
295
|
+
isGenerator: false,
|
|
296
|
+
isExported: true,
|
|
297
|
+
},
|
|
298
|
+
],
|
|
299
|
+
};
|
|
300
|
+
|
|
301
|
+
const code = emitModule(module);
|
|
302
|
+
|
|
303
|
+
// Should use Union<T1, T2, T3, T4>
|
|
304
|
+
expect(code).to.include("Union<string, double, bool, Date> process()");
|
|
305
|
+
});
|
|
306
|
+
|
|
307
|
+
it("should emit eight-type union as Union<T1, T2, T3, T4, T5, T6, T7, T8>", () => {
|
|
308
|
+
const module: IrModule = {
|
|
309
|
+
kind: "module",
|
|
310
|
+
filePath: "/test/union8.ts",
|
|
311
|
+
namespace: "Test",
|
|
312
|
+
className: "union8",
|
|
313
|
+
isStaticContainer: true,
|
|
314
|
+
imports: [],
|
|
315
|
+
exports: [],
|
|
316
|
+
body: [
|
|
317
|
+
{
|
|
318
|
+
kind: "variableDeclaration",
|
|
319
|
+
declarationKind: "const",
|
|
320
|
+
isExported: false,
|
|
321
|
+
declarations: [
|
|
322
|
+
{
|
|
323
|
+
kind: "variableDeclarator",
|
|
324
|
+
name: { kind: "identifierPattern", name: "value" },
|
|
325
|
+
type: {
|
|
326
|
+
kind: "unionType",
|
|
327
|
+
types: [
|
|
328
|
+
{ kind: "primitiveType", name: "string" },
|
|
329
|
+
{ kind: "primitiveType", name: "number" },
|
|
330
|
+
{ kind: "primitiveType", name: "boolean" },
|
|
331
|
+
{ kind: "referenceType", name: "User", typeArguments: [] },
|
|
332
|
+
{ kind: "referenceType", name: "Product", typeArguments: [] },
|
|
333
|
+
{ kind: "referenceType", name: "Order", typeArguments: [] },
|
|
334
|
+
{ kind: "referenceType", name: "Payment", typeArguments: [] },
|
|
335
|
+
{ kind: "referenceType", name: "Invoice", typeArguments: [] },
|
|
336
|
+
],
|
|
337
|
+
},
|
|
338
|
+
initializer: { kind: "literal", value: "hello" },
|
|
339
|
+
},
|
|
340
|
+
],
|
|
341
|
+
},
|
|
342
|
+
],
|
|
343
|
+
};
|
|
344
|
+
|
|
345
|
+
const code = emitModule(module);
|
|
346
|
+
|
|
347
|
+
// Should use Union<T1, T2, T3, T4, T5, T6, T7, T8>
|
|
348
|
+
expect(code).to.include(
|
|
349
|
+
"Union<string, double, bool, User, Product, Order, Payment, Invoice> value"
|
|
350
|
+
);
|
|
351
|
+
});
|
|
352
|
+
|
|
353
|
+
it("should fall back to object for unions with more than 8 types", () => {
|
|
354
|
+
const module: IrModule = {
|
|
355
|
+
kind: "module",
|
|
356
|
+
filePath: "/test/union9.ts",
|
|
357
|
+
namespace: "Test",
|
|
358
|
+
className: "union9",
|
|
359
|
+
isStaticContainer: true,
|
|
360
|
+
imports: [],
|
|
361
|
+
exports: [],
|
|
362
|
+
body: [
|
|
363
|
+
{
|
|
364
|
+
kind: "variableDeclaration",
|
|
365
|
+
declarationKind: "const",
|
|
366
|
+
isExported: false,
|
|
367
|
+
declarations: [
|
|
368
|
+
{
|
|
369
|
+
kind: "variableDeclarator",
|
|
370
|
+
name: { kind: "identifierPattern", name: "value" },
|
|
371
|
+
type: {
|
|
372
|
+
kind: "unionType",
|
|
373
|
+
types: [
|
|
374
|
+
{ kind: "primitiveType", name: "string" },
|
|
375
|
+
{ kind: "primitiveType", name: "number" },
|
|
376
|
+
{ kind: "primitiveType", name: "boolean" },
|
|
377
|
+
{ kind: "referenceType", name: "T1", typeArguments: [] },
|
|
378
|
+
{ kind: "referenceType", name: "T2", typeArguments: [] },
|
|
379
|
+
{ kind: "referenceType", name: "T3", typeArguments: [] },
|
|
380
|
+
{ kind: "referenceType", name: "T4", typeArguments: [] },
|
|
381
|
+
{ kind: "referenceType", name: "T5", typeArguments: [] },
|
|
382
|
+
{ kind: "referenceType", name: "T6", typeArguments: [] },
|
|
383
|
+
],
|
|
384
|
+
},
|
|
385
|
+
initializer: { kind: "literal", value: "hello" },
|
|
386
|
+
},
|
|
387
|
+
],
|
|
388
|
+
},
|
|
389
|
+
],
|
|
390
|
+
};
|
|
391
|
+
|
|
392
|
+
const code = emitModule(module);
|
|
393
|
+
|
|
394
|
+
// Should fall back to object for 9+ types
|
|
395
|
+
expect(code).to.include("object value");
|
|
396
|
+
});
|
|
397
|
+
});
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Union type emission
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { IrType } from "@tsonic/frontend";
|
|
6
|
+
import { EmitterContext } from "../types.js";
|
|
7
|
+
import { emitType } from "./emitter.js";
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Emit union types as nullable (T?), Union<T1, T2>, or object
|
|
11
|
+
*/
|
|
12
|
+
export const emitUnionType = (
|
|
13
|
+
type: Extract<IrType, { kind: "unionType" }>,
|
|
14
|
+
context: EmitterContext
|
|
15
|
+
): [string, EmitterContext] => {
|
|
16
|
+
// C# doesn't have native union types
|
|
17
|
+
// Strategy:
|
|
18
|
+
// 1. Nullable types (T | null | undefined) → T?
|
|
19
|
+
// 2. Two-type unions → Union<T1, T2>
|
|
20
|
+
// 3. Multi-type unions → object (fallback)
|
|
21
|
+
|
|
22
|
+
// Check if it's a nullable type (T | null | undefined)
|
|
23
|
+
const nonNullTypes = type.types.filter(
|
|
24
|
+
(t) =>
|
|
25
|
+
!(
|
|
26
|
+
t.kind === "primitiveType" &&
|
|
27
|
+
(t.name === "null" || t.name === "undefined")
|
|
28
|
+
)
|
|
29
|
+
);
|
|
30
|
+
|
|
31
|
+
if (nonNullTypes.length === 1) {
|
|
32
|
+
// This is a nullable type (T | null | undefined)
|
|
33
|
+
const firstType = nonNullTypes[0];
|
|
34
|
+
if (!firstType) {
|
|
35
|
+
return ["object?", context];
|
|
36
|
+
}
|
|
37
|
+
const [baseType, newContext] = emitType(firstType, context);
|
|
38
|
+
// Add ? suffix for nullable types (both value types and reference types)
|
|
39
|
+
// This includes string?, int?, double?, etc. per spec/04-type-mappings.md
|
|
40
|
+
return [`${baseType}?`, newContext];
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
// Multi-type unions (2-8 types) → Union<T1, T2, ...>
|
|
44
|
+
if (type.types.length >= 2 && type.types.length <= 8) {
|
|
45
|
+
const typeStrings: string[] = [];
|
|
46
|
+
let currentContext = context;
|
|
47
|
+
|
|
48
|
+
for (const t of type.types) {
|
|
49
|
+
const [typeStr, newContext] = emitType(t, currentContext);
|
|
50
|
+
typeStrings.push(typeStr);
|
|
51
|
+
currentContext = newContext;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
return [
|
|
55
|
+
`global::Tsonic.Runtime.Union<${typeStrings.join(", ")}>`,
|
|
56
|
+
currentContext,
|
|
57
|
+
];
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
// Fallback for unions with more than 8 types: use object
|
|
61
|
+
return ["object", context];
|
|
62
|
+
};
|
package/src/types.ts
ADDED
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* C# Emitter Types
|
|
3
|
+
* Main dispatcher - re-exports from emitter-types/ subdirectory
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
export type {
|
|
7
|
+
EmitterOptions,
|
|
8
|
+
EmitterContext,
|
|
9
|
+
EmitResult,
|
|
10
|
+
CSharpFragment,
|
|
11
|
+
CSharpAccessModifier,
|
|
12
|
+
CSharpClassModifier,
|
|
13
|
+
CSharpMethodModifier,
|
|
14
|
+
CSharpUsing,
|
|
15
|
+
ImportBinding,
|
|
16
|
+
ModuleIdentity,
|
|
17
|
+
ModuleMap,
|
|
18
|
+
ExportSource,
|
|
19
|
+
ExportMap,
|
|
20
|
+
} from "./emitter-types/index.js";
|
|
21
|
+
export {
|
|
22
|
+
createContext,
|
|
23
|
+
indent,
|
|
24
|
+
dedent,
|
|
25
|
+
withStatic,
|
|
26
|
+
withAsync,
|
|
27
|
+
withClassName,
|
|
28
|
+
getIndent,
|
|
29
|
+
renderTypeFQN,
|
|
30
|
+
renderMemberFQN,
|
|
31
|
+
renderFQN,
|
|
32
|
+
FQN,
|
|
33
|
+
} from "./emitter-types/index.js";
|
|
@@ -0,0 +1,213 @@
|
|
|
1
|
+
# Golden Test Suite
|
|
2
|
+
|
|
3
|
+
This directory contains **golden tests** for the Tsonic emitter - tests that verify the exact C# output generated from TypeScript input.
|
|
4
|
+
|
|
5
|
+
## Structure
|
|
6
|
+
|
|
7
|
+
Each test case is a directory containing three files:
|
|
8
|
+
|
|
9
|
+
```
|
|
10
|
+
testcases/
|
|
11
|
+
category/ # e.g., arrays, async, functions
|
|
12
|
+
subcategory/ # e.g., basic, advanced
|
|
13
|
+
title.txt # One-line test description
|
|
14
|
+
FileName.ts # TypeScript input (name becomes C# class name)
|
|
15
|
+
expected.cs # Expected C# output
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
**Important**: The `.ts` filename determines the generated C# class name. For example:
|
|
19
|
+
|
|
20
|
+
- `Basic.ts` → `class Basic`
|
|
21
|
+
- `UserService.ts` → `class UserService`
|
|
22
|
+
|
|
23
|
+
## How It Works
|
|
24
|
+
|
|
25
|
+
1. **Auto-discovery**: The test harness (`golden.test.ts`) automatically discovers all test cases
|
|
26
|
+
2. **Nested describes**: Test cases are organized into nested `describe` blocks matching the directory structure
|
|
27
|
+
3. **Full pipeline**: Each test runs the complete TypeScript → IR → C# pipeline
|
|
28
|
+
4. **Exact comparison**: Generated C# is compared character-by-character with expected output
|
|
29
|
+
|
|
30
|
+
## Adding a New Test
|
|
31
|
+
|
|
32
|
+
1. **Create a directory** under the appropriate category:
|
|
33
|
+
|
|
34
|
+
```bash
|
|
35
|
+
mkdir -p testcases/arrays/destructuring
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
2. **Add three files**:
|
|
39
|
+
|
|
40
|
+
**`title.txt`** - Single line description:
|
|
41
|
+
|
|
42
|
+
```
|
|
43
|
+
should emit array destructuring assignment
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
**`ArrayDestructure.ts`** - TypeScript input:
|
|
47
|
+
|
|
48
|
+
```typescript
|
|
49
|
+
export function destructure(arr: number[]): number {
|
|
50
|
+
const [first, second] = arr;
|
|
51
|
+
return first + second;
|
|
52
|
+
}
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
**`expected.cs`** - Expected C# output (without header):
|
|
56
|
+
|
|
57
|
+
```csharp
|
|
58
|
+
using Tsonic.Runtime;
|
|
59
|
+
|
|
60
|
+
namespace TestCases.Arrays
|
|
61
|
+
{
|
|
62
|
+
public static class ArrayDestructure
|
|
63
|
+
{
|
|
64
|
+
public static double destructure(Tsonic.Runtime.Array<double> arr)
|
|
65
|
+
{
|
|
66
|
+
var first = arr[0];
|
|
67
|
+
var second = arr[1];
|
|
68
|
+
return first + second;
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
**Note**: Do NOT include the file header (`// Generated from:...`) in expected files. The test harness automatically generates and prepends the header using a shared constant from `constants.ts`. This ensures tests don't break when the header format changes.
|
|
75
|
+
|
|
76
|
+
3. **Run tests**:
|
|
77
|
+
```bash
|
|
78
|
+
npm run test:emitter
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
## Tips
|
|
82
|
+
|
|
83
|
+
### Getting the Expected Output
|
|
84
|
+
|
|
85
|
+
The easiest way to create `expected.cs` is to:
|
|
86
|
+
|
|
87
|
+
1. Create the `.ts` file with your input
|
|
88
|
+
2. Create a temporary `expected.cs` with placeholder content
|
|
89
|
+
3. Run the tests - they will fail and show you the actual output
|
|
90
|
+
4. Copy the actual output to `expected.cs`
|
|
91
|
+
5. **Remove the header** (the first 4 lines starting with `// Generated from:`)
|
|
92
|
+
|
|
93
|
+
Or use the CLI to generate it:
|
|
94
|
+
|
|
95
|
+
```bash
|
|
96
|
+
cd packages/emitter/testcases/arrays/destructuring
|
|
97
|
+
tsonic emit ArrayDestructure.ts --out-dir temp
|
|
98
|
+
# Remove header lines before saving
|
|
99
|
+
tail -n +5 temp/ArrayDestructure.cs > expected.cs
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
### File Path Normalization
|
|
103
|
+
|
|
104
|
+
The harness normalizes:
|
|
105
|
+
|
|
106
|
+
- Line endings (`\r\n` → `\n`)
|
|
107
|
+
- Trailing whitespace
|
|
108
|
+
- Timestamps (`Generated at: ...` → `TIMESTAMP`)
|
|
109
|
+
|
|
110
|
+
This makes tests resilient to minor formatting changes.
|
|
111
|
+
|
|
112
|
+
### Namespace Calculation
|
|
113
|
+
|
|
114
|
+
The namespace is derived from the directory path:
|
|
115
|
+
|
|
116
|
+
```
|
|
117
|
+
testcases/arrays/basic/Basic.ts
|
|
118
|
+
└─ TestCases.Arrays (namespace)
|
|
119
|
+
└─ Basic (class name from filename)
|
|
120
|
+
|
|
121
|
+
testcases/async/advanced/PromiseHelper.ts
|
|
122
|
+
└─ TestCases.Async.Advanced
|
|
123
|
+
└─ PromiseHelper
|
|
124
|
+
```
|
|
125
|
+
|
|
126
|
+
## Test Organization
|
|
127
|
+
|
|
128
|
+
Organize tests by feature category:
|
|
129
|
+
|
|
130
|
+
- **`arrays/`** - Array operations, destructuring, spread
|
|
131
|
+
- **`async/`** - Async/await, promises, generators
|
|
132
|
+
- **`functions/`** - Function declarations, arrow functions, closures
|
|
133
|
+
- **`classes/`** - Class declarations, methods, inheritance
|
|
134
|
+
- **`types/`** - Type mappings, unions, generics
|
|
135
|
+
- **`interop/`** - .NET interop, imports
|
|
136
|
+
- **`edge-cases/`** - Corner cases, error conditions
|
|
137
|
+
|
|
138
|
+
Within each category, use subdirectories for complexity:
|
|
139
|
+
|
|
140
|
+
- `basic/` - Simple, fundamental cases
|
|
141
|
+
- `advanced/` - Complex scenarios
|
|
142
|
+
- `edge-cases/` - Unusual inputs
|
|
143
|
+
|
|
144
|
+
## Running Tests
|
|
145
|
+
|
|
146
|
+
```bash
|
|
147
|
+
# Run all emitter tests (including golden tests)
|
|
148
|
+
npm run test:emitter
|
|
149
|
+
|
|
150
|
+
# Run only golden tests (via mocha grep)
|
|
151
|
+
npm run test:emitter -- --grep "Golden Tests"
|
|
152
|
+
|
|
153
|
+
# Run specific category
|
|
154
|
+
npm run test:emitter -- --grep "Golden Tests arrays"
|
|
155
|
+
```
|
|
156
|
+
|
|
157
|
+
## Troubleshooting
|
|
158
|
+
|
|
159
|
+
### Test fails with "No .ts input file found"
|
|
160
|
+
|
|
161
|
+
Make sure your test directory contains exactly one `.ts` file (other than `title.txt`).
|
|
162
|
+
|
|
163
|
+
### Test fails with "Incomplete test case"
|
|
164
|
+
|
|
165
|
+
You're missing one of the required files:
|
|
166
|
+
|
|
167
|
+
- `title.txt`
|
|
168
|
+
- `*.ts`
|
|
169
|
+
- `expected.cs`
|
|
170
|
+
|
|
171
|
+
### Test fails with diff showing wrong class name
|
|
172
|
+
|
|
173
|
+
The C# class name comes from the `.ts` filename, not the directory name.
|
|
174
|
+
|
|
175
|
+
Example:
|
|
176
|
+
|
|
177
|
+
- ❌ `input.ts` → `class input`
|
|
178
|
+
- ✅ `ArrayHelper.ts` → `class ArrayHelper`
|
|
179
|
+
|
|
180
|
+
### Test fails with diff showing wrong namespace
|
|
181
|
+
|
|
182
|
+
Check the directory structure. Namespace is built from path components:
|
|
183
|
+
|
|
184
|
+
```
|
|
185
|
+
testcases/foo/bar/Test.ts → TestCases.Foo (not TestCases.Foo.Bar)
|
|
186
|
+
```
|
|
187
|
+
|
|
188
|
+
The last directory component becomes the class name prefix, not part of the namespace.
|
|
189
|
+
|
|
190
|
+
## Benefits
|
|
191
|
+
|
|
192
|
+
✅ **Easy to add** - Just create 3 files
|
|
193
|
+
✅ **Exact verification** - Catches any output changes
|
|
194
|
+
✅ **Auto-discovered** - No manual test registration
|
|
195
|
+
✅ **Well-organized** - Nested describes match directory structure
|
|
196
|
+
✅ **Fast feedback** - Clear diffs when tests fail
|
|
197
|
+
✅ **Comprehensive** - Tests full TS → IR → C# pipeline
|
|
198
|
+
|
|
199
|
+
## Contribution Guidelines
|
|
200
|
+
|
|
201
|
+
When adding golden tests:
|
|
202
|
+
|
|
203
|
+
1. **Be specific** - One test per feature/behavior
|
|
204
|
+
2. **Use descriptive names** - File and title should be clear
|
|
205
|
+
3. **Keep it simple** - Focus on one aspect per test
|
|
206
|
+
4. **Add comments** - Explain non-obvious behavior in the `.ts` file
|
|
207
|
+
5. **Verify output** - Ensure expected.cs is actually correct
|
|
208
|
+
|
|
209
|
+
## See Also
|
|
210
|
+
|
|
211
|
+
- [Emitter Implementation](../src/emitter.ts)
|
|
212
|
+
- [Golden Test Harness](../src/golden.test.ts)
|
|
213
|
+
- [Tsonic Spec](../../../spec/)
|