@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.
Files changed (145) hide show
  1. package/package.json +53 -0
  2. package/src/dependency-graph.ts +18 -0
  3. package/src/dotnet-metadata.ts +121 -0
  4. package/src/graph/builder.ts +81 -0
  5. package/src/graph/circular.ts +58 -0
  6. package/src/graph/extraction/exports.ts +55 -0
  7. package/src/graph/extraction/imports.ts +81 -0
  8. package/src/graph/extraction/index.ts +7 -0
  9. package/src/graph/extraction/orchestrator.ts +99 -0
  10. package/src/graph/extraction.ts +10 -0
  11. package/src/graph/helpers.ts +51 -0
  12. package/src/graph/index.ts +17 -0
  13. package/src/graph/types.ts +13 -0
  14. package/src/index.ts +80 -0
  15. package/src/ir/binding-resolution.test.ts +585 -0
  16. package/src/ir/builder/exports.ts +78 -0
  17. package/src/ir/builder/helpers.ts +27 -0
  18. package/src/ir/builder/imports.ts +153 -0
  19. package/src/ir/builder/index.ts +10 -0
  20. package/src/ir/builder/orchestrator.ts +178 -0
  21. package/src/ir/builder/statements.ts +55 -0
  22. package/src/ir/builder/types.ts +8 -0
  23. package/src/ir/builder/validation.ts +129 -0
  24. package/src/ir/builder.test.ts +581 -0
  25. package/src/ir/builder.ts +14 -0
  26. package/src/ir/converters/expressions/access.ts +99 -0
  27. package/src/ir/converters/expressions/calls.ts +137 -0
  28. package/src/ir/converters/expressions/collections.ts +84 -0
  29. package/src/ir/converters/expressions/functions.ts +62 -0
  30. package/src/ir/converters/expressions/helpers.ts +264 -0
  31. package/src/ir/converters/expressions/index.ts +43 -0
  32. package/src/ir/converters/expressions/literals.ts +22 -0
  33. package/src/ir/converters/expressions/operators.ts +147 -0
  34. package/src/ir/converters/expressions/other.ts +60 -0
  35. package/src/ir/converters/statements/control/blocks.ts +22 -0
  36. package/src/ir/converters/statements/control/conditionals.ts +67 -0
  37. package/src/ir/converters/statements/control/exceptions.ts +43 -0
  38. package/src/ir/converters/statements/control/index.ts +17 -0
  39. package/src/ir/converters/statements/control/loops.ts +99 -0
  40. package/src/ir/converters/statements/control.ts +17 -0
  41. package/src/ir/converters/statements/declarations/classes/constructors.ts +120 -0
  42. package/src/ir/converters/statements/declarations/classes/index.ts +12 -0
  43. package/src/ir/converters/statements/declarations/classes/methods.ts +61 -0
  44. package/src/ir/converters/statements/declarations/classes/orchestrator.ts +166 -0
  45. package/src/ir/converters/statements/declarations/classes/override-detection.ts +116 -0
  46. package/src/ir/converters/statements/declarations/classes/properties.ts +63 -0
  47. package/src/ir/converters/statements/declarations/classes.ts +6 -0
  48. package/src/ir/converters/statements/declarations/enums.ts +29 -0
  49. package/src/ir/converters/statements/declarations/functions.ts +39 -0
  50. package/src/ir/converters/statements/declarations/index.ts +14 -0
  51. package/src/ir/converters/statements/declarations/interfaces.ts +131 -0
  52. package/src/ir/converters/statements/declarations/registry.ts +45 -0
  53. package/src/ir/converters/statements/declarations/type-aliases.ts +25 -0
  54. package/src/ir/converters/statements/declarations/variables.ts +60 -0
  55. package/src/ir/converters/statements/declarations.ts +16 -0
  56. package/src/ir/converters/statements/helpers.ts +174 -0
  57. package/src/ir/converters/statements/index.ts +40 -0
  58. package/src/ir/expression-converter.ts +207 -0
  59. package/src/ir/generic-validator.ts +100 -0
  60. package/src/ir/hierarchical-bindings-e2e.test.ts +163 -0
  61. package/src/ir/index.ts +6 -0
  62. package/src/ir/statement-converter.ts +128 -0
  63. package/src/ir/type-converter/arrays.ts +20 -0
  64. package/src/ir/type-converter/converter.ts +10 -0
  65. package/src/ir/type-converter/functions.ts +22 -0
  66. package/src/ir/type-converter/index.ts +11 -0
  67. package/src/ir/type-converter/inference.ts +122 -0
  68. package/src/ir/type-converter/literals.ts +40 -0
  69. package/src/ir/type-converter/objects.ts +107 -0
  70. package/src/ir/type-converter/orchestrator.ts +85 -0
  71. package/src/ir/type-converter/patterns.ts +73 -0
  72. package/src/ir/type-converter/primitives.ts +57 -0
  73. package/src/ir/type-converter/references.ts +64 -0
  74. package/src/ir/type-converter/unions-intersections.ts +34 -0
  75. package/src/ir/type-converter.ts +13 -0
  76. package/src/ir/types/expressions.ts +215 -0
  77. package/src/ir/types/guards.ts +39 -0
  78. package/src/ir/types/helpers.ts +135 -0
  79. package/src/ir/types/index.ts +108 -0
  80. package/src/ir/types/ir-types.ts +96 -0
  81. package/src/ir/types/module.ts +57 -0
  82. package/src/ir/types/statements.ts +238 -0
  83. package/src/ir/types.ts +97 -0
  84. package/src/metadata/bindings-loader.test.ts +144 -0
  85. package/src/metadata/bindings-loader.ts +357 -0
  86. package/src/metadata/index.ts +15 -0
  87. package/src/metadata/library-loader.ts +153 -0
  88. package/src/metadata/loader.test.ts +156 -0
  89. package/src/metadata/loader.ts +382 -0
  90. package/src/program/bindings.test.ts +512 -0
  91. package/src/program/bindings.ts +253 -0
  92. package/src/program/config.ts +30 -0
  93. package/src/program/creation.ts +249 -0
  94. package/src/program/dependency-graph.ts +245 -0
  95. package/src/program/diagnostics.ts +103 -0
  96. package/src/program/index.ts +19 -0
  97. package/src/program/metadata.ts +68 -0
  98. package/src/program/queries.ts +18 -0
  99. package/src/program/types.ts +38 -0
  100. package/src/program.ts +13 -0
  101. package/src/resolver/dotnet-import-resolver.ts +226 -0
  102. package/src/resolver/import-resolution.ts +177 -0
  103. package/src/resolver/index.ts +18 -0
  104. package/src/resolver/namespace.test.ts +86 -0
  105. package/src/resolver/namespace.ts +42 -0
  106. package/src/resolver/naming.ts +38 -0
  107. package/src/resolver/path-resolution.ts +22 -0
  108. package/src/resolver/types.ts +15 -0
  109. package/src/resolver.test.ts +155 -0
  110. package/src/resolver.ts +14 -0
  111. package/src/symbol-table/builder.ts +114 -0
  112. package/src/symbol-table/creation.ts +42 -0
  113. package/src/symbol-table/helpers.ts +18 -0
  114. package/src/symbol-table/index.ts +13 -0
  115. package/src/symbol-table/queries.ts +42 -0
  116. package/src/symbol-table/types.ts +28 -0
  117. package/src/symbol-table.ts +14 -0
  118. package/src/types/bindings.ts +172 -0
  119. package/src/types/diagnostic.test.ts +164 -0
  120. package/src/types/diagnostic.ts +153 -0
  121. package/src/types/explicit-views.test.ts +113 -0
  122. package/src/types/explicit-views.ts +218 -0
  123. package/src/types/metadata.ts +229 -0
  124. package/src/types/module.ts +99 -0
  125. package/src/types/nested-types.test.ts +194 -0
  126. package/src/types/nested-types.ts +215 -0
  127. package/src/types/parameter-modifiers.ts +173 -0
  128. package/src/types/ref-parameters.test.ts +192 -0
  129. package/src/types/ref-parameters.ts +268 -0
  130. package/src/types/result.test.ts +157 -0
  131. package/src/types/result.ts +48 -0
  132. package/src/types/support-types.test.ts +81 -0
  133. package/src/types/support-types.ts +288 -0
  134. package/src/types/test-harness.ts +180 -0
  135. package/src/validation/exports.ts +98 -0
  136. package/src/validation/features.ts +89 -0
  137. package/src/validation/generics.ts +40 -0
  138. package/src/validation/helpers.ts +31 -0
  139. package/src/validation/imports.ts +97 -0
  140. package/src/validation/index.ts +11 -0
  141. package/src/validation/orchestrator.ts +51 -0
  142. package/src/validation/static-safety.ts +267 -0
  143. package/src/validator.test.ts +468 -0
  144. package/src/validator.ts +15 -0
  145. package/tsconfig.json +13 -0
