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.
- package/README.md +1 -1
- package/dist/commands/compile.d.ts +1 -0
- package/dist/commands/compile.d.ts.map +1 -1
- package/dist/commands/compile.js +51 -1
- package/dist/commands/compile.js.map +1 -1
- package/dist/commands/init.d.ts.map +1 -1
- package/dist/commands/init.js +2 -0
- package/dist/commands/init.js.map +1 -1
- package/dist/commands/test.d.ts +7 -0
- package/dist/commands/test.d.ts.map +1 -0
- package/dist/commands/test.js +60 -0
- package/dist/commands/test.js.map +1 -0
- package/dist/compiler/analysis.d.ts +7 -0
- package/dist/compiler/analysis.d.ts.map +1 -0
- package/dist/compiler/analysis.js +234 -0
- package/dist/compiler/analysis.js.map +1 -0
- package/dist/compiler/codegen.d.ts +10 -1
- package/dist/compiler/codegen.d.ts.map +1 -1
- package/dist/compiler/codegen.js +389 -22
- package/dist/compiler/codegen.js.map +1 -1
- package/dist/compiler/compiler.d.ts +1 -0
- package/dist/compiler/compiler.d.ts.map +1 -1
- package/dist/compiler/compiler.js +90 -6
- package/dist/compiler/compiler.js.map +1 -1
- package/dist/compiler/parser.d.ts +7 -1
- package/dist/compiler/parser.d.ts.map +1 -1
- package/dist/compiler/parser.js +731 -47
- package/dist/compiler/parser.js.map +1 -1
- package/dist/config/config.d.ts.map +1 -1
- package/dist/config/config.js +2 -0
- package/dist/config/config.js.map +1 -1
- package/dist/exports.d.ts +13 -2
- package/dist/exports.d.ts.map +1 -1
- package/dist/exports.js.map +1 -1
- package/dist/index.js +13 -3
- package/dist/index.js.map +1 -1
- package/dist/testing.d.ts +93 -0
- package/dist/testing.d.ts.map +1 -0
- package/dist/testing.js +143 -0
- package/dist/testing.js.map +1 -0
- package/dist/types/index.d.ts +50 -2
- package/dist/types/index.d.ts.map +1 -1
- package/dist/types/index.js +1 -0
- package/dist/types/index.js.map +1 -1
- package/package.json +2 -2
package/dist/compiler/parser.js
CHANGED
|
@@ -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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
|
|
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
|
|
840
|
-
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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";
|