skittles 1.2.7 → 1.3.0

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 (45) hide show
  1. package/README.md +1 -1
  2. package/dist/commands/compile.d.ts +1 -0
  3. package/dist/commands/compile.d.ts.map +1 -1
  4. package/dist/commands/compile.js +51 -1
  5. package/dist/commands/compile.js.map +1 -1
  6. package/dist/commands/init.d.ts.map +1 -1
  7. package/dist/commands/init.js +2 -0
  8. package/dist/commands/init.js.map +1 -1
  9. package/dist/commands/test.d.ts +7 -0
  10. package/dist/commands/test.d.ts.map +1 -0
  11. package/dist/commands/test.js +60 -0
  12. package/dist/commands/test.js.map +1 -0
  13. package/dist/compiler/analysis.d.ts +7 -0
  14. package/dist/compiler/analysis.d.ts.map +1 -0
  15. package/dist/compiler/analysis.js +234 -0
  16. package/dist/compiler/analysis.js.map +1 -0
  17. package/dist/compiler/codegen.d.ts +10 -1
  18. package/dist/compiler/codegen.d.ts.map +1 -1
  19. package/dist/compiler/codegen.js +389 -22
  20. package/dist/compiler/codegen.js.map +1 -1
  21. package/dist/compiler/compiler.d.ts +1 -0
  22. package/dist/compiler/compiler.d.ts.map +1 -1
  23. package/dist/compiler/compiler.js +90 -6
  24. package/dist/compiler/compiler.js.map +1 -1
  25. package/dist/compiler/parser.d.ts +7 -1
  26. package/dist/compiler/parser.d.ts.map +1 -1
  27. package/dist/compiler/parser.js +731 -47
  28. package/dist/compiler/parser.js.map +1 -1
  29. package/dist/config/config.d.ts.map +1 -1
  30. package/dist/config/config.js +2 -0
  31. package/dist/config/config.js.map +1 -1
  32. package/dist/exports.d.ts +13 -2
  33. package/dist/exports.d.ts.map +1 -1
  34. package/dist/exports.js.map +1 -1
  35. package/dist/index.js +13 -3
  36. package/dist/index.js.map +1 -1
  37. package/dist/testing.d.ts +93 -0
  38. package/dist/testing.d.ts.map +1 -0
  39. package/dist/testing.js +143 -0
  40. package/dist/testing.js.map +1 -0
  41. package/dist/types/index.d.ts +50 -2
  42. package/dist/types/index.d.ts.map +1 -1
  43. package/dist/types/index.js +1 -0
  44. package/dist/types/index.js.map +1 -1
  45. package/package.json +2 -2
@@ -2,9 +2,47 @@ import ts from "typescript";
2
2
  // Module-level registries, populated during parse()
3
3
  let _knownStructs = new Map();
4
4
  let _knownContractInterfaces = new Set();
5
- let _knownEnums = new Set();
5
+ let _knownEnums = new Map();
6
6
  let _knownCustomErrors = new Set();
7
7
  let _fileConstants = new Map();
8
+ let _currentSourceFile = null;
9
+ // String type tracking for string.length and string comparison transforms
10
+ let _currentVarTypes = new Map();
11
+ let _currentStringNames = new Set();
12
+ function getSourceLine(node) {
13
+ if (!_currentSourceFile)
14
+ return undefined;
15
+ return _currentSourceFile.getLineAndCharacterOfPosition(node.getStart()).line + 1; // 1-based
16
+ }
17
+ function setupStringTracking(parameters, varTypes) {
18
+ _currentVarTypes = varTypes;
19
+ _currentStringNames = new Set();
20
+ for (const param of parameters) {
21
+ if (param.type.kind === "string") {
22
+ _currentStringNames.add(param.name);
23
+ }
24
+ }
25
+ }
26
+ function isStringExpr(expr) {
27
+ if (expr.kind === "string-literal")
28
+ return true;
29
+ if (expr.kind === "identifier" && _currentStringNames.has(expr.name))
30
+ return true;
31
+ if (expr.kind === "property-access" &&
32
+ expr.object.kind === "identifier" &&
33
+ expr.object.name === "this") {
34
+ const type = _currentVarTypes.get(expr.property);
35
+ return type?.kind === "string";
36
+ }
37
+ if (expr.kind === "call" &&
38
+ expr.callee.kind === "property-access" &&
39
+ expr.callee.object.kind === "identifier" &&
40
+ expr.callee.object.name === "string" &&
41
+ expr.callee.property === "concat") {
42
+ return true;
43
+ }
44
+ return false;
45
+ }
8
46
  // ============================================================
9
47
  // Main entry
10
48
  // ============================================================