@@ -0,0 +1,31 @@
1
+ /**
2
+ * Validation helper functions
3
+ */
4
+
5
+ import * as ts from "typescript";
6
+
7
+ // Re-export hasExportModifier from graph helpers to avoid duplication
8
+ export { hasExportModifier } from "../graph/helpers.js";
9
+
10
+ /**
11
+ * Get location information for a node
12
+ */
13
+ export const getNodeLocation = (
14
+ sourceFile: ts.SourceFile,
15
+ node: ts.Node
16
+ ): {
17
+ readonly file: string;
18
+ readonly line: number;
19
+ readonly column: number;
20
+ readonly length: number;
21
+ } => {
22
+ const { line, character } = sourceFile.getLineAndCharacterOfPosition(
23
+ node.getStart()
24
+ );
25
+ return {
26
+ file: sourceFile.fileName,
27
+ line: line + 1,
28
+ column: character + 1,
29
+ length: node.getWidth(),
30
+ };
31
+ };
@@ -0,0 +1,97 @@
1
+ /**
2
+ * Import validation
3
+ */
4
+
5
+ import * as ts from "typescript";
6
+ import { TsonicProgram } from "../program.js";
7
+ import {
8
+ DiagnosticsCollector,
9
+ addDiagnostic,
10
+ createDiagnostic,
11
+ } from "../types/diagnostic.js";
12
+ import { resolveImport } from "../resolver.js";
13
+ import { getNodeLocation } from "./helpers.js";
14
+
15
+ /**
16
+ * Validate all imports in a source file
17
+ */
18
+ export const validateImports = (
19
+ sourceFile: ts.SourceFile,
20
+ program: TsonicProgram,
21
+ collector: DiagnosticsCollector
22
+ ): DiagnosticsCollector => {
23
+ const visitor = (node: ts.Node): DiagnosticsCollector => {
24
+ if (ts.isImportDeclaration(node)) {
25
+ return validateImportDeclaration(node, sourceFile, program, collector);
26
+ }
27
+
28
+ if (ts.isImportTypeNode(node)) {
29
+ return addDiagnostic(
30
+ collector,
31
+ createDiagnostic(
32
+ "TSN2001",
33
+ "error",
34
+ "Import type syntax not supported",
35
+ getNodeLocation(sourceFile, node),
36
+ "Use regular imports instead"
37
+ )
38
+ );
39
+ }
40
+
41
+ return ts.forEachChild(node, visitor) ?? collector;
42
+ };
43
+
44
+ return visitor(sourceFile);
45
+ };
46
+
47
+ /**
48
+ * Validate a specific import declaration
49
+ */
50
+ export const validateImportDeclaration = (
51
+ node: ts.ImportDeclaration,
52
+ sourceFile: ts.SourceFile,
53
+ program: TsonicProgram,
54
+ collector: DiagnosticsCollector
55
+ ): DiagnosticsCollector => {
56
+ if (!ts.isStringLiteral(node.moduleSpecifier)) {
57
+ return addDiagnostic(
58
+ collector,
59
+ createDiagnostic(
60
+ "TSN2001",
61
+ "error",
62
+ "Dynamic imports not supported",
63
+ getNodeLocation(sourceFile, node),
64
+ "Use static import statements"
65
+ )
66
+ );
67
+ }
68
+
69
+ const importPath = node.moduleSpecifier.text;
70
+ const result = resolveImport(
71
+ importPath,
72
+ sourceFile.fileName,
73
+ program.options.sourceRoot,
74
+ program.dotnetResolver
75
+ );
76
+
77
+ if (!result.ok) {
78
+ const location = getNodeLocation(sourceFile, node.moduleSpecifier);
79
+ return addDiagnostic(collector, { ...result.error, location });
80
+ }
81
+
82
+ // Check for default imports from local modules (we might want to restrict this)
83
+ if (result.value.isLocal && node.importClause?.name) {
84
+ return addDiagnostic(
85
+ collector,
86
+ createDiagnostic(
87
+ "TSN2001",
88
+ "warning",
89
+ "Default imports from local modules may not work as expected",
90
+ getNodeLocation(sourceFile, node.importClause),
91
+ "Consider using named imports"
92
+ )
93
+ );
94
+ }
95
+
96
+ return collector;
97
+ };
@@ -0,0 +1,11 @@
1
+ /**
2
+ * Validation - Public API
3
+ */
4
+
5
+ export { validateProgram, validateSourceFile } from "./orchestrator.js";
6
+ export { validateImports, validateImportDeclaration } from "./imports.js";
7
+ export { validateExports } from "./exports.js";
8
+ export { validateUnsupportedFeatures } from "./features.js";
9
+ export { validateGenerics } from "./generics.js";
10
+ export { validateStaticSafety } from "./static-safety.js";
11
+ export { hasExportModifier, getNodeLocation } from "./helpers.js";
@@ -0,0 +1,51 @@
1
+ /**
2
+ * Validation orchestrator - coordinates all validation functions
3
+ */
4
+
5
+ import * as ts from "typescript";
6
+ import { TsonicProgram } from "../program.js";
7
+ import {
8
+ DiagnosticsCollector,
9
+ createDiagnosticsCollector,
10
+ } from "../types/diagnostic.js";
11
+ import { validateImports } from "./imports.js";
12
+ import { validateExports } from "./exports.js";
13
+ import { validateUnsupportedFeatures } from "./features.js";
14
+ import { validateGenerics } from "./generics.js";
15
+ import { validateStaticSafety } from "./static-safety.js";
16
+
17
+ /**
18
+ * Validate an entire Tsonic program
19
+ */
20
+ export const validateProgram = (
21
+ program: TsonicProgram
22
+ ): DiagnosticsCollector => {
23
+ const collector = createDiagnosticsCollector();
24
+
25
+ return program.sourceFiles.reduce(
26
+ (acc, sourceFile) => validateSourceFile(sourceFile, program, acc),
27
+ collector
28
+ );
29
+ };
30
+
31
+ /**
32
+ * Validate a single source file
33
+ */
34
+ export const validateSourceFile = (
35
+ sourceFile: ts.SourceFile,
36
+ program: TsonicProgram,
37
+ collector: DiagnosticsCollector
38
+ ): DiagnosticsCollector => {
39
+ const validationFns = [
40
+ validateImports,
41
+ validateExports,
42
+ validateUnsupportedFeatures,
43
+ validateGenerics,
44
+ validateStaticSafety,
45
+ ];
46
+
47
+ return validationFns.reduce(
48
+ (acc, fn) => fn(sourceFile, program, acc),
49
+ collector
50
+ );
51
+ };
@@ -0,0 +1,267 @@
1
+ /**
2
+ * Static Safety Validation
3
+ *
4
+ * Detects patterns that violate static typing requirements:
5
+ * - TSN7401: 'any' type usage
6
+ * - TSN7403: Object literal without contextual nominal type
7
+ * - TSN7405: Untyped function/arrow/lambda parameter
8
+ * - TSN7413: Dictionary key must be string type
9
+ *
10
+ * This ensures NativeAOT-compatible, predictable-performance output.
11
+ *
12
+ * Note: We intentionally do NOT validate JS built-in usage (arr.map, str.length)
13
+ * or dictionary dot-access patterns. These will fail naturally in C# if used
14
+ * incorrectly, which is an acceptable failure mode.
15
+ */
16
+
17
+ import * as ts from "typescript";
18
+ import { TsonicProgram } from "../program.js";
19
+ import {
20
+ DiagnosticsCollector,
21
+ addDiagnostic,
22
+ createDiagnostic,
23
+ } from "../types/diagnostic.js";
24
+ import { getNodeLocation } from "./helpers.js";
25
+
26
+ /**
27
+ * Validate a source file for static safety violations.
28
+ */
29
+ export const validateStaticSafety = (
30
+ sourceFile: ts.SourceFile,
31
+ program: TsonicProgram,
32
+ collector: DiagnosticsCollector
33
+ ): DiagnosticsCollector => {
34
+ const checker = program.checker;
35
+
36
+ const visitor = (
37
+ node: ts.Node,
38
+ accCollector: DiagnosticsCollector
39
+ ): DiagnosticsCollector => {
40
+ let currentCollector = accCollector;
41
+
42
+ // TSN7401: Check for explicit 'any' type annotations
43
+ if (node.kind === ts.SyntaxKind.AnyKeyword) {
44
+ currentCollector = addDiagnostic(
45
+ currentCollector,
46
+ createDiagnostic(
47
+ "TSN7401",
48
+ "error",
49
+ "'any' type is not supported. Provide a concrete type, use 'unknown', or define a nominal type.",
50
+ getNodeLocation(sourceFile, node),
51
+ "Replace 'any' with a specific type like 'unknown', 'object', or a custom interface."
52
+ )
53
+ );
54
+ }
55
+
56
+ // TSN7401: Check for 'as any' type assertions
57
+ if (
58
+ ts.isAsExpression(node) &&
59
+ node.type.kind === ts.SyntaxKind.AnyKeyword
60
+ ) {
61
+ currentCollector = addDiagnostic(
62
+ currentCollector,
63
+ createDiagnostic(
64
+ "TSN7401",
65
+ "error",
66
+ "'as any' type assertion is not supported. Use a specific type assertion.",
67
+ getNodeLocation(sourceFile, node),
68
+ "Replace 'as any' with a specific type like 'as unknown' or 'as YourType'."
69
+ )
70
+ );
71
+ }
72
+
73
+ // TSN7405: Check for untyped function parameters
74
+ // Covers: function declarations, methods, constructors, arrow functions, function expressions
75
+ if (ts.isParameter(node) && !node.type) {
76
+ const parent = node.parent;
77
+ const isFunctionLike =
78
+ ts.isFunctionDeclaration(parent) ||
79
+ ts.isMethodDeclaration(parent) ||
80
+ ts.isConstructorDeclaration(parent) ||
81
+ ts.isArrowFunction(parent) ||
82
+ ts.isFunctionExpression(parent) ||
83
+ ts.isGetAccessorDeclaration(parent) ||
84
+ ts.isSetAccessorDeclaration(parent);
85
+
86
+ if (isFunctionLike) {
87
+ const paramName = ts.isIdentifier(node.name) ? node.name.text : "param";
88
+ currentCollector = addDiagnostic(
89
+ currentCollector,
90
+ createDiagnostic(
91
+ "TSN7405",
92
+ "error",
93
+ `Parameter '${paramName}' must have an explicit type annotation.`,
94
+ getNodeLocation(sourceFile, node),
95
+ "Add a type annotation to this parameter."
96
+ )
97
+ );
98
+ }
99
+ }
100
+
101
+ // TSN7403: Check for object literals without contextual nominal type
102
+ if (ts.isObjectLiteralExpression(node)) {
103
+ const contextualType = checker.getContextualType(node);
104
+
105
+ // Must have a contextual type that resolves to a nominal type or dictionary
106
+ if (
107
+ !contextualType ||
108
+ !isNominalOrDictionaryType(contextualType, checker)
109
+ ) {
110
+ currentCollector = addDiagnostic(
111
+ currentCollector,
112
+ createDiagnostic(
113
+ "TSN7403",
114
+ "error",
115
+ "Object literal requires a contextual nominal type (interface, type alias, or class). Anonymous object types are not supported.",
116
+ getNodeLocation(sourceFile, node),
117
+ "Add a type annotation like 'const x: MyInterface = { ... }' or define an interface/type alias."
118
+ )
119
+ );
120
+ }
121
+ }
122
+
123
+ // TSN7413: Check for non-string dictionary keys
124
+ // Record<K, V> where K is not string
125
+ if (ts.isTypeReferenceNode(node)) {
126
+ const typeName = node.typeName;
127
+ if (ts.isIdentifier(typeName) && typeName.text === "Record") {
128
+ const typeArgs = node.typeArguments;
129
+ if (typeArgs && typeArgs.length >= 1) {
130
+ const keyTypeNode = typeArgs[0]!;
131
+ if (!isStringKeyType(keyTypeNode)) {
132
+ currentCollector = addDiagnostic(
133
+ currentCollector,
134
+ createDiagnostic(
135
+ "TSN7413",
136
+ "error",
137
+ "Dictionary key type must be 'string'. Non-string key types are not supported for NativeAOT compatibility.",
138
+ getNodeLocation(sourceFile, keyTypeNode),
139
+ "Use Record<string, V> instead of Record<number, V> or other key types."
140
+ )
141
+ );
142
+ }
143
+ }
144
+ }
145
+ }
146
+
147
+ // TSN7413: Check for non-string index signatures
148
+ // { [k: number]: V } is not allowed
149
+ if (ts.isIndexSignatureDeclaration(node)) {
150
+ const keyParam = node.parameters[0];
151
+ if (keyParam?.type && !isStringKeyType(keyParam.type)) {
152
+ currentCollector = addDiagnostic(
153
+ currentCollector,
154
+ createDiagnostic(
155
+ "TSN7413",
156
+ "error",
157
+ "Index signature key type must be 'string'. Non-string key types are not supported for NativeAOT compatibility.",
158
+ getNodeLocation(sourceFile, keyParam.type),
159
+ "Use { [key: string]: V } instead of { [key: number]: V }."
160
+ )
161
+ );
162
+ }
163
+ }
164
+
165
+ // Continue visiting children
166
+ ts.forEachChild(node, (child) => {
167
+ currentCollector = visitor(child, currentCollector);
168
+ });
169
+
170
+ return currentCollector;
171
+ };
172
+
173
+ return visitor(sourceFile, collector);
174
+ };
175
+
176
+ /**
177
+ * Check if a contextual type is a nominal type (interface, type alias, class)
178
+ * or a dictionary type that we can emit.
179
+ *
180
+ * Returns false for anonymous object types like `{ x: number }` without a name.
181
+ */
182
+ const isNominalOrDictionaryType = (
183
+ type: ts.Type,
184
+ checker: ts.TypeChecker
185
+ ): boolean => {
186
+ // Check if it's a dictionary type (Record<K,V> or index signature)
187
+ if (isTsDictionaryType(type)) {
188
+ return true;
189
+ }
190
+
191
+ // Check if the type has a symbol with a declaration (named type)
192
+ const symbol = type.getSymbol() ?? type.aliasSymbol;
193
+ if (symbol) {
194
+ const declarations = symbol.getDeclarations();
195
+ if (declarations && declarations.length > 0) {
196
+ const decl = declarations[0]!;
197
+ // Accept: interface, type alias, class
198
+ if (
199
+ ts.isInterfaceDeclaration(decl) ||
200
+ ts.isTypeAliasDeclaration(decl) ||
201
+ ts.isClassDeclaration(decl)
202
+ ) {
203
+ return true;
204
+ }
205
+ }
206
+ }
207
+
208
+ // Check for primitive types (allowed)
209
+ if (
210
+ type.flags & ts.TypeFlags.String ||
211
+ type.flags & ts.TypeFlags.Number ||
212
+ type.flags & ts.TypeFlags.Boolean ||
213
+ type.flags & ts.TypeFlags.Null ||
214
+ type.flags & ts.TypeFlags.Undefined ||
215
+ type.flags & ts.TypeFlags.Void
216
+ ) {
217
+ return true;
218
+ }
219
+
220
+ // Check for array types (allowed)
221
+ if (checker.isArrayType(type) || checker.isTupleType(type)) {
222
+ return true;
223
+ }
224
+
225
+ return false;
226
+ };
227
+
228
+ /**
229
+ * Check if a type is a TS dictionary type (Record<K,V> or index signature).
230
+ *
231
+ * TS dictionary types:
232
+ * - Record<string, T> → has aliasSymbol named "Record"
233
+ * - { [k: string]: T } → has string index signature
234
+ */
235
+ const isTsDictionaryType = (type: ts.Type): boolean => {
236
+ // Check for Record<K,V> utility type
237
+ if (type.aliasSymbol?.name === "Record") {
238
+ return true;
239
+ }
240
+
241
+ // Check for index signature type like { [k: string]: T }
242
+ const stringIndexType = type.getStringIndexType();
243
+ const numberIndexType = type.getNumberIndexType();
244
+
245
+ return !!(stringIndexType || numberIndexType);
246
+ };
247
+
248
+ /**
249
+ * Check if a type node represents a string key type.
250
+ * Only `string` keyword is allowed for dictionary keys.
251
+ */
252
+ const isStringKeyType = (typeNode: ts.TypeNode): boolean => {
253
+ // Direct string keyword
254
+ if (typeNode.kind === ts.SyntaxKind.StringKeyword) {
255
+ return true;
256
+ }
257
+
258
+ // Type reference to "string"
259
+ if (ts.isTypeReferenceNode(typeNode)) {
260
+ const typeName = typeNode.typeName;
261
+ if (ts.isIdentifier(typeName) && typeName.text === "string") {
262
+ return true;
263
+ }
264
+ }
265
+
266
+ return false;
267
+ };