@tsonic/frontend 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 +53 -0
- package/src/dependency-graph.ts +18 -0
- package/src/dotnet-metadata.ts +121 -0
- package/src/graph/builder.ts +81 -0
- package/src/graph/circular.ts +58 -0
- package/src/graph/extraction/exports.ts +55 -0
- package/src/graph/extraction/imports.ts +81 -0
- package/src/graph/extraction/index.ts +7 -0
- package/src/graph/extraction/orchestrator.ts +99 -0
- package/src/graph/extraction.ts +10 -0
- package/src/graph/helpers.ts +51 -0
- package/src/graph/index.ts +17 -0
- package/src/graph/types.ts +13 -0
- package/src/index.ts +80 -0
- package/src/ir/binding-resolution.test.ts +585 -0
- package/src/ir/builder/exports.ts +78 -0
- package/src/ir/builder/helpers.ts +27 -0
- package/src/ir/builder/imports.ts +153 -0
- package/src/ir/builder/index.ts +10 -0
- package/src/ir/builder/orchestrator.ts +178 -0
- package/src/ir/builder/statements.ts +55 -0
- package/src/ir/builder/types.ts +8 -0
- package/src/ir/builder/validation.ts +129 -0
- package/src/ir/builder.test.ts +581 -0
- package/src/ir/builder.ts +14 -0
- package/src/ir/converters/expressions/access.ts +99 -0
- package/src/ir/converters/expressions/calls.ts +137 -0
- package/src/ir/converters/expressions/collections.ts +84 -0
- package/src/ir/converters/expressions/functions.ts +62 -0
- package/src/ir/converters/expressions/helpers.ts +264 -0
- package/src/ir/converters/expressions/index.ts +43 -0
- package/src/ir/converters/expressions/literals.ts +22 -0
- package/src/ir/converters/expressions/operators.ts +147 -0
- package/src/ir/converters/expressions/other.ts +60 -0
- package/src/ir/converters/statements/control/blocks.ts +22 -0
- package/src/ir/converters/statements/control/conditionals.ts +67 -0
- package/src/ir/converters/statements/control/exceptions.ts +43 -0
- package/src/ir/converters/statements/control/index.ts +17 -0
- package/src/ir/converters/statements/control/loops.ts +99 -0
- package/src/ir/converters/statements/control.ts +17 -0
- package/src/ir/converters/statements/declarations/classes/constructors.ts +120 -0
- package/src/ir/converters/statements/declarations/classes/index.ts +12 -0
- package/src/ir/converters/statements/declarations/classes/methods.ts +61 -0
- package/src/ir/converters/statements/declarations/classes/orchestrator.ts +166 -0
- package/src/ir/converters/statements/declarations/classes/override-detection.ts +116 -0
- package/src/ir/converters/statements/declarations/classes/properties.ts +63 -0
- package/src/ir/converters/statements/declarations/classes.ts +6 -0
- package/src/ir/converters/statements/declarations/enums.ts +29 -0
- package/src/ir/converters/statements/declarations/functions.ts +39 -0
- package/src/ir/converters/statements/declarations/index.ts +14 -0
- package/src/ir/converters/statements/declarations/interfaces.ts +131 -0
- package/src/ir/converters/statements/declarations/registry.ts +45 -0
- package/src/ir/converters/statements/declarations/type-aliases.ts +25 -0
- package/src/ir/converters/statements/declarations/variables.ts +60 -0
- package/src/ir/converters/statements/declarations.ts +16 -0
- package/src/ir/converters/statements/helpers.ts +174 -0
- package/src/ir/converters/statements/index.ts +40 -0
- package/src/ir/expression-converter.ts +207 -0
- package/src/ir/generic-validator.ts +100 -0
- package/src/ir/hierarchical-bindings-e2e.test.ts +163 -0
- package/src/ir/index.ts +6 -0
- package/src/ir/statement-converter.ts +128 -0
- package/src/ir/type-converter/arrays.ts +20 -0
- package/src/ir/type-converter/converter.ts +10 -0
- package/src/ir/type-converter/functions.ts +22 -0
- package/src/ir/type-converter/index.ts +11 -0
- package/src/ir/type-converter/inference.ts +122 -0
- package/src/ir/type-converter/literals.ts +40 -0
- package/src/ir/type-converter/objects.ts +107 -0
- package/src/ir/type-converter/orchestrator.ts +85 -0
- package/src/ir/type-converter/patterns.ts +73 -0
- package/src/ir/type-converter/primitives.ts +57 -0
- package/src/ir/type-converter/references.ts +64 -0
- package/src/ir/type-converter/unions-intersections.ts +34 -0
- package/src/ir/type-converter.ts +13 -0
- package/src/ir/types/expressions.ts +215 -0
- package/src/ir/types/guards.ts +39 -0
- package/src/ir/types/helpers.ts +135 -0
- package/src/ir/types/index.ts +108 -0
- package/src/ir/types/ir-types.ts +96 -0
- package/src/ir/types/module.ts +57 -0
- package/src/ir/types/statements.ts +238 -0
- package/src/ir/types.ts +97 -0
- package/src/metadata/bindings-loader.test.ts +144 -0
- package/src/metadata/bindings-loader.ts +357 -0
- package/src/metadata/index.ts +15 -0
- package/src/metadata/library-loader.ts +153 -0
- package/src/metadata/loader.test.ts +156 -0
- package/src/metadata/loader.ts +382 -0
- package/src/program/bindings.test.ts +512 -0
- package/src/program/bindings.ts +253 -0
- package/src/program/config.ts +30 -0
- package/src/program/creation.ts +249 -0
- package/src/program/dependency-graph.ts +245 -0
- package/src/program/diagnostics.ts +103 -0
- package/src/program/index.ts +19 -0
- package/src/program/metadata.ts +68 -0
- package/src/program/queries.ts +18 -0
- package/src/program/types.ts +38 -0
- package/src/program.ts +13 -0
- package/src/resolver/dotnet-import-resolver.ts +226 -0
- package/src/resolver/import-resolution.ts +177 -0
- package/src/resolver/index.ts +18 -0
- package/src/resolver/namespace.test.ts +86 -0
- package/src/resolver/namespace.ts +42 -0
- package/src/resolver/naming.ts +38 -0
- package/src/resolver/path-resolution.ts +22 -0
- package/src/resolver/types.ts +15 -0
- package/src/resolver.test.ts +155 -0
- package/src/resolver.ts +14 -0
- package/src/symbol-table/builder.ts +114 -0
- package/src/symbol-table/creation.ts +42 -0
- package/src/symbol-table/helpers.ts +18 -0
- package/src/symbol-table/index.ts +13 -0
- package/src/symbol-table/queries.ts +42 -0
- package/src/symbol-table/types.ts +28 -0
- package/src/symbol-table.ts +14 -0
- package/src/types/bindings.ts +172 -0
- package/src/types/diagnostic.test.ts +164 -0
- package/src/types/diagnostic.ts +153 -0
- package/src/types/explicit-views.test.ts +113 -0
- package/src/types/explicit-views.ts +218 -0
- package/src/types/metadata.ts +229 -0
- package/src/types/module.ts +99 -0
- package/src/types/nested-types.test.ts +194 -0
- package/src/types/nested-types.ts +215 -0
- package/src/types/parameter-modifiers.ts +173 -0
- package/src/types/ref-parameters.test.ts +192 -0
- package/src/types/ref-parameters.ts +268 -0
- package/src/types/result.test.ts +157 -0
- package/src/types/result.ts +48 -0
- package/src/types/support-types.test.ts +81 -0
- package/src/types/support-types.ts +288 -0
- package/src/types/test-harness.ts +180 -0
- package/src/validation/exports.ts +98 -0
- package/src/validation/features.ts +89 -0
- package/src/validation/generics.ts +40 -0
- package/src/validation/helpers.ts +31 -0
- package/src/validation/imports.ts +97 -0
- package/src/validation/index.ts +11 -0
- package/src/validation/orchestrator.ts +51 -0
- package/src/validation/static-safety.ts +267 -0
- package/src/validator.test.ts +468 -0
- package/src/validator.ts +15 -0
- package/tsconfig.json +13 -0
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Export extraction from TypeScript source
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import * as ts from "typescript";
|
|
6
|
+
import { IrExport } from "../types.js";
|
|
7
|
+
import { convertStatement } from "../statement-converter.js";
|
|
8
|
+
import { convertExpression } from "../expression-converter.js";
|
|
9
|
+
import { hasExportModifier, hasDefaultModifier } from "./helpers.js";
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Extract export declarations from source file
|
|
13
|
+
*/
|
|
14
|
+
export const extractExports = (
|
|
15
|
+
sourceFile: ts.SourceFile,
|
|
16
|
+
checker: ts.TypeChecker
|
|
17
|
+
): readonly IrExport[] => {
|
|
18
|
+
const exports: IrExport[] = [];
|
|
19
|
+
|
|
20
|
+
const visitor = (node: ts.Node): void => {
|
|
21
|
+
if (ts.isExportDeclaration(node)) {
|
|
22
|
+
if (node.exportClause && ts.isNamedExports(node.exportClause)) {
|
|
23
|
+
// Check if this is a re-export (has moduleSpecifier)
|
|
24
|
+
if (node.moduleSpecifier && ts.isStringLiteral(node.moduleSpecifier)) {
|
|
25
|
+
// Re-export: export { x } from "./other.ts"
|
|
26
|
+
const fromModule = node.moduleSpecifier.text;
|
|
27
|
+
node.exportClause.elements.forEach((spec) => {
|
|
28
|
+
exports.push({
|
|
29
|
+
kind: "reexport",
|
|
30
|
+
name: spec.name.text, // Exported name
|
|
31
|
+
originalName: (spec.propertyName ?? spec.name).text, // Name in source module
|
|
32
|
+
fromModule,
|
|
33
|
+
});
|
|
34
|
+
});
|
|
35
|
+
} else {
|
|
36
|
+
// Regular named export: export { x }
|
|
37
|
+
node.exportClause.elements.forEach((spec) => {
|
|
38
|
+
exports.push({
|
|
39
|
+
kind: "named",
|
|
40
|
+
name: spec.name.text,
|
|
41
|
+
localName: (spec.propertyName ?? spec.name).text,
|
|
42
|
+
});
|
|
43
|
+
});
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
} else if (ts.isExportAssignment(node)) {
|
|
47
|
+
exports.push({
|
|
48
|
+
kind: "default",
|
|
49
|
+
expression: convertExpression(node.expression, checker),
|
|
50
|
+
});
|
|
51
|
+
} else if (hasExportModifier(node)) {
|
|
52
|
+
const hasDefault = hasDefaultModifier(node);
|
|
53
|
+
if (hasDefault) {
|
|
54
|
+
// export default function/class/etc
|
|
55
|
+
const stmt = convertStatement(node, checker);
|
|
56
|
+
if (stmt) {
|
|
57
|
+
exports.push({
|
|
58
|
+
kind: "default",
|
|
59
|
+
expression: { kind: "identifier", name: "_default" }, // placeholder for now
|
|
60
|
+
});
|
|
61
|
+
}
|
|
62
|
+
} else {
|
|
63
|
+
// regular export
|
|
64
|
+
const stmt = convertStatement(node, checker);
|
|
65
|
+
if (stmt) {
|
|
66
|
+
exports.push({
|
|
67
|
+
kind: "declaration",
|
|
68
|
+
declaration: stmt,
|
|
69
|
+
});
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
ts.forEachChild(node, visitor);
|
|
74
|
+
};
|
|
75
|
+
|
|
76
|
+
visitor(sourceFile);
|
|
77
|
+
return exports;
|
|
78
|
+
};
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* IR Builder helper functions
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import * as ts from "typescript";
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Check if a node has export modifier
|
|
9
|
+
*/
|
|
10
|
+
export const hasExportModifier = (node: ts.Node): boolean => {
|
|
11
|
+
if (!ts.canHaveModifiers(node)) return false;
|
|
12
|
+
const modifiers = ts.getModifiers(node);
|
|
13
|
+
return (
|
|
14
|
+
modifiers?.some((m) => m.kind === ts.SyntaxKind.ExportKeyword) ?? false
|
|
15
|
+
);
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Check if a node has default modifier
|
|
20
|
+
*/
|
|
21
|
+
export const hasDefaultModifier = (node: ts.Node): boolean => {
|
|
22
|
+
if (!ts.canHaveModifiers(node)) return false;
|
|
23
|
+
const modifiers = ts.getModifiers(node);
|
|
24
|
+
return (
|
|
25
|
+
modifiers?.some((m) => m.kind === ts.SyntaxKind.DefaultKeyword) ?? false
|
|
26
|
+
);
|
|
27
|
+
};
|
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Import extraction from TypeScript source
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import * as ts from "typescript";
|
|
6
|
+
import { IrImport, IrImportSpecifier } from "../types.js";
|
|
7
|
+
import { getBindingRegistry } from "../converters/statements/declarations/registry.js";
|
|
8
|
+
import { getParameterModifierRegistry } from "../../types/parameter-modifiers.js";
|
|
9
|
+
import { DotNetImportResolver } from "../../resolver/dotnet-import-resolver.js";
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Extract import declarations from source file.
|
|
13
|
+
* Uses TypeChecker to determine if each import is a type or value.
|
|
14
|
+
* Uses DotNetImportResolver to detect CLR namespace imports.
|
|
15
|
+
*/
|
|
16
|
+
export const extractImports = (
|
|
17
|
+
sourceFile: ts.SourceFile,
|
|
18
|
+
checker: ts.TypeChecker,
|
|
19
|
+
dotnetResolver: DotNetImportResolver
|
|
20
|
+
): readonly IrImport[] => {
|
|
21
|
+
const imports: IrImport[] = [];
|
|
22
|
+
|
|
23
|
+
const visitor = (node: ts.Node): void => {
|
|
24
|
+
if (
|
|
25
|
+
ts.isImportDeclaration(node) &&
|
|
26
|
+
ts.isStringLiteral(node.moduleSpecifier)
|
|
27
|
+
) {
|
|
28
|
+
const source = node.moduleSpecifier.text;
|
|
29
|
+
const isLocal = source.startsWith(".") || source.startsWith("/");
|
|
30
|
+
|
|
31
|
+
// Use import-driven resolution to detect .NET imports
|
|
32
|
+
// This works for any package that provides bindings.json
|
|
33
|
+
const dotnetResolution = dotnetResolver.resolve(source);
|
|
34
|
+
const isDotNet = dotnetResolution.isDotNet;
|
|
35
|
+
const resolvedNamespace = dotnetResolution.isDotNet
|
|
36
|
+
? dotnetResolution.resolvedNamespace
|
|
37
|
+
: undefined;
|
|
38
|
+
|
|
39
|
+
const specifiers = extractImportSpecifiers(node, checker);
|
|
40
|
+
|
|
41
|
+
// Check for module binding (Node.js API, etc.)
|
|
42
|
+
const binding = getBindingRegistry().getBinding(source);
|
|
43
|
+
const hasModuleBinding = binding?.kind === "module";
|
|
44
|
+
|
|
45
|
+
// Track ref/out/In imports from @tsonic/types
|
|
46
|
+
if (source === "@tsonic/types") {
|
|
47
|
+
getParameterModifierRegistry().processImport(node);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
imports.push({
|
|
51
|
+
kind: "import",
|
|
52
|
+
source,
|
|
53
|
+
isLocal,
|
|
54
|
+
isDotNet,
|
|
55
|
+
specifiers,
|
|
56
|
+
resolvedNamespace,
|
|
57
|
+
resolvedClrType: hasModuleBinding ? binding.type : undefined,
|
|
58
|
+
resolvedAssembly: hasModuleBinding ? binding.assembly : undefined,
|
|
59
|
+
});
|
|
60
|
+
}
|
|
61
|
+
ts.forEachChild(node, visitor);
|
|
62
|
+
};
|
|
63
|
+
|
|
64
|
+
visitor(sourceFile);
|
|
65
|
+
return imports;
|
|
66
|
+
};
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Extract import specifiers from an import declaration.
|
|
70
|
+
* Uses TypeChecker to determine if each named import is a type or value.
|
|
71
|
+
*/
|
|
72
|
+
export const extractImportSpecifiers = (
|
|
73
|
+
node: ts.ImportDeclaration,
|
|
74
|
+
checker: ts.TypeChecker
|
|
75
|
+
): readonly IrImportSpecifier[] => {
|
|
76
|
+
const specifiers: IrImportSpecifier[] = [];
|
|
77
|
+
|
|
78
|
+
if (node.importClause) {
|
|
79
|
+
// Default import
|
|
80
|
+
if (node.importClause.name) {
|
|
81
|
+
specifiers.push({
|
|
82
|
+
kind: "default",
|
|
83
|
+
localName: node.importClause.name.text,
|
|
84
|
+
});
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// Named or namespace imports
|
|
88
|
+
if (node.importClause.namedBindings) {
|
|
89
|
+
if (ts.isNamespaceImport(node.importClause.namedBindings)) {
|
|
90
|
+
specifiers.push({
|
|
91
|
+
kind: "namespace",
|
|
92
|
+
localName: node.importClause.namedBindings.name.text,
|
|
93
|
+
});
|
|
94
|
+
} else if (ts.isNamedImports(node.importClause.namedBindings)) {
|
|
95
|
+
node.importClause.namedBindings.elements.forEach((spec) => {
|
|
96
|
+
const isType = isTypeImport(spec, checker);
|
|
97
|
+
specifiers.push({
|
|
98
|
+
kind: "named",
|
|
99
|
+
name: (spec.propertyName ?? spec.name).text,
|
|
100
|
+
localName: spec.name.text,
|
|
101
|
+
isType,
|
|
102
|
+
});
|
|
103
|
+
});
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
return specifiers;
|
|
109
|
+
};
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* Determine if an import specifier refers to a type (interface, class, type alias, enum).
|
|
113
|
+
* Uses TypeChecker to resolve the symbol and check its declaration kind.
|
|
114
|
+
*/
|
|
115
|
+
const isTypeImport = (
|
|
116
|
+
spec: ts.ImportSpecifier,
|
|
117
|
+
checker: ts.TypeChecker
|
|
118
|
+
): boolean => {
|
|
119
|
+
try {
|
|
120
|
+
// TypeScript's isTypeOnly flag on the specifier itself (for `import { type Foo }`)
|
|
121
|
+
if (spec.isTypeOnly) {
|
|
122
|
+
return true;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
// Get the symbol for this import specifier
|
|
126
|
+
const symbol = checker.getSymbolAtLocation(spec.name);
|
|
127
|
+
if (!symbol) {
|
|
128
|
+
return false;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
// Follow aliases to get the actual symbol
|
|
132
|
+
const resolvedSymbol =
|
|
133
|
+
symbol.flags & ts.SymbolFlags.Alias
|
|
134
|
+
? checker.getAliasedSymbol(symbol)
|
|
135
|
+
: symbol;
|
|
136
|
+
|
|
137
|
+
if (!resolvedSymbol) {
|
|
138
|
+
return false;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
// Check if the symbol is a type
|
|
142
|
+
const flags = resolvedSymbol.flags;
|
|
143
|
+
return !!(
|
|
144
|
+
flags & ts.SymbolFlags.Interface ||
|
|
145
|
+
flags & ts.SymbolFlags.Class ||
|
|
146
|
+
flags & ts.SymbolFlags.TypeAlias ||
|
|
147
|
+
flags & ts.SymbolFlags.Enum ||
|
|
148
|
+
flags & ts.SymbolFlags.TypeParameter
|
|
149
|
+
);
|
|
150
|
+
} catch {
|
|
151
|
+
return false;
|
|
152
|
+
}
|
|
153
|
+
};
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* IR Builder - Public API
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
export type { IrBuildOptions } from "./types.js";
|
|
6
|
+
export { buildIrModule, buildIr } from "./orchestrator.js";
|
|
7
|
+
export { extractImports, extractImportSpecifiers } from "./imports.js";
|
|
8
|
+
export { extractExports } from "./exports.js";
|
|
9
|
+
export { extractStatements, isExecutableStatement } from "./statements.js";
|
|
10
|
+
export { hasExportModifier, hasDefaultModifier } from "./helpers.js";
|
|
@@ -0,0 +1,178 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* IR Builder orchestration - Main module building logic
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import * as ts from "typescript";
|
|
6
|
+
import { relative } from "path";
|
|
7
|
+
import { IrModule } from "../types.js";
|
|
8
|
+
import { TsonicProgram } from "../../program.js";
|
|
9
|
+
import { getNamespaceFromPath, getClassNameFromPath } from "../../resolver.js";
|
|
10
|
+
import { Result, ok, error } from "../../types/result.js";
|
|
11
|
+
import { Diagnostic, createDiagnostic } from "../../types/diagnostic.js";
|
|
12
|
+
import {
|
|
13
|
+
setMetadataRegistry,
|
|
14
|
+
setBindingRegistry,
|
|
15
|
+
} from "../statement-converter.js";
|
|
16
|
+
import { IrBuildOptions } from "./types.js";
|
|
17
|
+
import { extractImports } from "./imports.js";
|
|
18
|
+
import { extractExports } from "./exports.js";
|
|
19
|
+
import { extractStatements, isExecutableStatement } from "./statements.js";
|
|
20
|
+
import { validateClassImplements } from "./validation.js";
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Build IR module from TypeScript source file
|
|
24
|
+
*/
|
|
25
|
+
export const buildIrModule = (
|
|
26
|
+
sourceFile: ts.SourceFile,
|
|
27
|
+
program: TsonicProgram,
|
|
28
|
+
options: IrBuildOptions
|
|
29
|
+
): Result<IrModule, Diagnostic> => {
|
|
30
|
+
try {
|
|
31
|
+
// Set the metadata registry for this compilation
|
|
32
|
+
setMetadataRegistry(program.metadata);
|
|
33
|
+
|
|
34
|
+
// Set the binding registry for this compilation
|
|
35
|
+
setBindingRegistry(program.bindings);
|
|
36
|
+
|
|
37
|
+
const namespace = getNamespaceFromPath(
|
|
38
|
+
sourceFile.fileName,
|
|
39
|
+
options.sourceRoot,
|
|
40
|
+
options.rootNamespace
|
|
41
|
+
);
|
|
42
|
+
const className = getClassNameFromPath(sourceFile.fileName);
|
|
43
|
+
|
|
44
|
+
const imports = extractImports(
|
|
45
|
+
sourceFile,
|
|
46
|
+
program.checker,
|
|
47
|
+
program.dotnetResolver
|
|
48
|
+
);
|
|
49
|
+
const exports = extractExports(sourceFile, program.checker);
|
|
50
|
+
const statements = extractStatements(sourceFile, program.checker);
|
|
51
|
+
|
|
52
|
+
// Check for file name / export name collision (Issue #4)
|
|
53
|
+
// When file name matches an exported function/variable name, C# will have illegal code
|
|
54
|
+
// Example: main.ts exporting function main() → class main { void main() } ❌
|
|
55
|
+
// Note: Classes are allowed to match file name (Person.ts → class Person) - that's the normal pattern
|
|
56
|
+
const collisionExport = exports.find((exp) => {
|
|
57
|
+
if (exp.kind === "declaration") {
|
|
58
|
+
const decl = exp.declaration;
|
|
59
|
+
// Only check functions and variables, NOT classes (classes matching filename is normal)
|
|
60
|
+
if (decl.kind === "functionDeclaration") {
|
|
61
|
+
return decl.name === className;
|
|
62
|
+
} else if (decl.kind === "variableDeclaration") {
|
|
63
|
+
// Check if any of the variable declarators has a matching name
|
|
64
|
+
return decl.declarations.some((declarator) => {
|
|
65
|
+
if (declarator.name.kind === "identifierPattern") {
|
|
66
|
+
return declarator.name.name === className;
|
|
67
|
+
}
|
|
68
|
+
return false;
|
|
69
|
+
});
|
|
70
|
+
}
|
|
71
|
+
} else if (exp.kind === "named") {
|
|
72
|
+
// For named exports, we need to check what's being exported
|
|
73
|
+
// This is more complex because we'd need to look it up in statements
|
|
74
|
+
// For now, skip named exports (they're usually re-exports)
|
|
75
|
+
return false;
|
|
76
|
+
}
|
|
77
|
+
return false;
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
if (collisionExport) {
|
|
81
|
+
return error(
|
|
82
|
+
createDiagnostic(
|
|
83
|
+
"TSN2003",
|
|
84
|
+
"error",
|
|
85
|
+
`File name '${className}' conflicts with exported member name. In C#, a type cannot contain a member with the same name as the enclosing type. Consider renaming the file or the exported member.`,
|
|
86
|
+
{
|
|
87
|
+
file: sourceFile.fileName,
|
|
88
|
+
line: 1,
|
|
89
|
+
column: 1,
|
|
90
|
+
length: className.length,
|
|
91
|
+
}
|
|
92
|
+
)
|
|
93
|
+
);
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
// Validate class implements patterns
|
|
97
|
+
// TypeScript interfaces are nominalized to C# classes, so "implements" is invalid
|
|
98
|
+
const implementsDiagnostics = validateClassImplements(
|
|
99
|
+
sourceFile,
|
|
100
|
+
program.checker
|
|
101
|
+
);
|
|
102
|
+
const firstImplementsDiagnostic = implementsDiagnostics[0];
|
|
103
|
+
if (firstImplementsDiagnostic) {
|
|
104
|
+
return error(firstImplementsDiagnostic);
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
// Determine if this should be a static container
|
|
108
|
+
// Per spec: Files with a class matching the filename should NOT be static containers
|
|
109
|
+
// Static containers are for top-level functions and constants
|
|
110
|
+
const hasClassMatchingFilename = statements.some(
|
|
111
|
+
(stmt) => stmt.kind === "classDeclaration" && stmt.name === className
|
|
112
|
+
);
|
|
113
|
+
|
|
114
|
+
const hasTopLevelCode = statements.some(isExecutableStatement);
|
|
115
|
+
const isStaticContainer =
|
|
116
|
+
!hasClassMatchingFilename && !hasTopLevelCode && exports.length > 0;
|
|
117
|
+
|
|
118
|
+
// Compute relative file path from source root
|
|
119
|
+
// Normalize to forward slashes for cross-platform consistency
|
|
120
|
+
const relativePath = relative(
|
|
121
|
+
options.sourceRoot,
|
|
122
|
+
sourceFile.fileName
|
|
123
|
+
).replace(/\\/g, "/");
|
|
124
|
+
|
|
125
|
+
const module: IrModule = {
|
|
126
|
+
kind: "module",
|
|
127
|
+
filePath: relativePath, // Now stores relative path instead of absolute
|
|
128
|
+
namespace,
|
|
129
|
+
className,
|
|
130
|
+
isStaticContainer,
|
|
131
|
+
imports,
|
|
132
|
+
body: statements,
|
|
133
|
+
exports,
|
|
134
|
+
};
|
|
135
|
+
|
|
136
|
+
return ok(module);
|
|
137
|
+
} catch (err) {
|
|
138
|
+
return error(
|
|
139
|
+
createDiagnostic(
|
|
140
|
+
"TSN6001",
|
|
141
|
+
"error",
|
|
142
|
+
`Failed to build IR: ${err instanceof Error ? err.message : String(err)}`,
|
|
143
|
+
{
|
|
144
|
+
file: sourceFile.fileName,
|
|
145
|
+
line: 1,
|
|
146
|
+
column: 1,
|
|
147
|
+
length: 1,
|
|
148
|
+
}
|
|
149
|
+
)
|
|
150
|
+
);
|
|
151
|
+
}
|
|
152
|
+
};
|
|
153
|
+
|
|
154
|
+
/**
|
|
155
|
+
* Build IR for all source files in the program
|
|
156
|
+
*/
|
|
157
|
+
export const buildIr = (
|
|
158
|
+
program: TsonicProgram,
|
|
159
|
+
options: IrBuildOptions
|
|
160
|
+
): Result<readonly IrModule[], readonly Diagnostic[]> => {
|
|
161
|
+
const modules: IrModule[] = [];
|
|
162
|
+
const diagnostics: Diagnostic[] = [];
|
|
163
|
+
|
|
164
|
+
for (const sourceFile of program.sourceFiles) {
|
|
165
|
+
const result = buildIrModule(sourceFile, program, options);
|
|
166
|
+
if (result.ok) {
|
|
167
|
+
modules.push(result.value);
|
|
168
|
+
} else {
|
|
169
|
+
diagnostics.push(result.error);
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
if (diagnostics.length > 0) {
|
|
174
|
+
return error(diagnostics);
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
return ok(modules);
|
|
178
|
+
};
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Statement extraction from TypeScript source
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import * as ts from "typescript";
|
|
6
|
+
import { IrStatement } from "../types.js";
|
|
7
|
+
import { convertStatement } from "../statement-converter.js";
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Extract statements from source file
|
|
11
|
+
*/
|
|
12
|
+
export const extractStatements = (
|
|
13
|
+
sourceFile: ts.SourceFile,
|
|
14
|
+
checker: ts.TypeChecker
|
|
15
|
+
): readonly IrStatement[] => {
|
|
16
|
+
const statements: IrStatement[] = [];
|
|
17
|
+
|
|
18
|
+
sourceFile.statements.forEach((stmt) => {
|
|
19
|
+
// Skip imports and exports (handled separately)
|
|
20
|
+
if (
|
|
21
|
+
!ts.isImportDeclaration(stmt) &&
|
|
22
|
+
!ts.isExportDeclaration(stmt) &&
|
|
23
|
+
!ts.isExportAssignment(stmt)
|
|
24
|
+
) {
|
|
25
|
+
const converted = convertStatement(stmt, checker);
|
|
26
|
+
if (converted) {
|
|
27
|
+
statements.push(converted);
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
return statements;
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Check if a statement is executable (not a declaration)
|
|
37
|
+
*/
|
|
38
|
+
export const isExecutableStatement = (stmt: IrStatement): boolean => {
|
|
39
|
+
// Declarations are not executable - they become static members in the container
|
|
40
|
+
const declarationKinds = [
|
|
41
|
+
"functionDeclaration",
|
|
42
|
+
"classDeclaration",
|
|
43
|
+
"interfaceDeclaration",
|
|
44
|
+
"typeAliasDeclaration",
|
|
45
|
+
"enumDeclaration",
|
|
46
|
+
"variableDeclaration", // Added: variable declarations become static fields
|
|
47
|
+
];
|
|
48
|
+
|
|
49
|
+
// Empty statements are not executable
|
|
50
|
+
if (stmt.kind === "emptyStatement") {
|
|
51
|
+
return false;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
return !declarationKinds.includes(stmt.kind);
|
|
55
|
+
};
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* IR builder validation - checks for unsupported patterns
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import * as ts from "typescript";
|
|
6
|
+
import { Diagnostic, createDiagnostic } from "../../types/diagnostic.js";
|
|
7
|
+
import { getSourceLocation } from "../../program/diagnostics.js";
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Check if a type reference is the struct marker
|
|
11
|
+
* (used to mark types as C# value types)
|
|
12
|
+
*/
|
|
13
|
+
const isStructMarker = (
|
|
14
|
+
typeRef: ts.ExpressionWithTypeArguments,
|
|
15
|
+
checker: ts.TypeChecker
|
|
16
|
+
): boolean => {
|
|
17
|
+
const symbol = checker.getSymbolAtLocation(typeRef.expression);
|
|
18
|
+
return symbol?.escapedName === "struct" || symbol?.escapedName === "Struct";
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Check if a symbol represents a TypeScript interface
|
|
23
|
+
* (which Tsonic nominalizes to a C# class)
|
|
24
|
+
*/
|
|
25
|
+
const isNominalizedInterface = (
|
|
26
|
+
symbol: ts.Symbol | undefined,
|
|
27
|
+
_checker: ts.TypeChecker
|
|
28
|
+
): boolean => {
|
|
29
|
+
if (!symbol) return false;
|
|
30
|
+
|
|
31
|
+
const declarations = symbol.getDeclarations();
|
|
32
|
+
if (!declarations || declarations.length === 0) return false;
|
|
33
|
+
|
|
34
|
+
// Check if any declaration is an interface
|
|
35
|
+
return declarations.some((decl) => ts.isInterfaceDeclaration(decl));
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Check if a symbol represents a type alias for an object type
|
|
40
|
+
* (which Tsonic nominalizes to a C# class)
|
|
41
|
+
*/
|
|
42
|
+
const isNominalizedTypeAlias = (
|
|
43
|
+
symbol: ts.Symbol | undefined,
|
|
44
|
+
_checker: ts.TypeChecker
|
|
45
|
+
): boolean => {
|
|
46
|
+
if (!symbol) return false;
|
|
47
|
+
|
|
48
|
+
const declarations = symbol.getDeclarations();
|
|
49
|
+
if (!declarations || declarations.length === 0) return false;
|
|
50
|
+
|
|
51
|
+
// Check if declaration is a type alias with object literal type
|
|
52
|
+
return declarations.some((decl) => {
|
|
53
|
+
if (!ts.isTypeAliasDeclaration(decl)) return false;
|
|
54
|
+
// Type aliases for object shapes are nominalized
|
|
55
|
+
return ts.isTypeLiteralNode(decl.type);
|
|
56
|
+
});
|
|
57
|
+
};
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Validate a class declaration for implements clause issues
|
|
61
|
+
*/
|
|
62
|
+
const validateClassDeclaration = (
|
|
63
|
+
node: ts.ClassDeclaration,
|
|
64
|
+
checker: ts.TypeChecker
|
|
65
|
+
): readonly Diagnostic[] => {
|
|
66
|
+
const diagnostics: Diagnostic[] = [];
|
|
67
|
+
|
|
68
|
+
const implementsClause = node.heritageClauses?.find(
|
|
69
|
+
(h) => h.token === ts.SyntaxKind.ImplementsKeyword
|
|
70
|
+
);
|
|
71
|
+
|
|
72
|
+
if (!implementsClause) return [];
|
|
73
|
+
|
|
74
|
+
for (const typeRef of implementsClause.types) {
|
|
75
|
+
// Skip the struct marker - it's a special pattern for value types
|
|
76
|
+
if (isStructMarker(typeRef, checker)) {
|
|
77
|
+
continue;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
// Get the symbol for the identifier (not the resolved type)
|
|
81
|
+
// This preserves type alias identity
|
|
82
|
+
const identifierSymbol = checker.getSymbolAtLocation(typeRef.expression);
|
|
83
|
+
|
|
84
|
+
// Check if it's a nominalized interface or type alias
|
|
85
|
+
if (
|
|
86
|
+
isNominalizedInterface(identifierSymbol, checker) ||
|
|
87
|
+
isNominalizedTypeAlias(identifierSymbol, checker)
|
|
88
|
+
) {
|
|
89
|
+
const typeName = typeRef.expression.getText();
|
|
90
|
+
const location = getSourceLocation(
|
|
91
|
+
node.getSourceFile(),
|
|
92
|
+
typeRef.getStart(),
|
|
93
|
+
typeRef.getWidth()
|
|
94
|
+
);
|
|
95
|
+
|
|
96
|
+
diagnostics.push(
|
|
97
|
+
createDiagnostic(
|
|
98
|
+
"TSN7301",
|
|
99
|
+
"error",
|
|
100
|
+
`Class cannot implement '${typeName}': TypeScript interfaces are nominalized to C# classes in Tsonic. Use 'extends' instead, or refactor to composition.`,
|
|
101
|
+
location,
|
|
102
|
+
"In Tsonic, interfaces become classes for object initializer support. C# classes cannot implement other classes."
|
|
103
|
+
)
|
|
104
|
+
);
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
return diagnostics;
|
|
109
|
+
};
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* Validate all class declarations in a source file
|
|
113
|
+
*/
|
|
114
|
+
export const validateClassImplements = (
|
|
115
|
+
sourceFile: ts.SourceFile,
|
|
116
|
+
checker: ts.TypeChecker
|
|
117
|
+
): readonly Diagnostic[] => {
|
|
118
|
+
const diagnostics: Diagnostic[] = [];
|
|
119
|
+
|
|
120
|
+
const visit = (node: ts.Node): void => {
|
|
121
|
+
if (ts.isClassDeclaration(node)) {
|
|
122
|
+
diagnostics.push(...validateClassDeclaration(node, checker));
|
|
123
|
+
}
|
|
124
|
+
ts.forEachChild(node, visit);
|
|
125
|
+
};
|
|
126
|
+
|
|
127
|
+
visit(sourceFile);
|
|
128
|
+
return diagnostics;
|
|
129
|
+
};
|