@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,75 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Static container class emission
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { IrModule, IrStatement } from "@tsonic/frontend";
|
|
6
|
+
import { EmitterContext, indent, getIndent, withStatic } from "../../types.js";
|
|
7
|
+
import { emitStatement } from "../../statement-emitter.js";
|
|
8
|
+
import { emitExport } from "../exports.js";
|
|
9
|
+
|
|
10
|
+
export type StaticContainerResult = {
|
|
11
|
+
readonly code: string;
|
|
12
|
+
readonly context: EmitterContext;
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Check if there's a namespace-level class with the same name as the module
|
|
17
|
+
*/
|
|
18
|
+
export const hasMatchingClassName = (
|
|
19
|
+
declarations: readonly IrStatement[],
|
|
20
|
+
className: string
|
|
21
|
+
): boolean => {
|
|
22
|
+
return declarations.some(
|
|
23
|
+
(decl) =>
|
|
24
|
+
(decl.kind === "classDeclaration" ||
|
|
25
|
+
decl.kind === "interfaceDeclaration") &&
|
|
26
|
+
decl.name === className
|
|
27
|
+
);
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Emit static container class for module-level members
|
|
32
|
+
*/
|
|
33
|
+
export const emitStaticContainer = (
|
|
34
|
+
module: IrModule,
|
|
35
|
+
members: readonly IrStatement[],
|
|
36
|
+
baseContext: EmitterContext,
|
|
37
|
+
hasInheritance: boolean
|
|
38
|
+
): StaticContainerResult => {
|
|
39
|
+
const classContext = withStatic(indent(baseContext), true);
|
|
40
|
+
const bodyContext = indent(classContext);
|
|
41
|
+
const ind = getIndent(classContext);
|
|
42
|
+
|
|
43
|
+
const containerParts: string[] = [];
|
|
44
|
+
containerParts.push(`${ind}public static class ${module.className}`);
|
|
45
|
+
containerParts.push(`${ind}{`);
|
|
46
|
+
|
|
47
|
+
const bodyParts: string[] = [];
|
|
48
|
+
let bodyCurrentContext = bodyContext;
|
|
49
|
+
|
|
50
|
+
for (const stmt of members) {
|
|
51
|
+
const [code, newContext] = emitStatement(stmt, bodyCurrentContext);
|
|
52
|
+
bodyParts.push(code);
|
|
53
|
+
bodyCurrentContext = newContext;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
// Handle explicit exports
|
|
57
|
+
for (const exp of module.exports) {
|
|
58
|
+
const exportCode = emitExport(exp, bodyCurrentContext);
|
|
59
|
+
if (exportCode[0]) {
|
|
60
|
+
bodyParts.push(exportCode[0]);
|
|
61
|
+
bodyCurrentContext = exportCode[1];
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
if (bodyParts.length > 0) {
|
|
66
|
+
containerParts.push(bodyParts.join("\n\n"));
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
containerParts.push(`${ind}}`);
|
|
70
|
+
|
|
71
|
+
return {
|
|
72
|
+
code: containerParts.join("\n"),
|
|
73
|
+
context: { ...bodyCurrentContext, hasInheritance },
|
|
74
|
+
};
|
|
75
|
+
};
|
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tests for Module Generation
|
|
3
|
+
* Tests emission of static containers and regular classes
|
|
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("Module Generation", () => {
|
|
12
|
+
it("should emit a static container class", () => {
|
|
13
|
+
const module: IrModule = {
|
|
14
|
+
kind: "module",
|
|
15
|
+
filePath: "/src/math.ts",
|
|
16
|
+
namespace: "MyApp",
|
|
17
|
+
className: "math",
|
|
18
|
+
isStaticContainer: true,
|
|
19
|
+
imports: [],
|
|
20
|
+
body: [
|
|
21
|
+
{
|
|
22
|
+
kind: "variableDeclaration",
|
|
23
|
+
declarationKind: "const",
|
|
24
|
+
declarations: [
|
|
25
|
+
{
|
|
26
|
+
kind: "variableDeclarator",
|
|
27
|
+
name: { kind: "identifierPattern", name: "PI" },
|
|
28
|
+
initializer: { kind: "literal", value: 3.14159 },
|
|
29
|
+
},
|
|
30
|
+
],
|
|
31
|
+
isExported: true,
|
|
32
|
+
},
|
|
33
|
+
{
|
|
34
|
+
kind: "functionDeclaration",
|
|
35
|
+
name: "add",
|
|
36
|
+
parameters: [
|
|
37
|
+
{
|
|
38
|
+
kind: "parameter",
|
|
39
|
+
pattern: { kind: "identifierPattern", name: "a" },
|
|
40
|
+
type: { kind: "primitiveType", name: "number" },
|
|
41
|
+
isOptional: false,
|
|
42
|
+
isRest: false,
|
|
43
|
+
passing: "value",
|
|
44
|
+
},
|
|
45
|
+
{
|
|
46
|
+
kind: "parameter",
|
|
47
|
+
pattern: { kind: "identifierPattern", name: "b" },
|
|
48
|
+
type: { kind: "primitiveType", name: "number" },
|
|
49
|
+
isOptional: false,
|
|
50
|
+
isRest: false,
|
|
51
|
+
passing: "value",
|
|
52
|
+
},
|
|
53
|
+
],
|
|
54
|
+
returnType: { kind: "primitiveType", name: "number" },
|
|
55
|
+
body: {
|
|
56
|
+
kind: "blockStatement",
|
|
57
|
+
statements: [
|
|
58
|
+
{
|
|
59
|
+
kind: "returnStatement",
|
|
60
|
+
expression: {
|
|
61
|
+
kind: "binary",
|
|
62
|
+
operator: "+",
|
|
63
|
+
left: { kind: "identifier", name: "a" },
|
|
64
|
+
right: { kind: "identifier", name: "b" },
|
|
65
|
+
},
|
|
66
|
+
},
|
|
67
|
+
],
|
|
68
|
+
},
|
|
69
|
+
isExported: true,
|
|
70
|
+
isAsync: false,
|
|
71
|
+
isGenerator: false,
|
|
72
|
+
},
|
|
73
|
+
],
|
|
74
|
+
exports: [],
|
|
75
|
+
};
|
|
76
|
+
|
|
77
|
+
const result = emitModule(module);
|
|
78
|
+
|
|
79
|
+
expect(result).to.include("public static class math");
|
|
80
|
+
expect(result).to.include("var PI = 3.14159");
|
|
81
|
+
expect(result).to.include("public static double add(double a, double b)");
|
|
82
|
+
expect(result).to.include("return a + b");
|
|
83
|
+
expect(result).to.include("namespace MyApp");
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
it("should emit a regular class", () => {
|
|
87
|
+
const module: IrModule = {
|
|
88
|
+
kind: "module",
|
|
89
|
+
filePath: "/src/User.ts",
|
|
90
|
+
namespace: "MyApp",
|
|
91
|
+
className: "User",
|
|
92
|
+
isStaticContainer: false,
|
|
93
|
+
imports: [],
|
|
94
|
+
body: [
|
|
95
|
+
{
|
|
96
|
+
kind: "classDeclaration",
|
|
97
|
+
name: "User",
|
|
98
|
+
members: [
|
|
99
|
+
{
|
|
100
|
+
kind: "propertyDeclaration",
|
|
101
|
+
name: "name",
|
|
102
|
+
type: { kind: "primitiveType", name: "string" },
|
|
103
|
+
accessibility: "public",
|
|
104
|
+
isStatic: false,
|
|
105
|
+
isReadonly: false,
|
|
106
|
+
},
|
|
107
|
+
{
|
|
108
|
+
kind: "methodDeclaration",
|
|
109
|
+
name: "greet",
|
|
110
|
+
parameters: [],
|
|
111
|
+
returnType: { kind: "primitiveType", name: "string" },
|
|
112
|
+
body: {
|
|
113
|
+
kind: "blockStatement",
|
|
114
|
+
statements: [
|
|
115
|
+
{
|
|
116
|
+
kind: "returnStatement",
|
|
117
|
+
expression: {
|
|
118
|
+
kind: "templateLiteral",
|
|
119
|
+
quasis: ["Hello, I'm ", ""],
|
|
120
|
+
expressions: [
|
|
121
|
+
{
|
|
122
|
+
kind: "memberAccess",
|
|
123
|
+
object: { kind: "this" },
|
|
124
|
+
property: "name",
|
|
125
|
+
isComputed: false,
|
|
126
|
+
isOptional: false,
|
|
127
|
+
},
|
|
128
|
+
],
|
|
129
|
+
},
|
|
130
|
+
},
|
|
131
|
+
],
|
|
132
|
+
},
|
|
133
|
+
accessibility: "public",
|
|
134
|
+
isStatic: false,
|
|
135
|
+
isAsync: false,
|
|
136
|
+
isGenerator: false,
|
|
137
|
+
},
|
|
138
|
+
],
|
|
139
|
+
isStruct: false,
|
|
140
|
+
isExported: true,
|
|
141
|
+
implements: [],
|
|
142
|
+
},
|
|
143
|
+
],
|
|
144
|
+
exports: [],
|
|
145
|
+
};
|
|
146
|
+
|
|
147
|
+
const result = emitModule(module);
|
|
148
|
+
|
|
149
|
+
expect(result).to.include("public class User");
|
|
150
|
+
expect(result).to.include("public string name;");
|
|
151
|
+
expect(result).to.include("public string greet()");
|
|
152
|
+
expect(result).to.include('$"Hello, I\'m {this.name}"');
|
|
153
|
+
});
|
|
154
|
+
});
|
|
@@ -0,0 +1,218 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Module map for resolving cross-file imports
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { IrModule, Diagnostic } from "@tsonic/frontend";
|
|
6
|
+
import type {
|
|
7
|
+
ModuleIdentity,
|
|
8
|
+
ModuleMap,
|
|
9
|
+
ExportSource,
|
|
10
|
+
ExportMap,
|
|
11
|
+
} from "../emitter-types/core.js";
|
|
12
|
+
|
|
13
|
+
// Re-export types for backward compatibility
|
|
14
|
+
export type { ModuleIdentity, ModuleMap, ExportSource, ExportMap };
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Normalize a file path for use as module map key
|
|
18
|
+
* - Convert backslashes to forward slashes
|
|
19
|
+
* - Remove .ts extension if present
|
|
20
|
+
* - Normalize . and .. segments
|
|
21
|
+
*/
|
|
22
|
+
export const canonicalizeFilePath = (filePath: string): string => {
|
|
23
|
+
// Normalize slashes
|
|
24
|
+
let normalized = filePath.replace(/\\/g, "/");
|
|
25
|
+
|
|
26
|
+
// Remove .ts extension
|
|
27
|
+
if (normalized.endsWith(".ts")) {
|
|
28
|
+
normalized = normalized.slice(0, -3);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
// Split into segments and resolve . and ..
|
|
32
|
+
const segments: string[] = [];
|
|
33
|
+
for (const segment of normalized.split("/")) {
|
|
34
|
+
if (segment === "" || segment === ".") {
|
|
35
|
+
continue; // Skip empty and current directory
|
|
36
|
+
} else if (segment === "..") {
|
|
37
|
+
segments.pop(); // Go up one directory
|
|
38
|
+
} else {
|
|
39
|
+
segments.push(segment);
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
return segments.join("/");
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Result of building module map
|
|
48
|
+
*/
|
|
49
|
+
export type ModuleMapResult =
|
|
50
|
+
| {
|
|
51
|
+
readonly ok: true;
|
|
52
|
+
readonly value: ModuleMap;
|
|
53
|
+
readonly exportMap: ExportMap;
|
|
54
|
+
}
|
|
55
|
+
| { readonly ok: false; readonly errors: readonly Diagnostic[] };
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Build module map from IR modules.
|
|
59
|
+
* Returns an error if any two files in the same namespace have the same
|
|
60
|
+
* normalized class name (e.g., api-client.ts and apiclient.ts both map to "apiclient").
|
|
61
|
+
*/
|
|
62
|
+
export const buildModuleMap = (
|
|
63
|
+
modules: readonly IrModule[]
|
|
64
|
+
): ModuleMapResult => {
|
|
65
|
+
const map = new Map<string, ModuleIdentity>();
|
|
66
|
+
const errors: Diagnostic[] = [];
|
|
67
|
+
|
|
68
|
+
// Group modules by namespace to detect class name collisions
|
|
69
|
+
const byNamespace = new Map<string, IrModule[]>();
|
|
70
|
+
for (const module of modules) {
|
|
71
|
+
const existing = byNamespace.get(module.namespace) ?? [];
|
|
72
|
+
byNamespace.set(module.namespace, [...existing, module]);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
// Check for collisions within each namespace
|
|
76
|
+
for (const [namespace, nsModules] of byNamespace) {
|
|
77
|
+
const byClassName = new Map<string, IrModule[]>();
|
|
78
|
+
for (const module of nsModules) {
|
|
79
|
+
const existing = byClassName.get(module.className) ?? [];
|
|
80
|
+
byClassName.set(module.className, [...existing, module]);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
// Report collisions
|
|
84
|
+
for (const [className, colliding] of byClassName) {
|
|
85
|
+
if (colliding.length > 1) {
|
|
86
|
+
const fileNames = colliding
|
|
87
|
+
.map((m) => `'${m.filePath.split("/").pop()}'`)
|
|
88
|
+
.join(" and ");
|
|
89
|
+
errors.push({
|
|
90
|
+
code: "TSN9001",
|
|
91
|
+
message: `File name collision after normalization: ${fileNames} both map to class '${className}' in namespace '${namespace}'. Rename one file.`,
|
|
92
|
+
severity: "error",
|
|
93
|
+
});
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
if (errors.length > 0) {
|
|
99
|
+
return { ok: false, errors };
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
// Build the map
|
|
103
|
+
for (const module of modules) {
|
|
104
|
+
const canonicalPath = canonicalizeFilePath(module.filePath);
|
|
105
|
+
map.set(canonicalPath, {
|
|
106
|
+
namespace: module.namespace,
|
|
107
|
+
className: module.className,
|
|
108
|
+
filePath: canonicalPath,
|
|
109
|
+
});
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
// Build the export map
|
|
113
|
+
const exportMap = buildExportMap(modules);
|
|
114
|
+
|
|
115
|
+
return { ok: true, value: map, exportMap };
|
|
116
|
+
};
|
|
117
|
+
|
|
118
|
+
/**
|
|
119
|
+
* Build export map from IR modules.
|
|
120
|
+
* Maps (modulePath, exportName) -> actual source for re-exports.
|
|
121
|
+
*/
|
|
122
|
+
const buildExportMap = (modules: readonly IrModule[]): ExportMap => {
|
|
123
|
+
const exportMap = new Map<string, ExportSource>();
|
|
124
|
+
|
|
125
|
+
// First pass: collect all re-exports
|
|
126
|
+
for (const module of modules) {
|
|
127
|
+
const modulePath = canonicalizeFilePath(module.filePath);
|
|
128
|
+
|
|
129
|
+
for (const exp of module.exports) {
|
|
130
|
+
if (exp.kind === "reexport") {
|
|
131
|
+
// Resolve the source module path
|
|
132
|
+
const sourcePath = resolveImportPath(module.filePath, exp.fromModule);
|
|
133
|
+
const key = `${modulePath}:${exp.name}`;
|
|
134
|
+
exportMap.set(key, {
|
|
135
|
+
sourceFile: sourcePath,
|
|
136
|
+
sourceName: exp.originalName,
|
|
137
|
+
});
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
// Second pass: resolve transitive re-exports
|
|
143
|
+
// Keep resolving until no changes (handles chains like A re-exports from B re-exports from C)
|
|
144
|
+
const resolveTransitive = (): boolean => {
|
|
145
|
+
let changed = false;
|
|
146
|
+
|
|
147
|
+
for (const [key, source] of exportMap) {
|
|
148
|
+
const transitiveKey = `${source.sourceFile}:${source.sourceName}`;
|
|
149
|
+
const transitiveSource = exportMap.get(transitiveKey);
|
|
150
|
+
|
|
151
|
+
if (transitiveSource) {
|
|
152
|
+
// This is a transitive re-export - update to point to the actual source
|
|
153
|
+
exportMap.set(key, transitiveSource);
|
|
154
|
+
changed = true;
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
return changed;
|
|
159
|
+
};
|
|
160
|
+
|
|
161
|
+
// Resolve transitive re-exports (max 10 iterations to prevent infinite loops)
|
|
162
|
+
for (let i = 0; i < 10 && resolveTransitive(); i++) {
|
|
163
|
+
// Keep resolving
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
return exportMap;
|
|
167
|
+
};
|
|
168
|
+
|
|
169
|
+
/**
|
|
170
|
+
* Resolve a relative import path to a canonical file path
|
|
171
|
+
*/
|
|
172
|
+
export const resolveImportPath = (
|
|
173
|
+
currentFilePath: string,
|
|
174
|
+
importSource: string
|
|
175
|
+
): string => {
|
|
176
|
+
// Normalize current file path
|
|
177
|
+
const currentCanonical = canonicalizeFilePath(currentFilePath);
|
|
178
|
+
|
|
179
|
+
// Get directory of current file
|
|
180
|
+
const lastSlash = currentCanonical.lastIndexOf("/");
|
|
181
|
+
const currentDir = lastSlash >= 0 ? currentCanonical.slice(0, lastSlash) : "";
|
|
182
|
+
|
|
183
|
+
// Normalize import source
|
|
184
|
+
let source = importSource.replace(/\\/g, "/");
|
|
185
|
+
|
|
186
|
+
// Remove .ts extension if present
|
|
187
|
+
if (source.endsWith(".ts")) {
|
|
188
|
+
source = source.slice(0, -3);
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
// Resolve relative path
|
|
192
|
+
let resolvedPath: string;
|
|
193
|
+
if (source.startsWith("./")) {
|
|
194
|
+
// Same directory or subdirectory
|
|
195
|
+
resolvedPath = currentDir
|
|
196
|
+
? `${currentDir}/${source.slice(2)}`
|
|
197
|
+
: source.slice(2);
|
|
198
|
+
} else if (source.startsWith("../")) {
|
|
199
|
+
// Parent directory
|
|
200
|
+
const parts = currentDir.split("/");
|
|
201
|
+
let remaining = source;
|
|
202
|
+
while (remaining.startsWith("../")) {
|
|
203
|
+
parts.pop();
|
|
204
|
+
remaining = remaining.slice(3);
|
|
205
|
+
}
|
|
206
|
+
resolvedPath =
|
|
207
|
+
parts.length > 0 ? `${parts.join("/")}/${remaining}` : remaining;
|
|
208
|
+
} else if (source.startsWith("/")) {
|
|
209
|
+
// Absolute path (remove leading slash)
|
|
210
|
+
resolvedPath = source.slice(1);
|
|
211
|
+
} else {
|
|
212
|
+
// No ./ or ../, treat as same directory
|
|
213
|
+
resolvedPath = currentDir ? `${currentDir}/${source}` : source;
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
// Canonicalize the result
|
|
217
|
+
return canonicalizeFilePath(resolvedPath);
|
|
218
|
+
};
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Emitter options and defaults
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { EmitterOptions } from "../types.js";
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Default emitter options
|
|
9
|
+
*/
|
|
10
|
+
export const defaultOptions: EmitterOptions = {
|
|
11
|
+
rootNamespace: "MyApp",
|
|
12
|
+
includeSourceMaps: false,
|
|
13
|
+
indent: 4,
|
|
14
|
+
maxLineLength: 120,
|
|
15
|
+
includeTimestamp: true,
|
|
16
|
+
};
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Type parameter collection
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { IrModule, IrTypeParameter } from "@tsonic/frontend";
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Collect all type parameters from declarations in a module
|
|
9
|
+
*/
|
|
10
|
+
export const collectTypeParameters = (
|
|
11
|
+
module: IrModule
|
|
12
|
+
): readonly IrTypeParameter[] => {
|
|
13
|
+
const typeParams: IrTypeParameter[] = [];
|
|
14
|
+
|
|
15
|
+
for (const stmt of module.body) {
|
|
16
|
+
if (stmt.kind === "functionDeclaration" && stmt.typeParameters) {
|
|
17
|
+
typeParams.push(...stmt.typeParameters);
|
|
18
|
+
} else if (stmt.kind === "classDeclaration" && stmt.typeParameters) {
|
|
19
|
+
typeParams.push(...stmt.typeParameters);
|
|
20
|
+
// Also collect from class members
|
|
21
|
+
for (const member of stmt.members) {
|
|
22
|
+
if (member.kind === "methodDeclaration" && member.typeParameters) {
|
|
23
|
+
typeParams.push(...member.typeParameters);
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
} else if (stmt.kind === "interfaceDeclaration" && stmt.typeParameters) {
|
|
27
|
+
typeParams.push(...stmt.typeParameters);
|
|
28
|
+
} else if (stmt.kind === "typeAliasDeclaration" && stmt.typeParameters) {
|
|
29
|
+
typeParams.push(...stmt.typeParameters);
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
return typeParams;
|
|
34
|
+
};
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Context creation and manipulation functions
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { EmitterOptions, EmitterContext } from "./core.js";
|
|
6
|
+
import {
|
|
7
|
+
loadLibraries,
|
|
8
|
+
buildBindingsRegistry,
|
|
9
|
+
} from "@tsonic/frontend/metadata/index.js";
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Create a new emitter context with default values
|
|
13
|
+
*/
|
|
14
|
+
export const createContext = (options: EmitterOptions): EmitterContext => {
|
|
15
|
+
// Load metadata and bindings from library directories
|
|
16
|
+
let metadata: EmitterContext["metadata"] = undefined;
|
|
17
|
+
let bindingsRegistry: EmitterContext["bindingsRegistry"] = undefined;
|
|
18
|
+
|
|
19
|
+
if (options.libraries && options.libraries.length > 0) {
|
|
20
|
+
const librariesResult = loadLibraries(options.libraries);
|
|
21
|
+
if (librariesResult.ok) {
|
|
22
|
+
metadata = librariesResult.value.metadata;
|
|
23
|
+
bindingsRegistry = buildBindingsRegistry(librariesResult.value.bindings);
|
|
24
|
+
} else {
|
|
25
|
+
// TODO: Report diagnostics from librariesResult.error
|
|
26
|
+
// Need to integrate diagnostic reporting infrastructure
|
|
27
|
+
console.warn(
|
|
28
|
+
"[Tsonic] Failed to load libraries:",
|
|
29
|
+
librariesResult.error.map((d) => d.message).join(", ")
|
|
30
|
+
);
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
return {
|
|
35
|
+
indentLevel: 0,
|
|
36
|
+
options,
|
|
37
|
+
isStatic: false,
|
|
38
|
+
isAsync: false,
|
|
39
|
+
metadata,
|
|
40
|
+
bindingsRegistry,
|
|
41
|
+
};
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Increase indentation level
|
|
46
|
+
*/
|
|
47
|
+
export const indent = (context: EmitterContext): EmitterContext => ({
|
|
48
|
+
...context,
|
|
49
|
+
indentLevel: context.indentLevel + 1,
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Decrease indentation level
|
|
54
|
+
*/
|
|
55
|
+
export const dedent = (context: EmitterContext): EmitterContext => ({
|
|
56
|
+
...context,
|
|
57
|
+
indentLevel: Math.max(0, context.indentLevel - 1),
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Set static context flag
|
|
62
|
+
*/
|
|
63
|
+
export const withStatic = (
|
|
64
|
+
context: EmitterContext,
|
|
65
|
+
isStatic: boolean
|
|
66
|
+
): EmitterContext => ({
|
|
67
|
+
...context,
|
|
68
|
+
isStatic,
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Set async context flag
|
|
73
|
+
*/
|
|
74
|
+
export const withAsync = (
|
|
75
|
+
context: EmitterContext,
|
|
76
|
+
isAsync: boolean
|
|
77
|
+
): EmitterContext => ({
|
|
78
|
+
...context,
|
|
79
|
+
isAsync,
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* Set current class name in context
|
|
84
|
+
*/
|
|
85
|
+
export const withClassName = (
|
|
86
|
+
context: EmitterContext,
|
|
87
|
+
className: string
|
|
88
|
+
): EmitterContext => ({
|
|
89
|
+
...context,
|
|
90
|
+
className,
|
|
91
|
+
});
|