lt-script 1.0.1 → 1.0.5

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.
@@ -0,0 +1,682 @@
1
+ import * as AST from '../parser/AST.js';
2
+ class Scope {
3
+ symbols = new Map();
4
+ parent;
5
+ constructor(parent) {
6
+ this.parent = parent;
7
+ }
8
+ define(name, kind, type, line) {
9
+ // In strict mode we might check for redeclaration here
10
+ this.symbols.set(name, { name, kind, type, definedAtLine: line });
11
+ }
12
+ lookup(name) {
13
+ let current = this;
14
+ while (current) {
15
+ if (current.symbols.has(name)) {
16
+ return current.symbols.get(name);
17
+ }
18
+ current = current.parent;
19
+ }
20
+ return undefined;
21
+ }
22
+ }
23
+ export class SemanticAnalyzer {
24
+ globalScope = new Scope();
25
+ currentScope = this.globalScope;
26
+ typeRegistry = new Map(); // Stores interface fields
27
+ typeAliasRegistry = new Map(); // Stores type alias strings
28
+ memberTypes = new Map(); // Tracks Table.Field -> type mappings
29
+ functionRegistry = new Map(); // Stores function signatures
30
+ analyze(program) {
31
+ // Reset scope for fresh analysis (though typically instance is fresh)
32
+ this.currentScope = this.globalScope;
33
+ this.typeRegistry.clear();
34
+ this.typeAliasRegistry.clear();
35
+ this.memberTypes.clear();
36
+ this.functionRegistry.clear();
37
+ // First pass: Register types (to allow forward references)
38
+ for (const stmt of program.body) {
39
+ if (stmt.kind === AST.NodeType.TypeDecl) {
40
+ this.visitTypeDecl(stmt);
41
+ }
42
+ else if (stmt.kind === AST.NodeType.TypeAliasDecl) {
43
+ this.visitTypeAliasDecl(stmt);
44
+ }
45
+ }
46
+ // Second pass: Analyze code
47
+ for (const stmt of program.body) {
48
+ if (stmt.kind !== AST.NodeType.TypeDecl && stmt.kind !== AST.NodeType.TypeAliasDecl) {
49
+ this.visitStatement(stmt);
50
+ }
51
+ }
52
+ }
53
+ enterScope() {
54
+ this.currentScope = new Scope(this.currentScope);
55
+ }
56
+ exitScope() {
57
+ if (this.currentScope.parent) {
58
+ this.currentScope = this.currentScope.parent;
59
+ }
60
+ }
61
+ visitStatement(stmt) {
62
+ switch (stmt.kind) {
63
+ case AST.NodeType.VariableDecl:
64
+ this.visitVariableDecl(stmt);
65
+ break;
66
+ case AST.NodeType.TypeDecl:
67
+ this.visitTypeDecl(stmt);
68
+ break;
69
+ case AST.NodeType.AssignmentStmt:
70
+ this.visitAssignmentStmt(stmt);
71
+ break;
72
+ case AST.NodeType.CompoundAssignment:
73
+ this.visitCompoundAssignment(stmt);
74
+ break;
75
+ case AST.NodeType.FunctionDecl:
76
+ this.visitFunctionDecl(stmt);
77
+ break;
78
+ case AST.NodeType.Block:
79
+ this.visitBlock(stmt);
80
+ break;
81
+ case AST.NodeType.IfStmt:
82
+ {
83
+ const s = stmt;
84
+ this.visitExpression(s.condition);
85
+ this.visitBlock(s.thenBody);
86
+ s.elseIfClauses?.forEach(c => {
87
+ this.visitExpression(c.condition);
88
+ this.visitBlock(c.body);
89
+ });
90
+ if (s.elseBody)
91
+ this.visitBlock(s.elseBody);
92
+ }
93
+ break;
94
+ case AST.NodeType.WhileStmt:
95
+ {
96
+ const s = stmt;
97
+ this.visitExpression(s.condition);
98
+ this.visitBlock(s.body);
99
+ }
100
+ break;
101
+ case AST.NodeType.ForStmt:
102
+ {
103
+ const s = stmt;
104
+ this.enterScope(); // For loop creates scope for iterators
105
+ s.iterators.forEach(i => this.currentScope.define(i.name, 'let', 'any', s.line || 0)); // Iterators can be anything in generic for
106
+ this.visitExpression(s.iterable);
107
+ // We can treat the body as part of this scope or nested.
108
+ // AST.Block usually suggests its own scope, but let's reuse this one or just visit statements.
109
+ // Since AST.Block logic below creates a NEW scope, we have: Scope(Iterators) -> Scope(Body). This is correct.
110
+ this.visitBlock(s.body);
111
+ this.exitScope();
112
+ }
113
+ break;
114
+ case AST.NodeType.RangeForStmt:
115
+ {
116
+ const s = stmt;
117
+ this.enterScope();
118
+ this.currentScope.define(s.counter.name, 'let', 'number', s.line || 0); // Range for is always number
119
+ this.visitExpression(s.start);
120
+ this.visitExpression(s.end);
121
+ if (s.step)
122
+ this.visitExpression(s.step);
123
+ this.visitBlock(s.body);
124
+ this.exitScope();
125
+ }
126
+ break;
127
+ // ... Handle other control flows that have blocks ...
128
+ case AST.NodeType.CommandStmt:
129
+ {
130
+ const s = stmt;
131
+ this.enterScope();
132
+ s.params.forEach(p => this.currentScope.define(p.name.name, 'param', 'string', s.line || 0)); // Command params are strings (usually)
133
+ this.visitBlock(s.body);
134
+ this.exitScope();
135
+ }
136
+ break;
137
+ case AST.NodeType.EventHandler:
138
+ {
139
+ const s = stmt;
140
+ this.enterScope();
141
+ s.params.forEach(p => this.currentScope.define(p.name.name, 'param', 'any', s.line || 0));
142
+ this.visitBlock(s.body);
143
+ this.exitScope();
144
+ }
145
+ break;
146
+ case AST.NodeType.TryCatchStmt:
147
+ {
148
+ const s = stmt;
149
+ this.visitBlock(s.tryBody);
150
+ this.enterScope();
151
+ this.currentScope.define(s.catchParam.name, 'let', 'any', s.line || 0);
152
+ this.visitBlock(s.catchBody);
153
+ this.exitScope();
154
+ }
155
+ break;
156
+ // Handle expression statements (standalone function calls like ProcessNumber("hello"))
157
+ case AST.NodeType.CallExpr:
158
+ this.visitExpression(stmt);
159
+ break;
160
+ // Basic fallback recursive visiting
161
+ default:
162
+ // Identify other nodes that contain blocks/expressions to visit
163
+ this.visitChildren(stmt);
164
+ }
165
+ }
166
+ visitChildren(node) {
167
+ if (!node || typeof node !== 'object')
168
+ return;
169
+ for (const key in node) {
170
+ if (key === 'kind')
171
+ continue;
172
+ const val = node[key];
173
+ if (Array.isArray(val)) {
174
+ val.forEach(v => {
175
+ if (v && typeof v === 'object' && 'kind' in v) {
176
+ if (v.kind.endsWith('Stmt') || v.kind === 'VariableDecl' || v.kind === 'Block') {
177
+ this.visitStatement(v);
178
+ }
179
+ else {
180
+ this.visitExpression(v);
181
+ }
182
+ }
183
+ });
184
+ }
185
+ else if (val && typeof val === 'object' && 'kind' in val) {
186
+ if (val.kind.endsWith('Stmt') || val.kind === 'VariableDecl' || val.kind === 'Block') {
187
+ this.visitStatement(val);
188
+ }
189
+ else {
190
+ this.visitExpression(val);
191
+ }
192
+ }
193
+ }
194
+ }
195
+ visitVariableDecl(decl) {
196
+ // Visit values first
197
+ if (decl.values) {
198
+ decl.values.forEach(v => this.visitExpression(v));
199
+ }
200
+ const kind = decl.scope === 'const' ? 'const' : 'let';
201
+ decl.names.forEach((n, index) => {
202
+ let type = 'any';
203
+ let customTypeName;
204
+ // 1. Try to get type from annotation
205
+ if (decl.typeAnnotations && decl.typeAnnotations[index]) {
206
+ const annotation = decl.typeAnnotations[index];
207
+ customTypeName = this.typeRegistry.has(annotation) ? annotation : undefined;
208
+ type = this.stringToType(annotation);
209
+ }
210
+ // 2. Try to infer from value if available
211
+ if (decl.values && decl.values[index]) {
212
+ const value = decl.values[index];
213
+ const inferred = this.inferType(value);
214
+ const annotation = decl.typeAnnotations && decl.typeAnnotations[index];
215
+ // If it's an array type (e.g., RobberyConfig[] or number[]) and value is a table, validate each element
216
+ if (annotation && annotation.endsWith('[]') && value.kind === AST.NodeType.TableLiteral) {
217
+ const elementTypeName = annotation.slice(0, -2); // Remove []
218
+ const expectedElementType = this.stringToType(elementTypeName);
219
+ const tableValue = value;
220
+ // Iterate through each element and validate
221
+ for (let i = 0; i < tableValue.fields.length; i++) {
222
+ const field = tableValue.fields[i];
223
+ // If element is a TableLiteral and element type is an interface, validate against interface
224
+ if (field.value.kind === AST.NodeType.TableLiteral && this.typeRegistry.has(elementTypeName)) {
225
+ this.validateTableAgainstType(field.value, elementTypeName, decl.line || 0);
226
+ }
227
+ else {
228
+ // For primitive types (number[], string[], etc.), check each element type
229
+ const actualType = this.inferType(field.value);
230
+ if (expectedElementType !== 'any' && actualType !== 'any' && expectedElementType !== actualType) {
231
+ throw new Error(`Type mismatch in array at line ${decl.line}: Element ${i + 1} expected '${elementTypeName}', got '${actualType}'`);
232
+ }
233
+ }
234
+ }
235
+ }
236
+ // If we have a custom type annotation and the value is a table (not array), validate it
237
+ else if (customTypeName && value.kind === AST.NodeType.TableLiteral) {
238
+ this.validateTableAgainstType(value, customTypeName, decl.line || 0);
239
+ }
240
+ // If we have an annotation, check compatibility
241
+ if (type !== 'any' && inferred !== 'any' && type !== inferred) {
242
+ throw new Error(`Type mismatch at line ${decl.line}: Expected '${type}', but got '${inferred}'`);
243
+ }
244
+ // If no annotation, keep type as 'any' (default)
245
+ // if (type === 'any' && (!decl.typeAnnotations || !decl.typeAnnotations[index])) {
246
+ // type = inferred;
247
+ // }
248
+ }
249
+ if (n.kind === AST.NodeType.Identifier) {
250
+ this.currentScope.define(n.name, kind, type, decl.line || 0);
251
+ }
252
+ else if (n.kind === AST.NodeType.ObjectDestructure) {
253
+ n.properties.forEach(p => {
254
+ this.currentScope.define(p.name, kind, 'any', decl.line || 0);
255
+ });
256
+ }
257
+ else if (n.kind === AST.NodeType.ArrayDestructure) {
258
+ n.elements.forEach(el => {
259
+ this.currentScope.define(el.name, kind, 'any', decl.line || 0);
260
+ });
261
+ }
262
+ });
263
+ }
264
+ visitAssignmentStmt(stmt) {
265
+ // Check RHS
266
+ stmt.values.forEach(v => this.visitExpression(v));
267
+ // Check LHS
268
+ stmt.targets.forEach((t, index) => {
269
+ // If it's a simple identifier, check const and TYPE
270
+ if (t.kind === AST.NodeType.Identifier) {
271
+ const name = t.name;
272
+ const sym = this.currentScope.lookup(name);
273
+ if (sym) {
274
+ if (sym.kind === 'const') {
275
+ throw new Error(`Assignment to constant variable '${name}' at line ${stmt.line}`);
276
+ }
277
+ // Type Checking
278
+ if (stmt.values[index]) {
279
+ const valueType = this.inferType(stmt.values[index]);
280
+ if (sym.type !== 'any' && valueType !== 'any' && sym.type !== valueType) {
281
+ throw new Error(`Type mismatch at line ${stmt.line}: Variable '${name}' is type '${sym.type}', but assigned '${valueType}'`);
282
+ }
283
+ }
284
+ }
285
+ }
286
+ // Handle member expression type checking
287
+ if (t.kind === AST.NodeType.MemberExpr) {
288
+ const member = t;
289
+ // 1. Strict Property Existence Check
290
+ if (member.object.kind === AST.NodeType.Identifier && !member.computed) {
291
+ const objName = member.object.name;
292
+ const sym = this.currentScope.lookup(objName);
293
+ const propName = member.property.name;
294
+ if (sym && sym.type !== 'any' && sym.type !== 'table') {
295
+ // Check if it's an interface or alias
296
+ let fields;
297
+ // Remove array suffix for type lookup if needed (though variable shouldn't be array if accessing prop directly)
298
+ const baseType = sym.type.replace('[]', '');
299
+ if (this.typeRegistry.has(baseType)) {
300
+ fields = this.typeRegistry.get(baseType);
301
+ }
302
+ else if (this.typeAliasRegistry.has(baseType)) {
303
+ const aliasStr = this.typeAliasRegistry.get(baseType);
304
+ if (aliasStr.startsWith('{')) {
305
+ fields = this.parseObjectLiteralType(aliasStr);
306
+ }
307
+ }
308
+ if (fields) {
309
+ const fieldExists = fields.some(f => f.name === propName);
310
+ if (!fieldExists) {
311
+ throw new Error(`Property '${propName}' does not exist on type '${sym.type}' at line ${stmt.line}`);
312
+ }
313
+ }
314
+ }
315
+ }
316
+ const memberKey = this.getMemberExprKey(t);
317
+ // If this assignment has a type annotation, register it
318
+ if (stmt.typeAnnotation && index === 0) {
319
+ const type = this.stringToType(stmt.typeAnnotation);
320
+ this.memberTypes.set(memberKey, type);
321
+ // Also check that the value matches the declared type
322
+ if (stmt.values[index]) {
323
+ const valueType = this.inferType(stmt.values[index]);
324
+ if (type !== 'any' && valueType !== 'any' && type !== valueType) {
325
+ throw new Error(`Type mismatch at line ${stmt.line}: '${memberKey}' is declared as '${stmt.typeAnnotation}', but assigned '${valueType}'`);
326
+ }
327
+ }
328
+ }
329
+ else {
330
+ // Check if this member has a registered type
331
+ const registeredType = this.memberTypes.get(memberKey);
332
+ if (registeredType && stmt.values[index]) {
333
+ const valueType = this.inferType(stmt.values[index]);
334
+ if (registeredType !== 'any' && valueType !== 'any' && registeredType !== valueType) {
335
+ throw new Error(`Type mismatch at line ${stmt.line}: '${memberKey}' is type '${registeredType}', but assigned '${valueType}'`);
336
+ }
337
+ }
338
+ }
339
+ }
340
+ this.visitExpression(t);
341
+ });
342
+ }
343
+ // Helper to get a string key for a member expression (e.g., "Config.Locale")
344
+ getMemberExprKey(expr) {
345
+ let parts = [];
346
+ let current = expr;
347
+ while (current.kind === AST.NodeType.MemberExpr) {
348
+ const memberExpr = current;
349
+ if (memberExpr.property.kind === AST.NodeType.Identifier) {
350
+ parts.unshift(memberExpr.property.name);
351
+ }
352
+ current = memberExpr.object;
353
+ }
354
+ if (current.kind === AST.NodeType.Identifier) {
355
+ parts.unshift(current.name);
356
+ }
357
+ return parts.join('.');
358
+ }
359
+ visitCompoundAssignment(stmt) {
360
+ this.visitExpression(stmt.value);
361
+ if (stmt.target.kind === AST.NodeType.Identifier) {
362
+ const name = stmt.target.name;
363
+ const sym = this.currentScope.lookup(name);
364
+ if (sym && sym.kind === 'const') {
365
+ throw new Error(`Assignment to constant variable '${name}' at line ${stmt.line}`);
366
+ }
367
+ }
368
+ this.visitExpression(stmt.target);
369
+ }
370
+ visitFunctionDecl(decl) {
371
+ // Name is defined in CURRENT scope
372
+ let funcName;
373
+ if (decl.name && decl.name.kind === AST.NodeType.Identifier) {
374
+ funcName = decl.name.name;
375
+ this.currentScope.define(funcName, 'const', 'function', decl.line || 0);
376
+ }
377
+ // Register function signature for call validation
378
+ const paramTypes = [];
379
+ decl.params.forEach(p => {
380
+ let type = 'any';
381
+ if (p.typeAnnotation) {
382
+ type = this.stringToType(p.typeAnnotation);
383
+ }
384
+ paramTypes.push({ name: p.name.name, type });
385
+ });
386
+ if (funcName) {
387
+ this.functionRegistry.set(funcName, { params: paramTypes });
388
+ }
389
+ this.enterScope();
390
+ decl.params.forEach(p => {
391
+ // Parse param type annotation
392
+ let type = 'any';
393
+ if (p.typeAnnotation) {
394
+ type = this.stringToType(p.typeAnnotation);
395
+ }
396
+ this.currentScope.define(p.name.name, 'param', type, decl.line || 0);
397
+ if (p.defaultValue) {
398
+ const defType = this.inferType(p.defaultValue);
399
+ if (type !== 'any' && defType !== 'any' && type !== defType) {
400
+ throw new Error(`Type mismatch in parameter default value at line ${decl.line}`);
401
+ }
402
+ this.visitExpression(p.defaultValue);
403
+ }
404
+ });
405
+ this.visitBlock(decl.body);
406
+ this.exitScope();
407
+ }
408
+ visitBlock(block) {
409
+ this.enterScope();
410
+ block.statements.forEach(s => this.visitStatement(s));
411
+ this.exitScope();
412
+ }
413
+ visitExpression(expr) {
414
+ // Mostly just traversing to find nested scopes in functions/arrows
415
+ if (!expr)
416
+ return;
417
+ // Check for undefined variable usage
418
+ if (expr.kind === AST.NodeType.Identifier) {
419
+ const id = expr;
420
+ const sym = this.currentScope.lookup(id.name);
421
+ // Skip globals/builtins - only check if it looks like a user variable
422
+ // FiveM/Lua has many globals, so we only warn for simple lowercase identifiers
423
+ // that are not common Lua globals
424
+ const luaGlobals = ['print', 'type', 'tostring', 'tonumber', 'pairs', 'ipairs',
425
+ 'next', 'error', 'assert', 'pcall', 'xpcall', 'require',
426
+ 'table', 'string', 'math', 'os', 'io', 'coroutine', 'debug',
427
+ 'setmetatable', 'getmetatable', 'rawget', 'rawset', 'rawequal',
428
+ 'collectgarbage', 'dofile', 'load', 'loadfile', 'select',
429
+ 'unpack', '_G', '_VERSION', 'true', 'false', 'nil',
430
+ // FiveM common globals
431
+ 'Citizen', 'CreateThread', 'Wait', 'GetGameTimer', 'vector3',
432
+ 'vector2', 'vector4', 'GetCurrentResourceName', 'exports',
433
+ 'PlayerPedId', 'GetPlayerPed', 'GetEntityCoords', 'TriggerEvent',
434
+ 'TriggerServerEvent', 'TriggerClientEvent', 'RegisterNetEvent',
435
+ 'AddEventHandler', 'RegisterCommand', 'IsControlJustPressed',
436
+ 'AddEventHandler', 'RegisterCommand', 'LocalPlayer', 'PlayerId',
437
+ 'GetPlayerServerId', 'source', 'SetEntityHealth', 'ESX', 'QBCore',
438
+ // Additional FiveM/Lua globals
439
+ 'Config', 'Framework', 'MySQL', 'lib', 'cache', 'json',
440
+ 'NetworkGetNetworkIdFromEntity', 'NetworkGetEntityFromNetworkId',
441
+ 'GetPlayerPed', 'GetVehiclePedIsIn', 'IsPedInAnyVehicle',
442
+ 'SetPedCanRagdoll', 'SetTimeout', 'SetInterval', 'ClearTimeout',
443
+ 'GetHashKey', 'IsModelValid', 'RequestModel', 'HasModelLoaded'];
444
+ if (!sym && !luaGlobals.includes(id.name)) {
445
+ // Check if it's a type name (not a variable)
446
+ if (!this.typeRegistry.has(id.name) && !this.typeAliasRegistry.has(id.name)) {
447
+ throw new Error(`Undefined variable '${id.name}' at line ${id.line ?? 0}`);
448
+ }
449
+ }
450
+ return;
451
+ }
452
+ if (expr.kind === AST.NodeType.ArrowFunc) {
453
+ const arrow = expr;
454
+ this.enterScope();
455
+ arrow.params.forEach(p => {
456
+ let type = 'any';
457
+ if (p.typeAnnotation) {
458
+ type = this.stringToType(p.typeAnnotation);
459
+ }
460
+ this.currentScope.define(p.name.name, 'param', type, expr.line || 0);
461
+ if (p.defaultValue)
462
+ this.visitExpression(p.defaultValue);
463
+ });
464
+ if ('kind' in arrow.body && arrow.body.kind === AST.NodeType.Block) {
465
+ this.visitBlock(arrow.body);
466
+ }
467
+ else {
468
+ this.visitExpression(arrow.body);
469
+ }
470
+ this.exitScope();
471
+ return;
472
+ }
473
+ if (expr.kind === AST.NodeType.FunctionDecl) {
474
+ this.visitFunctionDecl(expr);
475
+ return;
476
+ }
477
+ // Handle TableLiteral - recurse into field values only
478
+ if (expr.kind === AST.NodeType.TableLiteral) {
479
+ const table = expr;
480
+ for (const field of table.fields) {
481
+ // Don't check keys - they are field names, not variable references
482
+ // Only check computed keys like [expression] or string keys
483
+ if (field.key && field.key.kind !== AST.NodeType.Identifier) {
484
+ this.visitExpression(field.key);
485
+ }
486
+ this.visitExpression(field.value);
487
+ }
488
+ return;
489
+ }
490
+ // Handle BinaryExpr
491
+ if (expr.kind === AST.NodeType.BinaryExpr) {
492
+ const bin = expr;
493
+ this.visitExpression(bin.left);
494
+ this.visitExpression(bin.right);
495
+ return;
496
+ }
497
+ // Handle CallExpr - recurse into callee and args, validate argument types
498
+ if (expr.kind === AST.NodeType.CallExpr) {
499
+ const call = expr;
500
+ this.visitExpression(call.callee);
501
+ // Validate argument types if callee is a known function
502
+ if (call.callee.kind === AST.NodeType.Identifier) {
503
+ const funcName = call.callee.name;
504
+ const funcInfo = this.functionRegistry.get(funcName);
505
+ if (funcInfo) {
506
+ // Check each argument against expected param type
507
+ for (let i = 0; i < call.args.length && i < funcInfo.params.length; i++) {
508
+ const expectedType = funcInfo.params[i].type;
509
+ const argType = this.inferType(call.args[i]);
510
+ if (expectedType !== 'any' && argType !== 'any' && expectedType !== argType) {
511
+ throw new Error(`Type mismatch in function call '${funcName}' at line ${call.line || 0}: Argument ${i + 1} expects '${expectedType}', got '${argType}'`);
512
+ }
513
+ }
514
+ }
515
+ }
516
+ for (const arg of call.args) {
517
+ this.visitExpression(arg);
518
+ }
519
+ return;
520
+ }
521
+ // Handle MemberExpr - only check the object, not property (obj.property)
522
+ if (expr.kind === AST.NodeType.MemberExpr) {
523
+ const member = expr;
524
+ this.visitExpression(member.object);
525
+ // Don't check member.property - it's a field access, not a variable
526
+ return;
527
+ }
528
+ }
529
+ inferType(expr) {
530
+ if (!expr)
531
+ return 'any';
532
+ switch (expr.kind) {
533
+ case AST.NodeType.NumberLiteral: return 'number';
534
+ case AST.NodeType.StringLiteral: return 'string';
535
+ case AST.NodeType.InterpolatedString: return 'string';
536
+ case AST.NodeType.BooleanLiteral: return 'boolean';
537
+ case AST.NodeType.NilLiteral: return 'nil';
538
+ case AST.NodeType.VectorLiteral: return 'vector';
539
+ case AST.NodeType.TableLiteral: return 'table';
540
+ case AST.NodeType.FunctionDecl: return 'function';
541
+ case AST.NodeType.ArrowFunc: return 'function';
542
+ case AST.NodeType.Identifier:
543
+ const sym = this.currentScope.lookup(expr.name);
544
+ return sym ? sym.type : 'any';
545
+ // TODO: Improve binary expr inference (e.g. number + number = number)
546
+ case AST.NodeType.BinaryExpr: return 'any';
547
+ default:
548
+ return 'any';
549
+ }
550
+ }
551
+ stringToType(str) {
552
+ // Array types are tables
553
+ if (str.endsWith('[]'))
554
+ return 'table';
555
+ // Remove array notation for basic type check: string[] -> string
556
+ const baseStr = str.replace(/\[\]/g, '');
557
+ switch (baseStr) {
558
+ case 'string': return 'string';
559
+ case 'number': return 'number';
560
+ case 'boolean': return 'boolean';
561
+ case 'vector':
562
+ case 'vector2':
563
+ case 'vector3':
564
+ case 'vector4': return 'vector';
565
+ case 'table': return 'table';
566
+ case 'function': return 'function';
567
+ case 'any': return 'any';
568
+ default:
569
+ // Check if it's a registered interface
570
+ if (this.typeRegistry.has(baseStr)) {
571
+ return 'table';
572
+ }
573
+ // Check if it's a type alias
574
+ if (this.typeAliasRegistry.has(baseStr)) {
575
+ // If alias resolves to a primitive, return that. If object literal, return 'table'
576
+ const aliased = this.typeAliasRegistry.get(baseStr);
577
+ if (aliased.startsWith('{'))
578
+ return 'table';
579
+ return this.stringToType(aliased);
580
+ }
581
+ return 'any';
582
+ }
583
+ }
584
+ visitTypeAliasDecl(decl) {
585
+ this.typeAliasRegistry.set(decl.name.name, decl.type);
586
+ // If it's an object literal type, validate field types
587
+ if (decl.type.startsWith('{')) {
588
+ const fields = this.parseObjectLiteralType(decl.type);
589
+ for (const field of fields) {
590
+ this.validateTypeExists(field.type, decl.name.line ?? 0, `type '${decl.name.name}'`);
591
+ }
592
+ }
593
+ else {
594
+ // It's a simple alias, validate the aliased type exists
595
+ this.validateTypeExists(decl.type, decl.name.line ?? 0, `type '${decl.name.name}'`);
596
+ }
597
+ }
598
+ visitTypeDecl(decl) {
599
+ const typeName = decl.name.name;
600
+ // Register the type
601
+ this.typeRegistry.set(typeName, decl.fields);
602
+ // Validate that all field types exist
603
+ for (const field of decl.fields) {
604
+ this.validateTypeExists(field.type, decl.name.line ?? 0, `interface '${typeName}'`);
605
+ }
606
+ }
607
+ validateTypeExists(typeStr, line, context) {
608
+ // Remove array brackets for base type check
609
+ const baseType = typeStr.replace(/\[\]/g, '').trim();
610
+ // Skip object literal types (they are inline definitions)
611
+ if (baseType.startsWith('{'))
612
+ return;
613
+ // Check primitive types (including FiveM vector types)
614
+ const primitives = ['string', 'number', 'boolean', 'nil', 'any', 'table', 'function', 'vector', 'void', 'vector2', 'vector3', 'vector4'];
615
+ if (primitives.includes(baseType))
616
+ return;
617
+ // Check type registry (interfaces)
618
+ if (this.typeRegistry.has(baseType))
619
+ return;
620
+ // Check type alias registry
621
+ if (this.typeAliasRegistry.has(baseType))
622
+ return;
623
+ // Type not found!
624
+ throw new Error(`Unknown type '${baseType}' in ${context} at line ${line}`);
625
+ }
626
+ validateTableAgainstType(table, typeName, line) {
627
+ let fields;
628
+ // Remove array brackets if present (e.g. validating a single object against MyType[])
629
+ // This shouldn't happen often in this function call context but good for safety
630
+ typeName = typeName.replace(/\[\]/g, '');
631
+ // 1. Check Interface Registry
632
+ if (this.typeRegistry.has(typeName)) {
633
+ fields = this.typeRegistry.get(typeName);
634
+ }
635
+ // 2. Check Type Aliases (specifically object literal aliases)
636
+ else if (this.typeAliasRegistry.has(typeName)) {
637
+ const aliasStr = this.typeAliasRegistry.get(typeName);
638
+ if (aliasStr.startsWith('{')) {
639
+ fields = this.parseObjectLiteralType(aliasStr);
640
+ }
641
+ }
642
+ if (!fields)
643
+ return; // Unknown type, skip validation
644
+ const providedFields = new Map();
645
+ for (const field of table.fields) {
646
+ if (field.key && field.key.kind === AST.NodeType.Identifier) {
647
+ providedFields.set(field.key.name, field.value);
648
+ }
649
+ else if (field.key && field.key.kind === AST.NodeType.StringLiteral) {
650
+ providedFields.set(field.key.value, field.value);
651
+ }
652
+ }
653
+ // Check each expected field
654
+ for (const expected of fields) {
655
+ const provided = providedFields.get(expected.name);
656
+ if (!provided) {
657
+ throw new Error(`Type '${typeName}' requires field '${expected.name}' at line ${line}`);
658
+ }
659
+ const providedType = this.inferType(provided);
660
+ const expectedType = this.stringToType(expected.type);
661
+ if (expectedType !== 'any' && providedType !== 'any' && expectedType !== providedType) {
662
+ throw new Error(`Type mismatch: field '${expected.name}' should be '${expected.type}', got '${providedType}' at line ${line}`);
663
+ }
664
+ }
665
+ }
666
+ parseObjectLiteralType(typeStr) {
667
+ const content = typeStr.trim().slice(1, -1);
668
+ if (!content)
669
+ return [];
670
+ const fields = [];
671
+ const parts = content.split(/,|;/);
672
+ for (const part of parts) {
673
+ if (!part.trim())
674
+ continue;
675
+ const [key, val] = part.split(':').map(s => s.trim());
676
+ if (key && val) {
677
+ fields.push({ name: key.replace('?', ''), type: val });
678
+ }
679
+ }
680
+ return fields;
681
+ }
682
+ }