@@ -23,7 +61,7 @@ export function collectTypes(source, filePath) {
23
61
  const prevEnums = _knownEnums;
24
62
  const prevInterfaces = _knownContractInterfaces;
25
63
  _knownStructs = structs;
26
- _knownEnums = new Set();
64
+ _knownEnums = new Map();
27
65
  _knownContractInterfaces = new Set();
28
66
  ts.forEachChild(sourceFile, (node) => {
29
67
  if (ts.isTypeAliasDeclaration(node) && node.name && ts.isTypeLiteralNode(node.type)) {
@@ -47,6 +85,7 @@ export function collectTypes(source, filePath) {
47
85
  }
48
86
  export function parse(source, filePath, externalTypes, externalFunctions) {
49
87
  const sourceFile = ts.createSourceFile(filePath, source, ts.ScriptTarget.Latest, true);
88
+ _currentSourceFile = sourceFile;
50
89
  const structs = new Map();
51
90
  const enums = new Map();
52
91
  const contractInterfaces = new Map();
@@ -79,7 +118,7 @@ export function parse(source, filePath, externalTypes, externalFunctions) {
79
118
  }
80
119
  });
81
120
  _knownStructs = structs;
82
- _knownEnums = new Set(enums.keys());
121
+ _knownEnums = new Map(enums);
83
122
  // Second pass: parse interfaces (may reference struct/enum types collected above)
84
123
  ts.forEachChild(sourceFile, (node) => {
85
124
  if (ts.isInterfaceDeclaration(node) && node.name) {
@@ -139,6 +178,34 @@ export function parse(source, filePath, externalTypes, externalFunctions) {
139
178
  }
140
179
  }
141
180
  });
181
+ // Post-process: infer overrides for abstract method implementations
182
+ const abstractMethodsByContract = new Map();
183
+ for (const contract of contracts) {
184
+ if (contract.isAbstract) {
185
+ const abstractMethods = new Set();
186
+ for (const fn of contract.functions) {
187
+ if (fn.isAbstract) {
188
+ abstractMethods.add(fn.name);
189
+ }
190
+ }
191
+ if (abstractMethods.size > 0) {
192
+ abstractMethodsByContract.set(contract.name, abstractMethods);
193
+ }
194
+ }
195
+ }
196
+ for (const contract of contracts) {
197
+ for (const parentName of contract.inherits) {
198
+ const abstractMethods = abstractMethodsByContract.get(parentName);
199
+ if (!abstractMethods)
200
+ continue;
201
+ for (const fn of contract.functions) {
202
+ if (abstractMethods.has(fn.name) && !fn.isOverride) {
203
+ fn.isOverride = true;
204
+ fn.isVirtual = false;
205
+ }
206
+ }
207
+ }
208
+ }
142
209
  return contracts;
143
210
  }
144
211
  /**
@@ -156,7 +223,7 @@ export function collectFunctions(source, filePath) {
156
223
  const prevEnums = _knownEnums;
157
224
  const prevInterfaces = _knownContractInterfaces;
158
225
  _knownStructs = new Map();
159
- _knownEnums = new Set();
226
+ _knownEnums = new Map();
160
227
  _knownContractInterfaces = new Set();
161
228
  ts.forEachChild(sourceFile, (node) => {
162
229
  if (ts.isFunctionDeclaration(node) && node.name && node.body) {
@@ -180,6 +247,21 @@ export function collectFunctions(source, filePath) {
180
247
  _knownContractInterfaces = prevInterfaces;
181
248
  return { functions, constants };
182
249
  }
250
+ /**
251
+ * Pre-scan a source file to collect the names of all contract classes
252
+ * (top-level class declarations that do not extend Error).
253
+ * Used by the compiler to track which file defines each contract.
254
+ */
255
+ export function collectClassNames(source, filePath) {
256
+ const sourceFile = ts.createSourceFile(filePath, source, ts.ScriptTarget.Latest, true);
257
+ const names = [];
258
+ ts.forEachChild(sourceFile, (node) => {
259
+ if (ts.isClassDeclaration(node) && node.name && !extendsError(node)) {
260
+ names.push(node.name.text);
261
+ }
262
+ });
263
+ return names;
264
+ }
183
265
  function parseArrayDestructuring(pattern, initializer, varTypes) {
184
266
  const statements = [];
185
267
  if (ts.isArrayLiteralExpression(initializer)) {
@@ -233,9 +315,140 @@ function parseArrayDestructuring(pattern, initializer, varTypes) {
233
315
  }
234
316
  return statements;
235
317
  }
318
+ function parseObjectDestructuring(pattern, initializer, varTypes, decl) {
319
+ const statements = [];
320
+ if (ts.isObjectLiteralExpression(initializer)) {
321
+ // Direct object literal: const { a, b } = { a: 1, b: 2 }
322
+ const propMap = new Map();
323
+ for (const prop of initializer.properties) {
324
+ if (ts.isPropertyAssignment(prop) && ts.isIdentifier(prop.name)) {
325
+ propMap.set(prop.name.text, prop.initializer);
326
+ }
327
+ }
328
+ for (const elem of pattern.elements) {
329
+ if (ts.isBindingElement(elem) && ts.isIdentifier(elem.name)) {
330
+ const name = elem.name.text;
331
+ const propName = elem.propertyName && ts.isIdentifier(elem.propertyName)
332
+ ? elem.propertyName.text
333
+ : name;
334
+ const init = propMap.has(propName)
335
+ ? parseExpression(propMap.get(propName))
336
+ : undefined;
337
+ const type = init ? inferType(init, varTypes) : undefined;
338
+ statements.push({
339
+ kind: "variable-declaration",
340
+ name,
341
+ type,
342
+ initializer: init,
343
+ });
344
+ }
345
+ }
346
+ return statements;
347
+ }
348
+ // Non-literal initializer: const { amount, timestamp } = this.getStakeInfo(account)
349
+ // Try to resolve struct type for a temp variable approach
350
+ let structType;
351
+ // Check explicit type annotation
352
+ if (decl.type) {
353
+ structType = parseType(decl.type);
354
+ }
355
+ // If no explicit type, try to find it from a this.method() call
356
+ if (!structType && ts.isCallExpression(initializer)) {
357
+ const callee = initializer.expression;
358
+ if (ts.isPropertyAccessExpression(callee) &&
359
+ callee.expression.kind === ts.SyntaxKind.ThisKeyword) {
360
+ const methodName = callee.name.text;
361
+ const cls = findEnclosingClass(decl);
362
+ if (cls) {
363
+ const retTypeNode = findMethodReturnType(cls, methodName);
364
+ if (retTypeNode) {
365
+ structType = parseType(retTypeNode);
366
+ }
367
+ }
368
+ }
369
+ }
370
+ const initExpr = parseExpression(initializer);
371
+ if (structType?.kind === "struct" && structType.structName) {
372
+ // Temp variable + field accesses
373
+ const tempName = `_${structType.structName.charAt(0).toLowerCase()}${structType.structName.slice(1)}`;
374
+ statements.push({
375
+ kind: "variable-declaration",
376
+ name: tempName,
377
+ type: structType,
378
+ initializer: initExpr,
379
+ });
380
+ const fieldMap = new Map();
381
+ if (structType.structFields) {
382
+ for (const f of structType.structFields) {
383
+ fieldMap.set(f.name, f.type);
384
+ }
385
+ }
386
+ for (const elem of pattern.elements) {
387
+ if (ts.isBindingElement(elem) && ts.isIdentifier(elem.name)) {
388
+ const name = elem.name.text;
389
+ const propName = elem.propertyName && ts.isIdentifier(elem.propertyName)
390
+ ? elem.propertyName.text
391
+ : name;
392
+ statements.push({
393
+ kind: "variable-declaration",
394
+ name,
395
+ type: fieldMap.get(propName),
396
+ initializer: {
397
+ kind: "property-access",
398
+ object: { kind: "identifier", name: tempName },
399
+ property: propName,
400
+ },
401
+ });
402
+ }
403
+ }
404
+ }
405
+ else {
406
+ // Fallback: property-access expressions directly on the initializer
407
+ for (const elem of pattern.elements) {
408
+ if (ts.isBindingElement(elem) && ts.isIdentifier(elem.name)) {
409
+ const name = elem.name.text;
410
+ const propName = elem.propertyName && ts.isIdentifier(elem.propertyName)
411
+ ? elem.propertyName.text
412
+ : name;
413
+ statements.push({
414
+ kind: "variable-declaration",
415
+ name,
416
+ type: undefined,
417
+ initializer: {
418
+ kind: "property-access",
419
+ object: initExpr,
420
+ property: propName,
421
+ },
422
+ });
423
+ }
424
+ }
425
+ }
426
+ return statements;
427
+ }
428
+ function findEnclosingClass(node) {
429
+ let current = node.parent;
430
+ while (current) {
431
+ if (ts.isClassDeclaration(current))
432
+ return current;
433
+ current = current.parent;
434
+ }
435
+ return undefined;
436
+ }
437
+ function findMethodReturnType(cls, methodName) {
438
+ for (const member of cls.members) {
439
+ if (ts.isMethodDeclaration(member) &&
440
+ member.name &&
441
+ ts.isIdentifier(member.name) &&
442
+ member.name.text === methodName) {
443
+ return member.type;
444
+ }
445
+ }
446
+ return undefined;
447
+ }
236
448
  function parseStandaloneFunction(node, varTypes, eventNames) {
237
449
  const name = node.name ? node.name.text : "unknown";
238
450
  const parameters = node.parameters.map(parseParameter);
451
+ setupStringTracking(parameters, varTypes);
239
452
  const returnType = node.type ? parseType(node.type) : null;
240
453
  const body = node.body ? parseBlock(node.body, varTypes, eventNames) : [];
241
454
  const stateMutability = inferStateMutability(body);
@@ -248,12 +461,14 @@ function parseStandaloneFunction(node, varTypes, eventNames) {
248
461
  isVirtual: false,
249
462
  isOverride: false,
250
463
  body,
464
+ sourceLine: getSourceLine(node),
251
465
  };
252
466
  }
253
467
  function parseStandaloneArrowFunction(decl, varTypes, eventNames) {
254
468
  const name = ts.isIdentifier(decl.name) ? decl.name.text : "unknown";
255
469
  const arrow = decl.initializer;
256
470
  const parameters = arrow.parameters.map(parseParameter);
471
+ setupStringTracking(parameters, varTypes);
257
472
  const returnType = arrow.type ? parseType(arrow.type) : null;
258
473
  let body = [];
259
474
  if (arrow.body) {
@@ -274,6 +489,7 @@ function parseStandaloneArrowFunction(decl, varTypes, eventNames) {
274
489
  isVirtual: false,
275
490
  isOverride: false,
276
491
  body,
492
+ sourceLine: getSourceLine(decl),
277
493
  };
278
494
  }
279
495
  function extendsError(node) {
@@ -330,6 +546,7 @@ function parseInterfaceAsContractInterface(node) {
330
546
  // ============================================================
331
547
  function parseClass(node, filePath, knownStructs = new Map(), knownEnums = new Map(), knownContractInterfaces = new Map(), knownCustomErrors = new Map(), fileFunctions = [], fileConstants = new Map()) {
332
548
  const name = node.name?.text ?? "Unknown";
549
+ const isAbstract = hasModifier(node.modifiers, ts.SyntaxKind.AbstractKeyword);
333
550
  const variables = [];
334
551
  const functions = [];
335
552
  const events = [];
@@ -413,9 +630,48 @@ function parseClass(node, filePath, knownStructs = new Map(), knownEnums = new M
413
630
  functions.push(parseSetAccessor(member, varTypes, eventNames));
414
631
  }
415
632
  }
416
- // Inject file level standalone functions as internal helpers
633
+ // Inject only file level standalone functions that are actually used by this contract.
634
+ // First, collect function names called directly by class methods, constructor, and variable initializers.
635
+ const fileFnNames = new Set(fileFunctions.map((f) => f.name));
636
+ const usedFileFnNames = new Set();
637
+ const collectFnCalls = (stmts) => {
638
+ walkStatements(stmts, (expr) => {
639
+ if (expr.kind === "call" && expr.callee.kind === "identifier" && fileFnNames.has(expr.callee.name)) {
640
+ usedFileFnNames.add(expr.callee.name);
641
+ }
642
+ });
643
+ };
644
+ for (const f of functions)
645
+ collectFnCalls(f.body);
646
+ if (ctor)
647
+ collectFnCalls(ctor.body);
648
+ for (const v of variables) {
649
+ if (v.initialValue) {
650
+ walkStatements([{ kind: "expression", expression: v.initialValue }], (expr) => {
651
+ if (expr.kind === "call" && expr.callee.kind === "identifier" && fileFnNames.has(expr.callee.name)) {
652
+ usedFileFnNames.add(expr.callee.name);
653
+ }
654
+ });
655
+ }
656
+ }
657
+ // Transitively include file functions called by other used file functions
658
+ let fnChanged = true;
659
+ while (fnChanged) {
660
+ fnChanged = false;
661
+ for (const fn of fileFunctions) {
662
+ if (!usedFileFnNames.has(fn.name))
663
+ continue;
664
+ walkStatements(fn.body, (expr) => {
665
+ if (expr.kind === "call" && expr.callee.kind === "identifier" && fileFnNames.has(expr.callee.name) && !usedFileFnNames.has(expr.callee.name)) {
666
+ usedFileFnNames.add(expr.callee.name);
667
+ fnChanged = true;
668
+ }
669
+ });
670
+ }
671
+ }
417
672
  for (const fn of fileFunctions) {
418
- // Avoid duplicates if a class method already has the same name
673
+ if (!usedFileFnNames.has(fn.name))
674
+ continue;
419
675
  if (!functions.some((f) => f.name === fn.name)) {
420
676
  functions.push(fn);
421
677
  }
@@ -436,13 +692,92 @@ function parseClass(node, filePath, knownStructs = new Map(), knownEnums = new M
436
692
  }
437
693
  }
438
694
  }
695
+ // Determine which structs and enums are actually referenced by this contract
696
+ const usedStructNames = new Set();
697
+ const usedEnumNames = new Set();
698
+ const collectTypeRef = (type) => {
699
+ if (!type)
700
+ return;
701
+ if (type.kind === "struct" && type.structName)
702
+ usedStructNames.add(type.structName);
703
+ if (type.kind === "enum" && type.structName)
704
+ usedEnumNames.add(type.structName);
705
+ if (type.keyType)
706
+ collectTypeRef(type.keyType);
707
+ if (type.valueType)
708
+ collectTypeRef(type.valueType);
709
+ if (type.tupleTypes)
710
+ for (const t of type.tupleTypes)
711
+ collectTypeRef(t);
712
+ // Include struct field types transitively
713
+ if (type.structFields)
714
+ for (const f of type.structFields)
715
+ collectTypeRef(f.type);
716
+ };
717
+ const collectBodyTypeRefs = (stmts) => {
718
+ walkStatements(stmts, (expr) => {
719
+ // Enum member access: Color.Red
720
+ if (expr.kind === "property-access" && expr.object.kind === "identifier" && knownEnums.has(expr.object.name)) {
721
+ usedEnumNames.add(expr.object.name);
722
+ }
723
+ // Type arguments on call expressions (e.g. contract interface casts)
724
+ if (expr.kind === "call" && expr.typeArgs) {
725
+ for (const t of expr.typeArgs)
726
+ collectTypeRef(t);
727
+ }
728
+ }, (stmt) => {
729
+ if (stmt.kind === "variable-declaration" && stmt.type)
730
+ collectTypeRef(stmt.type);
731
+ if (stmt.kind === "try-catch" && stmt.returnType)
732
+ collectTypeRef(stmt.returnType);
733
+ });
734
+ };
735
+ for (const v of variables) {
736
+ collectTypeRef(v.type);
737
+ if (v.initialValue)
738
+ collectBodyTypeRefs([{ kind: "expression", expression: v.initialValue }]);
739
+ }
740
+ for (const f of functions) {
741
+ for (const p of f.parameters)
742
+ collectTypeRef(p.type);
743
+ if (f.returnType)
744
+ collectTypeRef(f.returnType);
745
+ collectBodyTypeRefs(f.body);
746
+ }
747
+ if (ctor) {
748
+ for (const p of ctor.parameters)
749
+ collectTypeRef(p.type);
750
+ collectBodyTypeRefs(ctor.body);
751
+ }
752
+ for (const e of events) {
753
+ for (const p of e.parameters)
754
+ collectTypeRef(p.type);
755
+ }
756
+ // Transitively include structs whose fields reference other structs/enums
757
+ let typeChanged = true;
758
+ while (typeChanged) {
759
+ typeChanged = false;
760
+ for (const sName of usedStructNames) {
761
+ const fields = knownStructs.get(sName);
762
+ if (!fields)
763
+ continue;
764
+ for (const field of fields) {
765
+ const sizeBefore = usedStructNames.size + usedEnumNames.size;
766
+ collectTypeRef(field.type);
767
+ if (usedStructNames.size + usedEnumNames.size > sizeBefore)
768
+ typeChanged = true;
769
+ }
770
+ }
771
+ }
439
772
  const contractStructs = [];
440
773
  for (const [sName, fields] of knownStructs) {
441
- contractStructs.push({ name: sName, fields });
774
+ if (usedStructNames.has(sName))
775
+ contractStructs.push({ name: sName, fields });
442
776
  }
443
777
  const contractEnums = [];
444
778
  for (const [eName, members] of knownEnums) {
445
- contractEnums.push({ name: eName, members });
779
+ if (usedEnumNames.has(eName))
780
+ contractEnums.push({ name: eName, members });
446
781
  }
447
782
  // Determine which interfaces this contract actually references
448
783
  const usedIfaceNames = new Set();
@@ -458,10 +793,12 @@ function parseClass(node, filePath, knownStructs = new Map(), knownEnums = new M
458
793
  collectContractInterfaceTypeRefs(p.type, usedIfaceNames);
459
794
  if (f.returnType)
460
795
  collectContractInterfaceTypeRefs(f.returnType, usedIfaceNames);
796
+ collectBodyContractInterfaceRefs(f.body, usedIfaceNames);
461
797
  }
462
798
  if (ctor) {
463
799
  for (const p of ctor.parameters)
464
800
  collectContractInterfaceTypeRefs(p.type, usedIfaceNames);
801
+ collectBodyContractInterfaceRefs(ctor.body, usedIfaceNames);
465
802
  }
466
803
  // Deep copy only used interfaces so mutability updates don't leak to shared state
467
804
  const contractIfaceList = [];
@@ -530,6 +867,8 @@ function parseClass(node, filePath, knownStructs = new Map(), knownEnums = new M
530
867
  customErrors: contractCustomErrors,
531
868
  ctor,
532
869
  inherits,
870
+ isAbstract,
871
+ sourceLine: getSourceLine(node),
533
872
  };
534
873
  }
535
874
  // ============================================================
@@ -545,11 +884,11 @@ function tryParseEvent(node) {
545
884
  return null;
546
885
  const name = node.name && ts.isIdentifier(node.name) ? node.name.text : "Unknown";
547
886
  if (!node.type.typeArguments || node.type.typeArguments.length === 0) {
548
- return { name, parameters: [] };
887
+ return { name, parameters: [], sourceLine: getSourceLine(node) };
549
888
  }
550
889
  const typeArg = node.type.typeArguments[0];
551
890
  if (!ts.isTypeLiteralNode(typeArg)) {
552
- return { name, parameters: [] };
891
+ return { name, parameters: [], sourceLine: getSourceLine(node) };
553
892
  }
554
893
  const parameters = [];
555
894
  for (const member of typeArg.members) {
@@ -574,7 +913,7 @@ function tryParseEvent(node) {
574
913
  parameters.push({ name: paramName, type: paramType, indexed });
575
914
  }
576
915
  }
577
- return { name, parameters };
916
+ return { name, parameters, sourceLine: getSourceLine(node) };
578
917
  }
579
918
  // ============================================================
580
919
  // Error detection (SkittlesError<{...}>)
@@ -629,49 +968,54 @@ function parseProperty(node) {
629
968
  initialValue = parseExpression(node.initializer);
630
969
  }
631
970
  }
632
- return { name, type, visibility, immutable, constant, initialValue };
971
+ return { name, type, visibility, immutable, constant, initialValue, sourceLine: getSourceLine(node) };
633
972
  }
634
973
  function parseMethod(node, varTypes, eventNames) {
635
974
  const name = node.name && ts.isIdentifier(node.name) ? node.name.text : "unknown";
636
975
  const parameters = node.parameters.map(parseParameter);
976
+ setupStringTracking(parameters, varTypes);
637
977
  const returnType = node.type
638
978
  ? parseType(node.type)
639
979
  : null;
640
980
  const visibility = getVisibility(node.modifiers);
981
+ const isAbstractMethod = hasModifier(node.modifiers, ts.SyntaxKind.AbstractKeyword);
641
982
  const body = node.body ? parseBlock(node.body, varTypes, eventNames) : [];
642
- const stateMutability = inferStateMutability(body, varTypes);
983
+ const stateMutability = isAbstractMethod ? inferAbstractStateMutability() : inferStateMutability(body, varTypes, parameters);
643
984
  const isOverride = hasModifier(node.modifiers, ts.SyntaxKind.OverrideKeyword);
644
985
  const isVirtual = !isOverride;
645
- return { name, parameters, returnType, visibility, stateMutability, isVirtual, isOverride, body };
986
+ return { name, parameters, returnType, visibility, stateMutability, isVirtual, isOverride, isAbstract: isAbstractMethod ? true : undefined, body, sourceLine: getSourceLine(node) };
646
987
  }
647
988
  function parseGetAccessor(node, varTypes, eventNames) {
648
989
  const name = node.name && ts.isIdentifier(node.name) ? node.name.text : "unknown";
649
990
  const parameters = [];
991
+ setupStringTracking(parameters, varTypes);
650
992
  const returnType = node.type
651
993
  ? parseType(node.type)
652
994
  : null;
653
995
  const visibility = getVisibility(node.modifiers);
654
996
  const body = node.body ? parseBlock(node.body, varTypes, eventNames) : [];
655
- const stateMutability = inferStateMutability(body, varTypes);
997
+ const stateMutability = inferStateMutability(body, varTypes, parameters);
656
998
  const isOverride = hasModifier(node.modifiers, ts.SyntaxKind.OverrideKeyword);
657
999
  const isVirtual = !isOverride;
658
- return { name, parameters, returnType, visibility, stateMutability, isVirtual, isOverride, body };
1000
+ return { name, parameters, returnType, visibility, stateMutability, isVirtual, isOverride, body, sourceLine: getSourceLine(node) };
659
1001
  }
660
1002
  function parseSetAccessor(node, varTypes, eventNames) {
661
1003
  const name = node.name && ts.isIdentifier(node.name) ? node.name.text : "unknown";
662
1004
  const parameters = node.parameters.map(parseParameter);
1005
+ setupStringTracking(parameters, varTypes);
663
1006
  const returnType = null; // setters don't return
664
1007
  const visibility = getVisibility(node.modifiers);
665
1008
  const body = node.body ? parseBlock(node.body, varTypes, eventNames) : [];
666
- const stateMutability = inferStateMutability(body, varTypes);
1009
+ const stateMutability = inferStateMutability(body, varTypes, parameters);
667
1010
  const isOverride = hasModifier(node.modifiers, ts.SyntaxKind.OverrideKeyword);
668
1011
  const isVirtual = !isOverride;
669
- return { name, parameters, returnType, visibility, stateMutability, isVirtual, isOverride, body };
1012
+ return { name, parameters, returnType, visibility, stateMutability, isVirtual, isOverride, body, sourceLine: getSourceLine(node) };
670
1013
  }
671
1014
  function parseArrowProperty(node, varTypes, eventNames) {
672
1015
  const name = node.name && ts.isIdentifier(node.name) ? node.name.text : "unknown";
673
1016
  const arrow = node.initializer;
674
1017
  const parameters = arrow.parameters.map(parseParameter);
1018
+ setupStringTracking(parameters, varTypes);
675
1019
  const returnType = arrow.type
676
1020
  ? parseType(arrow.type)
677
1021
  : null;
@@ -686,22 +1030,27 @@ function parseArrowProperty(node, varTypes, eventNames) {
686
1030
  body = [{ kind: "return", value: parseExpression(arrow.body) }];
687
1031
  }
688
1032
  }
689
- const stateMutability = inferStateMutability(body, varTypes);
1033
+ const stateMutability = inferStateMutability(body, varTypes, parameters);
690
1034
  const isOverride = hasModifier(node.modifiers, ts.SyntaxKind.OverrideKeyword);
691
1035
  const isVirtual = !isOverride;
692
- return { name, parameters, returnType, visibility, stateMutability, isVirtual, isOverride, body };
1036
+ return { name, parameters, returnType, visibility, stateMutability, isVirtual, isOverride, body, sourceLine: getSourceLine(node) };
693
1037
  }
694
1038
  function parseConstructorDecl(node, varTypes, eventNames) {
695
1039
  const parameters = node.parameters.map(parseParameter);
1040
+ setupStringTracking(parameters, varTypes);
696
1041
  const body = node.body ? parseBlock(node.body, varTypes, eventNames) : [];
697
- return { parameters, body };
1042
+ return { parameters, body, sourceLine: getSourceLine(node) };
698
1043
  }
699
1044
  function parseParameter(node) {
700
1045
  const name = ts.isIdentifier(node.name) ? node.name.text : "unknown";
701
1046
  const type = node.type
702
1047
  ? parseType(node.type)
703
1048
  : { kind: "uint256" };
704
- return { name, type };
1049
+ const param = { name, type };
1050
+ if (node.initializer) {
1051
+ param.defaultValue = parseExpression(node.initializer);
1052
+ }
1053
+ return param;
705
1054
  }
706
1055
  // ============================================================
707
1056
  // Type parsing
@@ -711,7 +1060,7 @@ export function parseType(node) {
711
1060
  const name = ts.isIdentifier(node.typeName)
712
1061
  ? node.typeName.text
713
1062
  : "";
714
- if (name === "Record" &&
1063
+ if ((name === "Record" || name === "Map") &&
715
1064
  node.typeArguments &&
716
1065
  node.typeArguments.length === 2) {
717
1066
  return {
@@ -720,6 +1069,14 @@ export function parseType(node) {
720
1069
  valueType: parseType(node.typeArguments[1]),
721
1070
  };
722
1071
  }
1072
+ if (name === "ReadonlyArray" &&
1073
+ node.typeArguments &&
1074
+ node.typeArguments.length === 1) {
1075
+ return {
1076
+ kind: "array",
1077
+ valueType: parseType(node.typeArguments[0]),
1078
+ };
1079
+ }
723
1080
  if (name === "address")
724
1081
  return { kind: "address" };
725
1082
  if (name === "bytes")
@@ -751,6 +1108,20 @@ export function parseType(node) {
751
1108
  valueType: parseType(node.elementType),
752
1109
  };
753
1110
  }
1111
+ if (ts.isTypeOperatorNode(node) && node.operator === ts.SyntaxKind.ReadonlyKeyword) {
1112
+ return parseType(node.type);
1113
+ }
1114
+ if (ts.isTupleTypeNode(node)) {
1115
+ return {
1116
+ kind: "tuple",
1117
+ tupleTypes: node.elements.map((el) => {
1118
+ if (ts.isNamedTupleMember(el)) {
1119
+ return parseType(el.type);
1120
+ }
1121
+ return parseType(el);
1122
+ }),
1123
+ };
1124
+ }
754
1125
  switch (node.kind) {
755
1126
  case ts.SyntaxKind.NumberKeyword:
756
1127
  return { kind: "uint256" };
@@ -805,11 +1176,21 @@ export function parseExpression(node) {
805
1176
  return { kind: "number-literal", value: "0" };
806
1177
  }
807
1178
  if (ts.isPropertyAccessExpression(node)) {
808
- return {
809
- kind: "property-access",
810
- object: parseExpression(node.expression),
811
- property: node.name.text,
812
- };
1179
+ const object = parseExpression(node.expression);
1180
+ const property = node.name.text;
1181
+ // string.length → bytes(str).length
1182
+ if (property === "length" && isStringExpr(object)) {
1183
+ return {
1184
+ kind: "property-access",
1185
+ object: {
1186
+ kind: "call",
1187
+ callee: { kind: "identifier", name: "bytes" },
1188
+ args: [object],
1189
+ },
1190
+ property: "length",
1191
+ };
1192
+ }
1193
+ return { kind: "property-access", object, property };
813
1194
  }
814
1195
  if (ts.isElementAccessExpression(node)) {
815
1196
  return {
@@ -824,6 +1205,21 @@ export function parseExpression(node) {
824
1205
  if (opKind === ts.SyntaxKind.CommaToken) {
825
1206
  return parseExpression(node.right);
826
1207
  }
1208
+ // Desugar **= to x = x ** y (Solidity has no **= operator)
1209
+ if (opKind === ts.SyntaxKind.AsteriskAsteriskEqualsToken) {
1210
+ const target = parseExpression(node.left);
1211
+ return {
1212
+ kind: "assignment",
1213
+ operator: "=",
1214
+ target,
1215
+ value: {
1216
+ kind: "binary",
1217
+ operator: "**",
1218
+ left: target,
1219
+ right: parseExpression(node.right),
1220
+ },
1221
+ };
1222
+ }
827
1223
  const operator = getBinaryOperator(opKind);
828
1224
  if (isAssignmentOperator(opKind)) {
829
1225
  return {
@@ -833,11 +1229,22 @@ export function parseExpression(node) {
833
1229
  value: parseExpression(node.right),
834
1230
  };
835
1231
  }
1232
+ const left = parseExpression(node.left);
1233
+ const right = parseExpression(node.right);
1234
+ // String comparison: str === other → keccak256(str) == keccak256(other)
1235
+ if ((operator === "==" || operator === "!=") && (isStringExpr(left) || isStringExpr(right))) {
1236
+ return {
1237
+ kind: "binary",
1238
+ operator,
1239
+ left: { kind: "call", callee: { kind: "identifier", name: "keccak256" }, args: [left] },
1240
+ right: { kind: "call", callee: { kind: "identifier", name: "keccak256" }, args: [right] },
1241
+ };
1242
+ }
836
1243
  return {
837
1244
  kind: "binary",
838
1245
  operator,
839
- left: parseExpression(node.left),
840
- right: parseExpression(node.right),
1246
+ left,
1247
+ right,
841
1248
  };
842
1249
  }
843
1250
  if (ts.isPrefixUnaryExpression(node)) {
@@ -857,11 +1264,26 @@ export function parseExpression(node) {
857
1264
  };
858
1265
  }
859
1266
  if (ts.isCallExpression(node)) {
860
- return {
1267
+ const callExpr = {
861
1268
  kind: "call",
862
1269
  callee: parseExpression(node.expression),
863
1270
  args: node.arguments.map(parseExpression),
864
1271
  };
1272
+ if (node.typeArguments && node.typeArguments.length > 0) {
1273
+ const firstTypeArg = node.typeArguments[0];
1274
+ if (ts.isTupleTypeNode(firstTypeArg)) {
1275
+ callExpr.typeArgs = firstTypeArg.elements.map((elem) => {
1276
+ if (ts.isNamedTupleMember(elem)) {
1277
+ return parseType(elem.type);
1278
+ }
1279
+ return parseType(elem);
1280
+ });
1281
+ }
1282
+ else {
1283
+ callExpr.typeArgs = node.typeArguments.map((ta) => parseType(ta));
1284
+ }
1285
+ }
1286
+ return callExpr;
865
1287
  }
866
1288
  if (ts.isNewExpression(node)) {
867
1289
  const callee = ts.isIdentifier(node.expression)
@@ -897,6 +1319,13 @@ export function parseExpression(node) {
897
1319
  whenFalse: parseExpression(node.whenFalse),
898
1320
  };
899
1321
  }
1322
+ // Array literal expressions: [a, b, c] → tuple literal
1323
+ if (ts.isArrayLiteralExpression(node)) {
1324
+ return {
1325
+ kind: "tuple-literal",
1326
+ elements: node.elements.map(parseExpression),
1327
+ };
1328
+ }
900
1329
  // Object literal expressions: { x: 1, y: 2 } → struct construction
901
1330
  if (ts.isObjectLiteralExpression(node)) {
902
1331
  const properties = [];
@@ -949,6 +1378,7 @@ export function parseStatement(node, varTypes, eventNames = new Set()) {
949
1378
  value: node.expression
950
1379
  ? parseExpression(node.expression)
951
1380
  : undefined,
1381
+ sourceLine: getSourceLine(node),
952
1382
  };
953
1383
  }
954
1384
  if (ts.isVariableStatement(node)) {
@@ -959,17 +1389,27 @@ export function parseStatement(node, varTypes, eventNames = new Set()) {
959
1389
  ? parseExpression(decl.initializer)
960
1390
  : undefined;
961
1391
  const type = explicitType || (initializer ? inferType(initializer, varTypes) : undefined);
962
- return { kind: "variable-declaration", name, type, initializer };
1392
+ if (type?.kind === "string") {
1393
+ _currentStringNames.add(name);
1394
+ }
1395
+ return { kind: "variable-declaration", name, type, initializer, sourceLine: getSourceLine(node) };
963
1396
  }
964
1397
  if (ts.isExpressionStatement(node)) {
965
1398
  const emitStmt = tryParseEmitStatement(node.expression, eventNames);
966
- if (emitStmt)
1399
+ if (emitStmt) {
1400
+ emitStmt.sourceLine = getSourceLine(node);
967
1401
  return emitStmt;
1402
+ }
1403
+ const consoleLogStmt = tryParseConsoleLog(node.expression);
1404
+ if (consoleLogStmt) {
1405
+ consoleLogStmt.sourceLine = getSourceLine(node);
1406
+ return consoleLogStmt;
1407
+ }
968
1408
  // Detect delete expressions: `delete this.mapping[key]`
969
1409
  if (ts.isDeleteExpression(node.expression)) {
970
- return { kind: "delete", target: parseExpression(node.expression.expression) };
1410
+ return { kind: "delete", target: parseExpression(node.expression.expression), sourceLine: getSourceLine(node) };
971
1411
  }
972
- return { kind: "expression", expression: parseExpression(node.expression) };
1412
+ return { kind: "expression", expression: parseExpression(node.expression), sourceLine: getSourceLine(node) };
973
1413
  }
974
1414
  if (ts.isIfStatement(node)) {
975
1415
  const condition = parseExpression(node.expression);
@@ -977,7 +1417,7 @@ export function parseStatement(node, varTypes, eventNames = new Set()) {
977
1417
  const elseBody = node.elseStatement
978
1418
  ? parseBlock(node.elseStatement, varTypes, eventNames)
979
1419
  : undefined;
980
- return { kind: "if", condition, thenBody, elseBody };
1420
+ return { kind: "if", condition, thenBody, elseBody, sourceLine: getSourceLine(node) };
981
1421
  }
982
1422
  if (ts.isForStatement(node)) {
983
1423
  let initializer;
@@ -1013,6 +1453,7 @@ export function parseStatement(node, varTypes, eventNames = new Set()) {
1013
1453
  ? parseExpression(node.incrementor)
1014
1454
  : undefined,
1015
1455
  body: parseBlock(node.statement, varTypes, eventNames),
1456
+ sourceLine: getSourceLine(node),
1016
1457
  };
1017
1458
  }
1018
1459
  if (ts.isWhileStatement(node)) {
@@ -1020,6 +1461,7 @@ export function parseStatement(node, varTypes, eventNames = new Set()) {
1020
1461
  kind: "while",
1021
1462
  condition: parseExpression(node.expression),
1022
1463
  body: parseBlock(node.statement, varTypes, eventNames),
1464
+ sourceLine: getSourceLine(node),
1023
1465
  };
1024
1466
  }
1025
1467
  if (ts.isDoStatement(node)) {
@@ -1027,13 +1469,14 @@ export function parseStatement(node, varTypes, eventNames = new Set()) {
1027
1469
  kind: "do-while",
1028
1470
  condition: parseExpression(node.expression),
1029
1471
  body: parseBlock(node.statement, varTypes, eventNames),
1472
+ sourceLine: getSourceLine(node),
1030
1473
  };
1031
1474
  }
1032
1475
  if (node.kind === ts.SyntaxKind.BreakStatement) {
1033
- return { kind: "break" };
1476
+ return { kind: "break", sourceLine: getSourceLine(node) };
1034
1477
  }
1035
1478
  if (node.kind === ts.SyntaxKind.ContinueStatement) {
1036
- return { kind: "continue" };
1479
+ return { kind: "continue", sourceLine: getSourceLine(node) };
1037
1480
  }
1038
1481
  if (ts.isForOfStatement(node)) {
1039
1482
  // Desugar: for (const item of arr) { ... }
@@ -1083,8 +1526,55 @@ export function parseStatement(node, varTypes, eventNames = new Set()) {
1083
1526
  prefix: false,
1084
1527
  },
1085
1528
  body: [itemDecl, ...innerBody],
1529
+ sourceLine: getSourceLine(node),
1086
1530
  };
1087
1531
  }
1532
+ if (ts.isForInStatement(node)) {
1533
+ // Desugar: for (const item in EnumType) { ... }
1534
+ // → for (uint256 _i = 0; _i < memberCount; _i++) { EnumType item = EnumType(_i); ... }
1535
+ const enumName = ts.isIdentifier(node.expression) ? node.expression.text : "";
1536
+ const enumMembers = _knownEnums.get(enumName);
1537
+ if (enumMembers) {
1538
+ const itemName = ts.isVariableDeclarationList(node.initializer)
1539
+ ? (ts.isIdentifier(node.initializer.declarations[0].name) ? node.initializer.declarations[0].name.text : "_item")
1540
+ : "_item";
1541
+ const indexName = `_i_${itemName}`;
1542
+ const innerBody = parseBlock(node.statement, varTypes, eventNames);
1543
+ // Prepend: EnumType item = EnumType(_i);
1544
+ const itemDecl = {
1545
+ kind: "variable-declaration",
1546
+ name: itemName,
1547
+ type: { kind: "enum", structName: enumName },
1548
+ initializer: {
1549
+ kind: "call",
1550
+ callee: { kind: "identifier", name: enumName },
1551
+ args: [{ kind: "identifier", name: indexName }],
1552
+ },
1553
+ };
1554
+ return {
1555
+ kind: "for",
1556
+ initializer: {
1557
+ kind: "variable-declaration",
1558
+ name: indexName,
1559
+ type: { kind: "uint256" },
1560
+ initializer: { kind: "number-literal", value: "0" },
1561
+ },
1562
+ condition: {
1563
+ kind: "binary",
1564
+ operator: "<",
1565
+ left: { kind: "identifier", name: indexName },
1566
+ right: { kind: "number-literal", value: String(enumMembers.length) },
1567
+ },
1568
+ incrementor: {
1569
+ kind: "unary",
1570
+ operator: "++",
1571
+ operand: { kind: "identifier", name: indexName },
1572
+ prefix: false,
1573
+ },
1574
+ body: [itemDecl, ...innerBody],
1575
+ };
1576
+ }
1577
+ }
1088
1578
  if (ts.isSwitchStatement(node)) {
1089
1579
  const discriminant = parseExpression(node.expression);
1090
1580
  const cases = [];
@@ -1104,7 +1594,53 @@ export function parseStatement(node, varTypes, eventNames = new Set()) {
1104
1594
  cases.push({ value: undefined, body });
1105
1595
  }
1106
1596
  }
1107
- return { kind: "switch", discriminant, cases };
1597
+ return { kind: "switch", discriminant, cases, sourceLine: getSourceLine(node) };
1598
+ }
1599
+ if (ts.isTryStatement(node)) {
1600
+ const tryBlock = node.tryBlock;
1601
+ const catchClause = node.catchClause;
1602
+ const tryStatements = tryBlock.statements;
1603
+ if (tryStatements.length === 0) {
1604
+ throw new Error("try block must contain at least one statement with an external call");
1605
+ }
1606
+ // The first statement must be an external call (either variable declaration or expression)
1607
+ const firstStmt = tryStatements[0];
1608
+ let call;
1609
+ let returnVarName;
1610
+ let returnType;
1611
+ if (ts.isVariableStatement(firstStmt)) {
1612
+ const decl = firstStmt.declarationList.declarations[0];
1613
+ returnVarName = ts.isIdentifier(decl.name) ? decl.name.text : undefined;
1614
+ returnType = decl.type ? parseType(decl.type) : undefined;
1615
+ if (decl.initializer) {
1616
+ call = parseExpression(decl.initializer);
1617
+ if (!returnType) {
1618
+ returnType = inferType(call, varTypes);
1619
+ }
1620
+ }
1621
+ else {
1622
+ throw new Error("try block variable declaration must have an initializer with an external call");
1623
+ }
1624
+ }
1625
+ else if (ts.isExpressionStatement(firstStmt)) {
1626
+ call = parseExpression(firstStmt.expression);
1627
+ }
1628
+ else {
1629
+ throw new Error("First statement in try block must be an external call");
1630
+ }
1631
+ // Remaining statements become success body
1632
+ const successBody = [];
1633
+ for (let i = 1; i < tryStatements.length; i++) {
1634
+ successBody.push(...parseStatements(tryStatements[i], varTypes, eventNames));
1635
+ }
1636
+ // Parse catch body
1637
+ const catchBody = [];
1638
+ if (catchClause && catchClause.block) {
1639
+ for (const stmt of catchClause.block.statements) {
1640
+ catchBody.push(...parseStatements(stmt, varTypes, eventNames));
1641
+ }
1642
+ }
1643
+ return { kind: "try-catch", call, returnVarName, returnType, successBody, catchBody };
1108
1644
  }
1109
1645
  if (ts.isThrowStatement(node)) {
1110
1646
  // Pattern: throw new ErrorName(args) (class extends Error style)
@@ -1116,14 +1652,14 @@ export function parseStatement(node, varTypes, eventNames = new Set()) {
1116
1652
  const args = node.expression.arguments
1117
1653
  ? Array.from(node.expression.arguments).map(parseExpression)
1118
1654
  : [];
1119
- return { kind: "revert", customError: errorName, customErrorArgs: args };
1655
+ return { kind: "revert", customError: errorName, customErrorArgs: args, sourceLine: getSourceLine(node) };
1120
1656
  }
1121
1657
  let message;
1122
1658
  if (node.expression.arguments &&
1123
1659
  node.expression.arguments.length > 0) {
1124
1660
  message = parseExpression(node.expression.arguments[0]);
1125
1661
  }
1126
- return { kind: "revert", message };
1662
+ return { kind: "revert", message, sourceLine: getSourceLine(node) };
1127
1663
  }
1128
1664
  // Pattern: throw this.ErrorName(args) (SkittlesError property style)
1129
1665
  if (node.expression && ts.isCallExpression(node.expression)) {
@@ -1133,11 +1669,11 @@ export function parseStatement(node, varTypes, eventNames = new Set()) {
1133
1669
  const errorName = callee.name.text;
1134
1670
  if (_knownCustomErrors.has(errorName)) {
1135
1671
  const args = node.expression.arguments.map(parseExpression);
1136
- return { kind: "revert", customError: errorName, customErrorArgs: args };
1672
+ return { kind: "revert", customError: errorName, customErrorArgs: args, sourceLine: getSourceLine(node) };
1137
1673
  }
1138
1674
  }
1139
1675
  }
1140
- return { kind: "revert" };
1676
+ return { kind: "revert", sourceLine: getSourceLine(node) };
1141
1677
  }
1142
1678
  throw new Error(`Unsupported statement: ${ts.SyntaxKind[node.kind]}`);
1143
1679
  }
@@ -1155,6 +1691,7 @@ function parseStatements(node, varTypes, eventNames) {
1155
1691
  if (ts.isVariableStatement(node)) {
1156
1692
  // Multi declaration: let a=1, b=2, c=3
1157
1693
  if (node.declarationList.declarations.length > 1) {
1694
+ const sl = getSourceLine(node);
1158
1695
  return node.declarationList.declarations.map((decl) => {
1159
1696
  const name = ts.isIdentifier(decl.name) ? decl.name.text : "unknown";
1160
1697
  const explicitType = decl.type ? parseType(decl.type) : undefined;
@@ -1162,7 +1699,10 @@ function parseStatements(node, varTypes, eventNames) {
1162
1699
  ? parseExpression(decl.initializer)
1163
1700
  : undefined;
1164
1701
  const type = explicitType || (initializer ? inferType(initializer, varTypes) : undefined);
1165
- return { kind: "variable-declaration", name, type, initializer };
1702
+ if (type?.kind === "string") {
1703
+ _currentStringNames.add(name);
1704
+ }
1705
+ return { kind: "variable-declaration", name, type, initializer, sourceLine: sl };
1166
1706
  });
1167
1707
  }
1168
1708
  // Array destructuring: const [a, b, c] = [7, 8, 9]
@@ -1170,6 +1710,10 @@ function parseStatements(node, varTypes, eventNames) {
1170
1710
  if (decl.name && ts.isArrayBindingPattern(decl.name) && decl.initializer) {
1171
1711
  return parseArrayDestructuring(decl.name, decl.initializer, varTypes);
1172
1712
  }
1713
+ // Object destructuring: const { a, b } = { a: 1, b: 2 }
1714
+ if (decl.name && ts.isObjectBindingPattern(decl.name) && decl.initializer) {
1715
+ return parseObjectDestructuring(decl.name, decl.initializer, varTypes, decl);
1716
+ }
1173
1717
  }
1174
1718
  return [parseStatement(node, varTypes, eventNames)];
1175
1719
  }
@@ -1211,6 +1755,23 @@ function tryParseEmitStatement(node, eventNames) {
1211
1755
  return { kind: "emit", eventName, args };
1212
1756
  }
1213
1757
  // ============================================================
1758
+ // Console.log detection: console.log(args)
1759
+ // ============================================================
1760
+ function tryParseConsoleLog(node) {
1761
+ if (!ts.isCallExpression(node))
1762
+ return null;
1763
+ const callee = node.expression;
1764
+ if (!ts.isPropertyAccessExpression(callee))
1765
+ return null;
1766
+ if (callee.name.text !== "log")
1767
+ return null;
1768
+ const obj = callee.expression;
1769
+ if (!ts.isIdentifier(obj) || obj.text !== "console")
1770
+ return null;
1771
+ const args = node.arguments.map(parseExpression);
1772
+ return { kind: "console-log", args };
1773
+ }
1774
+ // ============================================================
1214
1775
  // State mutability inference
1215
1776
  // ============================================================
1216
1777
  /**
@@ -1344,6 +1905,14 @@ function walkStatements(stmts, onExpr, onStmt) {
1344
1905
  case "delete":
1345
1906
  walkExpr(stmt.target);
1346
1907
  break;
1908
+ case "try-catch":
1909
+ walkExpr(stmt.call);
1910
+ stmt.successBody.forEach(walkStmt);
1911
+ stmt.catchBody.forEach(walkStmt);
1912
+ break;
1913
+ case "console-log":
1914
+ stmt.args.forEach(walkExpr);
1915
+ break;
1347
1916
  }
1348
1917
  }
1349
1918
  stmts.forEach(walkStmt);
@@ -1360,13 +1929,31 @@ function collectThisCalls(stmts) {
1360
1929
  });
1361
1930
  return names;
1362
1931
  }
1363
- export function inferStateMutability(body, varTypes) {
1932
+ function inferAbstractStateMutability() {
1933
+ return "nonpayable";
1934
+ }
1935
+ export function inferStateMutability(body, varTypes, params) {
1364
1936
  let readsState = false;
1365
1937
  let writesState = false;
1366
1938
  let usesMsgValue = false;
1939
+ let readsEnvironment = false;
1940
+ const thisCallCallees = new Set();
1941
+ // Track local variable types for detecting external contract calls on locals
1942
+ const localVarTypes = new Map();
1943
+ if (params) {
1944
+ for (const p of params) {
1945
+ localVarTypes.set(p.name, p.type);
1946
+ }
1947
+ }
1367
1948
  walkStatements(body, (expr) => {
1949
+ if (expr.kind === "call" &&
1950
+ expr.callee.kind === "property-access" &&
1951
+ expr.callee.object.kind === "identifier" &&
1952
+ expr.callee.object.name === "this") {
1953
+ thisCallCallees.add(expr.callee);
1954
+ }
1368
1955
  if (expr.kind === "property-access") {
1369
- if (expr.object.kind === "identifier" && expr.object.name === "this") {
1956
+ if (expr.object.kind === "identifier" && expr.object.name === "this" && !thisCallCallees.has(expr)) {
1370
1957
  readsState = true;
1371
1958
  }
1372
1959
  if (expr.object.kind === "identifier" &&
@@ -1374,6 +1961,24 @@ export function inferStateMutability(body, varTypes) {
1374
1961
  expr.property === "value") {
1375
1962
  usesMsgValue = true;
1376
1963
  }
1964
+ // EVM environment reads: msg.sender, msg.data, msg.sig, block.*, tx.*
1965
+ // (msg.value is excluded here because it is handled separately as payable)
1966
+ if (expr.object.kind === "identifier" &&
1967
+ ((expr.object.name === "msg" && expr.property !== "value") ||
1968
+ expr.object.name === "block" ||
1969
+ expr.object.name === "tx")) {
1970
+ readsEnvironment = true;
1971
+ }
1972
+ }
1973
+ // `self` reads the contract's own address (address(this))
1974
+ if (expr.kind === "identifier" && expr.name === "self") {
1975
+ readsEnvironment = true;
1976
+ }
1977
+ // `gasleft()` reads remaining gas from the environment
1978
+ if (expr.kind === "call" &&
1979
+ expr.callee.kind === "identifier" &&
1980
+ expr.callee.name === "gasleft") {
1981
+ readsEnvironment = true;
1377
1982
  }
1378
1983
  if (expr.kind === "assignment" && isStateAccess(expr.target)) {
1379
1984
  writesState = true;
@@ -1386,9 +1991,21 @@ export function inferStateMutability(body, varTypes) {
1386
1991
  if (expr.kind === "call" && isStateMutatingCall(expr)) {
1387
1992
  writesState = true;
1388
1993
  }
1994
+ // addr.transfer(amount) sends ETH, which is state-mutating
1995
+ // Only match when the receiver is not `this` and not a contract-interface variable
1996
+ if (expr.kind === "call" &&
1997
+ expr.callee.kind === "property-access" &&
1998
+ expr.callee.property === "transfer" &&
1999
+ expr.args.length === 1 &&
2000
+ !isContractInterfaceReceiver(expr.callee.object, varTypes, localVarTypes)) {
2001
+ writesState = true;
2002
+ }
1389
2003
  if (expr.kind === "call" && varTypes && isExternalContractCall(expr, varTypes)) {
1390
2004
  writesState = true;
1391
2005
  }
2006
+ if (expr.kind === "call" && isExternalContractCallOnLocal(expr, localVarTypes)) {
2007
+ writesState = true;
2008
+ }
1392
2009
  }, (stmt) => {
1393
2010
  if (stmt.kind === "emit") {
1394
2011
  writesState = true;
@@ -1396,12 +2013,16 @@ export function inferStateMutability(body, varTypes) {
1396
2013
  if (stmt.kind === "delete" && isStateAccess(stmt.target)) {
1397
2014
  writesState = true;
1398
2015
  }
2016
+ if (stmt.kind === "variable-declaration" && stmt.type &&
2017
+ stmt.type.kind === "contract-interface" && stmt.name) {
2018
+ localVarTypes.set(stmt.name, stmt.type);
2019
+ }
1399
2020
  });
1400
2021
  if (usesMsgValue)
1401
2022
  return "payable";
1402
2023
  if (writesState)
1403
2024
  return "nonpayable";
1404
- if (readsState)
2025
+ if (readsState || readsEnvironment)
1405
2026
  return "view";
1406
2027
  return "pure";
1407
2028
  }
@@ -1480,6 +2101,29 @@ function collectContractInterfaceTypeRefs(type, refs) {
1480
2101
  if (type.valueType)
1481
2102
  collectContractInterfaceTypeRefs(type.valueType, refs);
1482
2103
  }
2104
+ function collectBodyContractInterfaceRefs(stmts, refs) {
2105
+ for (const stmt of stmts) {
2106
+ if (stmt.kind === "variable-declaration" && stmt.type) {
2107
+ collectContractInterfaceTypeRefs(stmt.type, refs);
2108
+ }
2109
+ if (stmt.kind === "if") {
2110
+ collectBodyContractInterfaceRefs(stmt.thenBody, refs);
2111
+ if (stmt.elseBody)
2112
+ collectBodyContractInterfaceRefs(stmt.elseBody, refs);
2113
+ }
2114
+ if (stmt.kind === "for" || stmt.kind === "while" || stmt.kind === "do-while") {
2115
+ collectBodyContractInterfaceRefs(stmt.body, refs);
2116
+ }
2117
+ if (stmt.kind === "switch") {
2118
+ for (const c of stmt.cases)
2119
+ collectBodyContractInterfaceRefs(c.body, refs);
2120
+ }
2121
+ if (stmt.kind === "try-catch") {
2122
+ collectBodyContractInterfaceRefs(stmt.successBody, refs);
2123
+ collectBodyContractInterfaceRefs(stmt.catchBody, refs);
2124
+ }
2125
+ }
2126
+ }
1483
2127
  function isStateAccess(expr) {
1484
2128
  if (expr.kind === "property-access" &&
1485
2129
  expr.object.kind === "identifier" &&
@@ -1504,6 +2148,18 @@ function isExternalContractCall(expr, varTypes) {
1504
2148
  }
1505
2149
  return false;
1506
2150
  }
2151
+ function isExternalContractCallOnLocal(expr, localVarTypes) {
2152
+ if (expr.callee.kind === "property-access" &&
2153
+ expr.callee.object.kind === "identifier" &&
2154
+ expr.callee.object.name !== "this") {
2155
+ const varName = expr.callee.object.name;
2156
+ const varType = localVarTypes.get(varName);
2157
+ if (varType && varType.kind === "contract-interface") {
2158
+ return true;
2159
+ }
2160
+ }
2161
+ return false;
2162
+ }
1507
2163
  function isStateMutatingCall(expr) {
1508
2164
  if (expr.callee.kind !== "property-access")
1509
2165
  return false;
@@ -1512,6 +2168,34 @@ function isStateMutatingCall(expr) {
1512
2168
  return false;
1513
2169
  return isStateAccess(expr.callee.object);
1514
2170
  }
2171
+ /**
2172
+ * Check if the receiver of a property-access is `this` or a contract-interface typed variable.
2173
+ * Used to distinguish `addr.transfer(amount)` (ETH transfer) from
2174
+ * `this.transfer(...)` or `token.transfer(...)` (contract method calls).
2175
+ */
2176
+ function isContractInterfaceReceiver(receiver, varTypes, localVarTypes) {
2177
+ // this.transfer(...) is an internal contract call
2178
+ if (receiver.kind === "identifier" && receiver.name === "this")
2179
+ return true;
2180
+ // this.token.transfer(...) where token is a contract-interface state variable
2181
+ if (receiver.kind === "property-access" &&
2182
+ receiver.object.kind === "identifier" &&
2183
+ receiver.object.name === "this") {
2184
+ const propType = varTypes?.get(receiver.property);
2185
+ if (propType && propType.kind === "contract-interface")
2186
+ return true;
2187
+ }
2188
+ // token.transfer(...) where token is a contract-interface local/param
2189
+ if (receiver.kind === "identifier") {
2190
+ const localType = localVarTypes?.get(receiver.name);
2191
+ if (localType && localType.kind === "contract-interface")
2192
+ return true;
2193
+ const stateType = varTypes?.get(receiver.name);
2194
+ if (stateType && stateType.kind === "contract-interface")
2195
+ return true;
2196
+ }
2197
+ return false;
2198
+ }
1515
2199
  function getVisibility(modifiers) {
1516
2200
  if (!modifiers)
1517
2201
  return "public";