diff --git a/CHANGELOG.md b/CHANGELOG.md index f49a504e..5817914c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -14,6 +14,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - `ShortCircuitCondition` detector: PR [#202](https://github.com/nowarp/misti/pull/202) - `PreferredStdlibApi` detector now suggest some preferred replacements for cell methods - Add Callgraph: PR [#185](https://github.com/nowarp/misti/pull/185) +- Add function effects to Callgraph: PR [#227](https://github.com/nowarp/misti/pull/227) ### Changed - `SuspiciousMessageMode` detector now suggests using SendDefaultMode instead of 0 for mode: PR [#199](https://github.com/nowarp/misti/pull/199/) diff --git a/src/detectors/builtin/ensurePrgSeed.ts b/src/detectors/builtin/ensurePrgSeed.ts index 71213797..9765d952 100644 --- a/src/detectors/builtin/ensurePrgSeed.ts +++ b/src/detectors/builtin/ensurePrgSeed.ts @@ -1,5 +1,9 @@ import { CompilationUnit } from "../../internals/ir"; -import { forEachExpression } from "../../internals/tact"; +import { + forEachExpression, + PRG_INIT_NAMES, + PRG_NATIVE_USE_NAMES, +} from "../../internals/tact"; import { MistiTactWarning, Severity } from "../../internals/warnings"; import { ASTDetector } from "../detector"; import { AstStaticCall, idText } from "@tact-lang/compiler/dist/grammar/ast"; @@ -38,20 +42,14 @@ export class EnsurePrgSeed extends ASTDetector { severity = Severity.MEDIUM; async check(cu: CompilationUnit): Promise { - const prgInitNames = new Set([ - "nativePrepareRandom", - "nativeRandomize", - "nativeRandomizeLt", - ]); - const prgUseNames = new Set(["nativeRandom", "nativeRandomInterval"]); const randomCalls = cu.ast.getProgramEntries().reduce( (acc, node) => { forEachExpression(node, (expr) => { if (expr.kind === "static_call") { - if (prgInitNames.has(idText(expr.function))) { + if (PRG_INIT_NAMES.has(idText(expr.function))) { acc.hasInitializer = true; } - if (prgUseNames.has(idText(expr.function))) { + if (PRG_NATIVE_USE_NAMES.has(idText(expr.function))) { acc.uses.push(expr); } } @@ -72,7 +70,7 @@ export class EnsurePrgSeed extends ASTDetector { `PRG seed should be initialized before using ${idText(use.function)}`, use.loc, { - suggestion: `Use ${Array.from(prgInitNames) + suggestion: `Use ${Array.from(PRG_INIT_NAMES) .map((name) => "`" + name + "`") .join( ", ", diff --git a/src/detectors/builtin/sendInLoop.ts b/src/detectors/builtin/sendInLoop.ts index 7a854b15..ec92156f 100644 --- a/src/detectors/builtin/sendInLoop.ts +++ b/src/detectors/builtin/sendInLoop.ts @@ -1,5 +1,5 @@ import { CompilationUnit } from "../../internals/ir"; -import { CallGraph } from "../../internals/ir/callGraph"; +import { CallGraph, Effect } from "../../internals/ir/callGraph"; import { forEachStatement, foldExpressions } from "../../internals/tact"; import { isSendCall } from "../../internals/tact/util"; import { MistiTactWarning, Severity } from "../../internals/warnings"; @@ -120,7 +120,7 @@ export class SendInLoop extends ASTDetector { const calleeNodeId = callGraph.getNodeIdByName(calleeName); if (calleeNodeId !== undefined) { const calleeNode = callGraph.getNode(calleeNodeId); - if (calleeNode && calleeNode.hasFlag(0b0001)) { + if (calleeNode && calleeNode.hasEffect(Effect.Send)) { const functionName = calleeNode.name.includes("::") ? calleeNode.name.split("::").pop() : calleeNode.name; diff --git a/src/internals/ir/callGraph.ts b/src/internals/ir/callGraph.ts index 4f178a0d..07eb7bf4 100644 --- a/src/internals/ir/callGraph.ts +++ b/src/internals/ir/callGraph.ts @@ -4,7 +4,14 @@ import { IdxGenerator } from "./indices"; import { MistiContext } from "../../"; import { Logger } from "../../internals/logger"; import { findInExpressions, forEachExpression } from "../tact/iterators"; -import { isSendCall } from "../tact/util"; +import { + DATETIME_NAMES, + isSelfAccess, + isSendCall, + PRG_INIT_NAMES, + PRG_NATIVE_USE_NAMES, + PRG_SAFE_USE_NAMES, +} from "../tact/util"; import { AstFunctionDef, AstReceiver, @@ -16,22 +23,35 @@ import { AstId, AstContractDeclaration, AstNode, + AstFieldAccess, + AstStatement, + idText, } from "@tact-lang/compiler/dist/grammar/ast"; +import { prettyPrint } from "@tact-lang/compiler/dist/prettyPrinter"; export type CGNodeId = number & { readonly brand: unique symbol }; export type CGEdgeId = number & { readonly brand: unique symbol }; /** - * Flag constants for CGNode. - * - * `FLAG_CALLS_SEND` (0b0001): Indicates that the function represented by this node - * contains a direct or indirect call to a "send" function. + * Effects flags for callgraph functions */ -const FLAG_CALLS_SEND = 0b0001; +export enum Effect { + /** Uses functions that send funds. */ + Send = 1 << 0, + /** Reads contract's state. */ + StateRead = 1 << 1, + /** Writes contract's state. */ + StateWrite = 1 << 2, + /** Accesses datetime functions. */ + AccessDatetime = 1 << 3, + /** Uses PRG. */ + PrgUse = 1 << 4, + /** Inits PRG seed. */ + PrgSeedInit = 1 << 5, +} /** * Represents an edge in the call graph, indicating a call from one function to another. - * Each edge has a unique index (`idx`) generated using `IdxGenerator`. */ class CGEdge { public idx: CGEdgeId; @@ -50,13 +70,12 @@ class CGEdge { /** * Represents a node in the call graph, corresponding to a function or method. - * Nodes maintain references to incoming and outgoing edges. */ class CGNode { public idx: CGNodeId; public inEdges: Set = new Set(); public outEdges: Set = new Set(); - public flags: number = 0; + public effects: number = 0; /** * @param astId The AST ID of the function or method this node represents (can be `undefined` for synthetic nodes) @@ -64,7 +83,7 @@ class CGNode { * @param logger A logger instance for logging messages */ constructor( - public astId: number | undefined, + public astId: AstNode["id"] | undefined, public name: string, private logger: Logger, ) { @@ -74,35 +93,44 @@ class CGNode { } } - // Method to set a flag - public setFlag(flag: number) { - this.flags |= flag; + public addEffect(effect: Effect) { + this.effects |= effect; } - // Method to check if a flag is set - public hasFlag(flag: number): boolean { - return (this.flags & flag) !== 0; + public hasEffect(effect: Effect): boolean { + return (this.effects & effect) !== 0; + } + + /** + * Pretty-prints a signature of the function is available + */ + public signature(ast: TactASTStore): string | undefined { + if (!this.astId) return undefined; + const fun = ast.getFunction(this.astId); + if (!fun) return undefined; + const signature = prettyPrint(fun) + .split("{")[0] + .replace(/\s+/g, " ") + .trim(); + return signature; } } /** * Represents the call graph, a directed graph where nodes represent functions or methods, * and edges indicate calls between them. - * - * The `CallGraph` class provides methods to build the graph from a Tact AST, retrieve nodes/edges, - * analyze connectivity, and add function calls dynamically. */ export class CallGraph { private nodeMap: Map = new Map(); private astIdToNodeId: Map = new Map(); private nameToNodeId: Map = new Map(); private edgesMap: Map = new Map(); - private logger: Logger; + private readonly logger: Logger; /** * @param ctx The MistiContext providing a logger and other utilities */ - constructor(private ctx: MistiContext) { + constructor(ctx: MistiContext) { this.logger = ctx.logger; } @@ -190,18 +218,18 @@ export class CallGraph { * @returns The constructed `CallGraph`. */ public build(astStore: TactASTStore): CallGraph { - for (const entry of astStore.getProgramEntries()) { + astStore.getProgramEntries().forEach((entry) => { if (entry.kind === "contract") { const contract = entry as AstContract; const contractName = contract.name.text; - for (const declaration of contract.declarations) { + contract.declarations.forEach((declaration) => { this.addContractDeclarationToGraph(declaration, contractName); - } + }); } else if (entry.kind === "function_def") { const func = entry as AstFunctionDef; this.addFunctionToGraph(func); } - } + }); this.analyzeFunctionCalls(astStore); return this; } @@ -285,7 +313,6 @@ export class CallGraph { for (const entry of astStore.getProgramEntries()) { if (entry.kind === "contract") { const contract = entry as AstContract; - const contractName = contract.name.text; for (const declaration of contract.declarations) { if ( declaration.kind === "function_def" || @@ -298,17 +325,11 @@ export class CallGraph { | AstReceiver; const funcNodeId = this.astIdToNodeId.get(func.id); if (funcNodeId !== undefined) { - forEachExpression(func, (expr) => { - this.processExpression(expr, funcNodeId, contractName); + const funcNode = this.getNode(funcNodeId); + if (!funcNode) continue; + func.statements.forEach((stmt) => { + this.processStatement(stmt, funcNodeId); }); - const sendCallFound = - findInExpressions(func, isSendCall) !== null; - if (sendCallFound) { - const funcNode = this.getNode(funcNodeId); - if (funcNode) { - funcNode.setFlag(FLAG_CALLS_SEND); - } - } } } } @@ -316,23 +337,46 @@ export class CallGraph { const func = entry as AstFunctionDef; const funcNodeId = this.astIdToNodeId.get(func.id); if (funcNodeId !== undefined) { - forEachExpression(func, (expr) => { - this.processExpression(expr, funcNodeId); + const funcNode = this.getNode(funcNodeId); + if (!funcNode) continue; + func.statements.forEach((stmt) => { + this.processStatement(stmt, funcNodeId); }); - const sendCallFound = findInExpressions(func, isSendCall) !== null; - if (sendCallFound) { - const funcNode = this.getNode(funcNodeId); - if (funcNode) { - funcNode.setFlag(FLAG_CALLS_SEND); - } - } } } } } /** - * Processes a single expression, identifying function or method calls to create edges. + * Processes a single statement, identifying assignments and other statements. + * Also detects effects and sets corresponding flags on the function node. + * @param stmt The statement to process. + * @param callerId The node ID of the calling function. + * @param contractName Name of the processed contract, if applicable. + */ + private processStatement( + stmt: AstStatement, + callerId: CGNodeId, + contractName?: string, + ) { + const funcNode = this.getNode(callerId); + if (!funcNode) return; + if (isContractStateWrite(stmt)) { + funcNode.addEffect(Effect.StateWrite); + } + if ( + stmt.kind === "statement_assign" || + stmt.kind === "statement_augmentedassign" + ) { + this.processExpression(stmt.expression, callerId, contractName); + } else + forEachExpression(stmt, (expr) => { + this.processExpression(expr, callerId, contractName); + }); + } + + /** + * Processes an expression, adding edges and setting effect flags as necessary. * @param expr The expression to process. * @param callerId The node ID of the calling function. * @param currentContractName The name of the contract, if applicable. @@ -342,6 +386,7 @@ export class CallGraph { callerId: CGNodeId, currentContractName?: string, ) { + // Connect CG nodes if (expr.kind === "static_call" || expr.kind === "method_call") { const functionName = this.getFunctionCallName( expr as AstStaticCall | AstMethodCall, @@ -356,6 +401,24 @@ export class CallGraph { ); } } + + // Add effects to the caller node + const funcNode = this.getNode(callerId); + if (!funcNode) return; + if (expr.kind === "static_call") { + const functionName = idText(expr.function); + if (DATETIME_NAMES.has(functionName)) + funcNode.addEffect(Effect.AccessDatetime); + else if ( + PRG_NATIVE_USE_NAMES.has(functionName) || + PRG_SAFE_USE_NAMES.has(functionName) + ) + funcNode.addEffect(Effect.PrgUse); + else if (PRG_INIT_NAMES.has(functionName)) + funcNode.addEffect(Effect.PrgSeedInit); + } + if (isSendCall(expr)) funcNode.addEffect(Effect.Send); + if (isContractStateRead(expr)) funcNode.addEffect(Effect.StateRead); } /** @@ -424,10 +487,59 @@ export class CallGraph { } /** - * Helper function to check if an expression represents 'self'. - * @param expr The expression to check. - * @returns True if the expression is 'self'; otherwise, false. + * Helper function to determine if an expression is a contract state read. + */ +function isContractStateRead(expr: AstExpression): boolean { + if (expr.kind === "field_access") { + const fieldAccess = expr as AstFieldAccess; + if (fieldAccess.aggregate.kind === "id") { + const idExpr = fieldAccess.aggregate as AstId; + if (idExpr.text === "self") { + return true; + } + } + } + return false; +} + +/** + * Helper function to determine if a statement is a contract state write. */ -export function isSelf(expr: AstExpression): boolean { - return expr.kind === "id" && (expr as AstId).text === "self"; +function isContractStateWrite(stmt: AstStatement): boolean { + if ( + stmt.kind === "statement_assign" || + stmt.kind === "statement_augmentedassign" + ) { + return isSelfAccess(stmt.path); + } + + // https://docs.tact-lang.org/book/maps/ + const MAP_MUTATING_OPERATIONS = new Set(["set", "del", "replace"]); + // For slices, cells, builders: + // https://github.com/tact-lang/tact/blob/08133e8418f3c6dcb49229b45cfeb7dd261bbe1f/stdlib/std/cells.tact#L75 + const CELL_MUTATING_OPERATIONS = new Set([ + "loadRef", + "loadBits", + "loadInt", + "loadUint", + "loadBool", + "loadBit", + "loadCoins", + "loadAddress", + "skipBits", + ]); + // Strings: + // https://github.com/tact-lang/tact/blob/08133e8418f3c6dcb49229b45cfeb7dd261bbe1f/stdlib/std/text.tact#L18 + const STRING_MUTATING_OPERATIONS = new Set(["append"]); + return ( + null !== + findInExpressions( + stmt, + (expr) => + expr.kind === "method_call" && + (MAP_MUTATING_OPERATIONS.has(idText(expr.method)) || + STRING_MUTATING_OPERATIONS.has(idText(expr.method)) || + CELL_MUTATING_OPERATIONS.has(idText(expr.method))), + ) + ); } diff --git a/src/internals/tact/util.ts b/src/internals/tact/util.ts index 08486b35..afeea8fa 100644 --- a/src/internals/tact/util.ts +++ b/src/internals/tact/util.ts @@ -23,6 +23,25 @@ import { prettyPrint } from "@tact-lang/compiler/dist/prettyPrinter"; import { Interval as RawInterval } from "ohm-js"; import * as path from "path"; +/** Stdlib functions that access datetime functions. */ +export const DATETIME_NAMES = new Set(["now", "timestamp"]); + +/** Stdlib functions that initialize PRG seed. */ +export const PRG_INIT_NAMES = new Set([ + "nativePrepareRandom", + "nativeRandomize", + "nativeRandomizeLt", +]); + +/** Native stdlib functions that use PRG. */ +export const PRG_NATIVE_USE_NAMES = new Set([ + "nativeRandom", + "nativeRandomInterval", +]); + +/** Safe Tact wrapper functions that use PRG. */ +export const PRG_SAFE_USE_NAMES = new Set(["random", "randomInt"]); + /** * Creates a concise string representation of `SrcInfo`. */ diff --git a/src/tools/dumpCallgraph.ts b/src/tools/dumpCallgraph.ts index bf3fd98a..d568c4c1 100644 --- a/src/tools/dumpCallgraph.ts +++ b/src/tools/dumpCallgraph.ts @@ -1,8 +1,8 @@ import { Tool } from "./tool"; import { ToolOutput } from "../cli/result"; import { MistiContext } from "../internals/context"; -import { CompilationUnit } from "../internals/ir"; -import { CallGraph } from "../internals/ir/callGraph"; +import { CompilationUnit, TactASTStore } from "../internals/ir"; +import { CallGraph, Effect } from "../internals/ir/callGraph"; import { unreachable } from "../internals/util"; import JSONbig from "json-bigint"; @@ -35,9 +35,15 @@ export class DumpCallGraph extends Tool { switch (format) { case "dot": - return this.makeOutput(cu, GraphvizDumper.dumpCallGraph(callGraph)); + return this.makeOutput( + cu, + GraphvizDumper.dumpCallGraph(cu.ast, callGraph), + ); case "mmd": - return this.makeOutput(cu, MermaidDumper.dumpCallGraph(callGraph)); + return this.makeOutput( + cu, + MermaidDumper.dumpCallGraph(cu.ast, callGraph), + ); case "json": return this.makeOutput(cu, JSONDumper.dumpCallGraph(callGraph)); default: @@ -60,15 +66,19 @@ export class DumpCallGraph extends Tool { * Utility class to dump the call graph in Mermaid format. */ class MermaidDumper { - public static dumpCallGraph(callGraph: CallGraph): string { + public static dumpCallGraph(ast: TactASTStore, callGraph: CallGraph): string { if (!callGraph || callGraph.getNodes().size === 0) { return 'graph TD\n empty["Empty Call Graph"]'; } let diagram = "graph TD\n"; callGraph.getNodes().forEach((node) => { const nodeId = `node_${node.idx}`; - const label = node.name?.replace(/"/g, '\\"') || "Unknown"; - diagram += ` ${nodeId}["${label}"]\n`; + const label = (node.signature(ast) || node.name || "Unknown").replace( + /"/g, + "'", + ); + const effects = getEffectsTooltip(node.effects); + diagram += ` ${nodeId}["${label}${effects}"]\n`; }); callGraph.getEdges().forEach((edge) => { const srcId = `node_${edge.src}`; @@ -83,15 +93,19 @@ class MermaidDumper { * Utility class to dump the call graph in DOT (Graphviz) format. */ class GraphvizDumper { - public static dumpCallGraph(callGraph: CallGraph): string { + public static dumpCallGraph(ast: TactASTStore, callGraph: CallGraph): string { if (!callGraph || callGraph.getNodes().size === 0) { return 'digraph "CallGraph" {\n node [shape=box];\n empty [label="Empty Call Graph"];\n}\n'; } let dot = `digraph "CallGraph" {\n node [shape=box];\n`; callGraph.getNodes().forEach((node) => { const nodeId = `node_${node.idx}`; - const label = node.name?.replace(/"/g, '\\"') || "Unknown"; - dot += ` ${nodeId} [label="${label}"];\n`; + const label = (node.signature(ast) || node.name || "Unknown").replace( + /"/g, + "'", + ); + const effects = getEffectsTooltip(node.effects); + dot += ` ${nodeId} [label="${label}${effects}"];\n`; }); callGraph.getEdges().forEach((edge) => { const srcId = `node_${edge.src}`; @@ -114,7 +128,7 @@ class JSONDumper { const data = { nodes: Array.from(callGraph.getNodes().values()).map((node) => ({ idx: node.idx, - name: node.name || "Unknown", + name: node.signature || node.name || "Unknown", inEdges: Array.from(node.inEdges || []), outEdges: Array.from(node.outEdges || []), })), @@ -127,3 +141,17 @@ class JSONDumper { return JSONbig.stringify(data, null, 2); } } + +function getEffectsTooltip(effects: number): string { + if (effects === 0) return ""; + + const effectsList = []; + if (effects & Effect.Send) effectsList.push("Send"); + if (effects & Effect.StateRead) effectsList.push("StateRead"); + if (effects & Effect.StateWrite) effectsList.push("StateWrite"); + if (effects & Effect.AccessDatetime) effectsList.push("AccessDatetime"); + if (effects & Effect.PrgUse) effectsList.push("PrgUse"); + if (effects & Effect.PrgSeedInit) effectsList.push("PrgSeedInit"); + + return effectsList.length > 0 ? `\n[${effectsList.join(",")}]` : ""; +} diff --git a/test/all/sample-jetton.expected.callgraph.dot b/test/all/sample-jetton.expected.callgraph.dot index aace0793..be8a94e0 100644 --- a/test/all/sample-jetton.expected.callgraph.dot +++ b/test/all/sample-jetton.expected.callgraph.dot @@ -1,28 +1,40 @@ digraph "CallGraph" { node [shape=box]; - node_1 [label="SampleJetton::contract_init_1817"]; - node_2 [label="SampleJetton::receiver_1857"]; - node_3 [label="SampleJetton::receiver_1882"]; - node_4 [label="SampleJetton::receiver_1905"]; - node_5 [label="JettonDefaultWallet::contract_init_2359"]; - node_6 [label="JettonDefaultWallet::receiver_2517"]; - node_7 [label="JettonDefaultWallet::receiver_2687"]; - node_8 [label="JettonDefaultWallet::msgValue"]; - node_9 [label="JettonDefaultWallet::receiver_2832"]; - node_10 [label="JettonDefaultWallet::receiver_2876"]; - node_11 [label="JettonDefaultWallet::get_wallet_data"]; + node_1 [label="init(owner: Address, content: Cell, max_supply: Int) +[StateWrite]"]; + node_2 [label="receive(msg: Mint) +[StateRead]"]; + node_3 [label="receive('Mint: 100') +[StateRead]"]; + node_4 [label="receive('Owner: MintClose') +[StateRead,StateWrite]"]; + node_5 [label="init(master: Address, owner: Address) +[StateWrite]"]; + node_6 [label="receive(msg: TokenTransfer) +[Send,StateRead,StateWrite]"]; + node_7 [label="receive(msg: TokenTransferInternal) +[Send,StateRead,StateWrite]"]; + node_8 [label="get fun msgValue(value: Int): Int +[StateRead]"]; + node_9 [label="receive(msg: TokenBurn) +[Send,StateRead,StateWrite]"]; + node_10 [label="bounced(msg: Slice) +[StateWrite]"]; + node_11 [label="get fun get_wallet_data(): JettonWalletData +[StateRead]"]; node_12 [label="context"]; node_13 [label="require"]; - node_14 [label="SampleJetton::mint"]; + node_14 [label="mint"]; node_15 [label="ctx::readForwardFee"]; node_16 [label="min"]; node_17 [label="ton"]; node_18 [label="contractAddress"]; node_19 [label="send"]; - node_20 [label="JettonDefaultWallet::toCell"]; - node_21 [label="myBalance"]; - node_22 [label="msg::loadUint"]; - node_23 [label="msg::loadCoins"]; + node_20 [label="toCell"]; + node_21 [label="msgValue"]; + node_22 [label="myBalance"]; + node_23 [label="msg::loadUint"]; + node_24 [label="msg::loadCoins"]; node_2 -> node_12; node_2 -> node_13; node_2 -> node_13; @@ -49,11 +61,11 @@ digraph "CallGraph" { node_7 -> node_13; node_7 -> node_19; node_7 -> node_20; - node_7 -> node_8; + node_7 -> node_21; node_7 -> node_15; node_7 -> node_19; node_7 -> node_20; - node_8 -> node_21; + node_8 -> node_22; node_8 -> node_16; node_9 -> node_12; node_9 -> node_13; @@ -62,8 +74,8 @@ digraph "CallGraph" { node_9 -> node_13; node_9 -> node_19; node_9 -> node_20; - node_10 -> node_22; - node_10 -> node_22; node_10 -> node_23; + node_10 -> node_23; + node_10 -> node_24; node_10 -> node_13; } diff --git a/test/all/sample-jetton.expected.callgraph.json b/test/all/sample-jetton.expected.callgraph.json index be183287..c7166931 100644 --- a/test/all/sample-jetton.expected.callgraph.json +++ b/test/all/sample-jetton.expected.callgraph.json @@ -2,13 +2,11 @@ "nodes": [ { "idx": 1, - "name": "SampleJetton::contract_init_1817", "inEdges": [], "outEdges": [] }, { "idx": 2, - "name": "SampleJetton::receiver_1857", "inEdges": [], "outEdges": [ 1, @@ -19,7 +17,6 @@ }, { "idx": 3, - "name": "SampleJetton::receiver_1882", "inEdges": [], "outEdges": [ 5, @@ -29,7 +26,6 @@ }, { "idx": 4, - "name": "SampleJetton::receiver_1905", "inEdges": [], "outEdges": [ 8, @@ -38,13 +34,11 @@ }, { "idx": 5, - "name": "JettonDefaultWallet::contract_init_2359", "inEdges": [], "outEdges": [] }, { "idx": 6, - "name": "JettonDefaultWallet::receiver_2517", "inEdges": [], "outEdges": [ 10, @@ -62,7 +56,6 @@ }, { "idx": 7, - "name": "JettonDefaultWallet::receiver_2687", "inEdges": [], "outEdges": [ 21, @@ -79,10 +72,7 @@ }, { "idx": 8, - "name": "JettonDefaultWallet::msgValue", - "inEdges": [ - 27 - ], + "inEdges": [], "outEdges": [ 31, 32 @@ -90,7 +80,6 @@ }, { "idx": 9, - "name": "JettonDefaultWallet::receiver_2832", "inEdges": [], "outEdges": [ 33, @@ -104,7 +93,6 @@ }, { "idx": 10, - "name": "JettonDefaultWallet::receiver_2876", "inEdges": [], "outEdges": [ 40, @@ -115,13 +103,11 @@ }, { "idx": 11, - "name": "JettonDefaultWallet::get_wallet_data", "inEdges": [], "outEdges": [] }, { "idx": 12, - "name": "context", "inEdges": [ 1, 5, @@ -134,7 +120,6 @@ }, { "idx": 13, - "name": "require", "inEdges": [ 2, 3, @@ -154,7 +139,6 @@ }, { "idx": 14, - "name": "SampleJetton::mint", "inEdges": [ 4, 7 @@ -163,7 +147,6 @@ }, { "idx": 15, - "name": "ctx::readForwardFee", "inEdges": [ 12, 13, @@ -174,7 +157,6 @@ }, { "idx": 16, - "name": "min", "inEdges": [ 15, 32 @@ -183,7 +165,6 @@ }, { "idx": 17, - "name": "ton", "inEdges": [ 16 ], @@ -191,7 +172,6 @@ }, { "idx": 18, - "name": "contractAddress", "inEdges": [ 18, 23 @@ -200,7 +180,6 @@ }, { "idx": 19, - "name": "send", "inEdges": [ 19, 25, @@ -211,7 +190,6 @@ }, { "idx": 20, - "name": "JettonDefaultWallet::toCell", "inEdges": [ 20, 26, @@ -222,15 +200,20 @@ }, { "idx": 21, - "name": "myBalance", "inEdges": [ - 31 + 27 ], "outEdges": [] }, { "idx": 22, - "name": "msg::loadUint", + "inEdges": [ + 31 + ], + "outEdges": [] + }, + { + "idx": 23, "inEdges": [ 40, 41 @@ -238,8 +221,7 @@ "outEdges": [] }, { - "idx": 23, - "name": "msg::loadCoins", + "idx": 24, "inEdges": [ 42 ], @@ -380,7 +362,7 @@ { "idx": 27, "src": 7, - "dst": 8 + "dst": 21 }, { "idx": 28, @@ -400,7 +382,7 @@ { "idx": 31, "src": 8, - "dst": 21 + "dst": 22 }, { "idx": 32, @@ -445,17 +427,17 @@ { "idx": 40, "src": 10, - "dst": 22 + "dst": 23 }, { "idx": 41, "src": 10, - "dst": 22 + "dst": 23 }, { "idx": 42, "src": 10, - "dst": 23 + "dst": 24 }, { "idx": 43, diff --git a/test/all/sample-jetton.expected.callgraph.mmd b/test/all/sample-jetton.expected.callgraph.mmd index 3cdb21bd..beb79431 100644 --- a/test/all/sample-jetton.expected.callgraph.mmd +++ b/test/all/sample-jetton.expected.callgraph.mmd @@ -1,27 +1,39 @@ graph TD - node_1["SampleJetton::contract_init_1817"] - node_2["SampleJetton::receiver_1857"] - node_3["SampleJetton::receiver_1882"] - node_4["SampleJetton::receiver_1905"] - node_5["JettonDefaultWallet::contract_init_2359"] - node_6["JettonDefaultWallet::receiver_2517"] - node_7["JettonDefaultWallet::receiver_2687"] - node_8["JettonDefaultWallet::msgValue"] - node_9["JettonDefaultWallet::receiver_2832"] - node_10["JettonDefaultWallet::receiver_2876"] - node_11["JettonDefaultWallet::get_wallet_data"] + node_1["init(owner: Address, content: Cell, max_supply: Int) +[StateWrite]"] + node_2["receive(msg: Mint) +[StateRead]"] + node_3["receive('Mint: 100') +[StateRead]"] + node_4["receive('Owner: MintClose') +[StateRead,StateWrite]"] + node_5["init(master: Address, owner: Address) +[StateWrite]"] + node_6["receive(msg: TokenTransfer) +[Send,StateRead,StateWrite]"] + node_7["receive(msg: TokenTransferInternal) +[Send,StateRead,StateWrite]"] + node_8["get fun msgValue(value: Int): Int +[StateRead]"] + node_9["receive(msg: TokenBurn) +[Send,StateRead,StateWrite]"] + node_10["bounced(msg: Slice) +[StateWrite]"] + node_11["get fun get_wallet_data(): JettonWalletData +[StateRead]"] node_12["context"] node_13["require"] - node_14["SampleJetton::mint"] + node_14["mint"] node_15["ctx::readForwardFee"] node_16["min"] node_17["ton"] node_18["contractAddress"] node_19["send"] - node_20["JettonDefaultWallet::toCell"] - node_21["myBalance"] - node_22["msg::loadUint"] - node_23["msg::loadCoins"] + node_20["toCell"] + node_21["msgValue"] + node_22["myBalance"] + node_23["msg::loadUint"] + node_24["msg::loadCoins"] node_2 --> node_12 node_2 --> node_13 node_2 --> node_13 @@ -48,11 +60,11 @@ graph TD node_7 --> node_13 node_7 --> node_19 node_7 --> node_20 - node_7 --> node_8 + node_7 --> node_21 node_7 --> node_15 node_7 --> node_19 node_7 --> node_20 - node_8 --> node_21 + node_8 --> node_22 node_8 --> node_16 node_9 --> node_12 node_9 --> node_13 @@ -61,7 +73,7 @@ graph TD node_9 --> node_13 node_9 --> node_19 node_9 --> node_20 - node_10 --> node_22 - node_10 --> node_22 node_10 --> node_23 + node_10 --> node_23 + node_10 --> node_24 node_10 --> node_13 diff --git a/test/all/syntax.expected.callgraph.dot b/test/all/syntax.expected.callgraph.dot index 565484dc..a96d3163 100644 --- a/test/all/syntax.expected.callgraph.dot +++ b/test/all/syntax.expected.callgraph.dot @@ -1,15 +1,46 @@ digraph "CallGraph" { node [shape=box]; - node_1 [label="test_try"]; - node_2 [label="test_loops"]; - node_3 [label="TestContract::getter"]; - node_4 [label="TestContractF::test"]; - node_5 [label="TestContractT::test"]; - node_6 [label="TestContractT::receiver_1722"]; - node_7 [label="dump"]; - node_8 [label="emptyMap"]; - node_9 [label="TestContractT::getA"]; - node_1 -> node_7; - node_2 -> node_8; - node_5 -> node_9; + node_1 [label="fun test_try(a: Int)"]; + node_2 [label="fun test_loops()"]; + node_3 [label="fun testTryCatch(a: Int)"]; + node_4 [label="fun testLoops() +[StateWrite]"]; + node_5 [label="override get fun getter(): Int"]; + node_6 [label="fun test()"]; + node_7 [label="fun test(): Int"]; + node_8 [label="external()"]; + node_9 [label="init() +[StateWrite]"]; + node_10 [label="fun funcWithSend() +[Send,StateRead]"]; + node_11 [label="fun funcWithStateRead() +[StateRead]"]; + node_12 [label="fun funcWithStateWrite() +[StateWrite]"]; + node_13 [label="fun funcWithMultipleEffects() +[AccessDatetime,PrgUse,PrgSeedInit]"]; + node_14 [label="dump"]; + node_15 [label="emptyMap"]; + node_16 [label="m::set"]; + node_17 [label="getA"]; + node_18 [label="sender"]; + node_19 [label="send"]; + node_20 [label="newAddress"]; + node_21 [label="now"]; + node_22 [label="random"]; + node_23 [label="nativeRandomizeLt"]; + node_1 -> node_14; + node_2 -> node_15; + node_3 -> node_14; + node_4 -> node_15; + node_4 -> node_16; + node_4 -> node_16; + node_4 -> node_16; + node_7 -> node_17; + node_9 -> node_18; + node_10 -> node_19; + node_12 -> node_20; + node_13 -> node_21; + node_13 -> node_22; + node_13 -> node_23; } diff --git a/test/all/syntax.expected.callgraph.json b/test/all/syntax.expected.callgraph.json index 346edc0b..d933ed51 100644 --- a/test/all/syntax.expected.callgraph.json +++ b/test/all/syntax.expected.callgraph.json @@ -2,7 +2,6 @@ "nodes": [ { "idx": 1, - "name": "test_try", "inEdges": [], "outEdges": [ 1 @@ -10,7 +9,6 @@ }, { "idx": 2, - "name": "test_loops", "inEdges": [], "outEdges": [ 2 @@ -18,51 +16,149 @@ }, { "idx": 3, - "name": "TestContract::getter", "inEdges": [], - "outEdges": [] + "outEdges": [ + 3 + ] }, { "idx": 4, - "name": "TestContractF::test", "inEdges": [], - "outEdges": [] + "outEdges": [ + 4, + 5, + 6, + 7 + ] }, { "idx": 5, - "name": "TestContractT::test", "inEdges": [], - "outEdges": [ - 3 - ] + "outEdges": [] }, { "idx": 6, - "name": "TestContractT::receiver_1722", "inEdges": [], "outEdges": [] }, { "idx": 7, - "name": "dump", + "inEdges": [], + "outEdges": [ + 8 + ] + }, + { + "idx": 8, + "inEdges": [], + "outEdges": [] + }, + { + "idx": 9, + "inEdges": [], + "outEdges": [ + 9 + ] + }, + { + "idx": 10, + "inEdges": [], + "outEdges": [ + 10 + ] + }, + { + "idx": 11, + "inEdges": [], + "outEdges": [] + }, + { + "idx": 12, + "inEdges": [], + "outEdges": [ + 11 + ] + }, + { + "idx": 13, + "inEdges": [], + "outEdges": [ + 12, + 13, + 14 + ] + }, + { + "idx": 14, "inEdges": [ - 1 + 1, + 3 ], "outEdges": [] }, { - "idx": 8, - "name": "emptyMap", + "idx": 15, "inEdges": [ - 2 + 2, + 4 ], "outEdges": [] }, { - "idx": 9, - "name": "TestContractT::getA", + "idx": 16, "inEdges": [ - 3 + 5, + 6, + 7 + ], + "outEdges": [] + }, + { + "idx": 17, + "inEdges": [ + 8 + ], + "outEdges": [] + }, + { + "idx": 18, + "inEdges": [ + 9 + ], + "outEdges": [] + }, + { + "idx": 19, + "inEdges": [ + 10 + ], + "outEdges": [] + }, + { + "idx": 20, + "inEdges": [ + 11 + ], + "outEdges": [] + }, + { + "idx": 21, + "inEdges": [ + 12 + ], + "outEdges": [] + }, + { + "idx": 22, + "inEdges": [ + 13 + ], + "outEdges": [] + }, + { + "idx": 23, + "inEdges": [ + 14 ], "outEdges": [] } @@ -71,17 +167,72 @@ { "idx": 1, "src": 1, - "dst": 7 + "dst": 14 }, { "idx": 2, "src": 2, - "dst": 8 + "dst": 15 }, { "idx": 3, - "src": 5, - "dst": 9 + "src": 3, + "dst": 14 + }, + { + "idx": 4, + "src": 4, + "dst": 15 + }, + { + "idx": 5, + "src": 4, + "dst": 16 + }, + { + "idx": 6, + "src": 4, + "dst": 16 + }, + { + "idx": 7, + "src": 4, + "dst": 16 + }, + { + "idx": 8, + "src": 7, + "dst": 17 + }, + { + "idx": 9, + "src": 9, + "dst": 18 + }, + { + "idx": 10, + "src": 10, + "dst": 19 + }, + { + "idx": 11, + "src": 12, + "dst": 20 + }, + { + "idx": 12, + "src": 13, + "dst": 21 + }, + { + "idx": 13, + "src": 13, + "dst": 22 + }, + { + "idx": 14, + "src": 13, + "dst": 23 } ] } \ No newline at end of file diff --git a/test/all/syntax.expected.callgraph.mmd b/test/all/syntax.expected.callgraph.mmd index 720a23bb..dc7d854c 100644 --- a/test/all/syntax.expected.callgraph.mmd +++ b/test/all/syntax.expected.callgraph.mmd @@ -1,13 +1,44 @@ graph TD - node_1["test_try"] - node_2["test_loops"] - node_3["TestContract::getter"] - node_4["TestContractF::test"] - node_5["TestContractT::test"] - node_6["TestContractT::receiver_1722"] - node_7["dump"] - node_8["emptyMap"] - node_9["TestContractT::getA"] - node_1 --> node_7 - node_2 --> node_8 - node_5 --> node_9 + node_1["fun test_try(a: Int)"] + node_2["fun test_loops()"] + node_3["fun testTryCatch(a: Int)"] + node_4["fun testLoops() +[StateWrite]"] + node_5["override get fun getter(): Int"] + node_6["fun test()"] + node_7["fun test(): Int"] + node_8["external()"] + node_9["init() +[StateWrite]"] + node_10["fun funcWithSend() +[Send,StateRead]"] + node_11["fun funcWithStateRead() +[StateRead]"] + node_12["fun funcWithStateWrite() +[StateWrite]"] + node_13["fun funcWithMultipleEffects() +[AccessDatetime,PrgUse,PrgSeedInit]"] + node_14["dump"] + node_15["emptyMap"] + node_16["m::set"] + node_17["getA"] + node_18["sender"] + node_19["send"] + node_20["newAddress"] + node_21["now"] + node_22["random"] + node_23["nativeRandomizeLt"] + node_1 --> node_14 + node_2 --> node_15 + node_3 --> node_14 + node_4 --> node_15 + node_4 --> node_16 + node_4 --> node_16 + node_4 --> node_16 + node_7 --> node_17 + node_9 --> node_18 + node_10 --> node_19 + node_12 --> node_20 + node_13 --> node_21 + node_13 --> node_22 + node_13 --> node_23 diff --git a/test/all/syntax.expected.cfg.dot b/test/all/syntax.expected.cfg.dot index 26e8f1c1..0d32e039 100644 --- a/test/all/syntax.expected.cfg.dot +++ b/test/all/syntax.expected.cfg.dot @@ -46,18 +46,95 @@ digraph "syntax" { "test_loops_85" -> "test_loops_86"; "test_loops_86" -> "test_loops_86"; } + subgraph "cluster_testTryCatch" { + label="testTryCatch"; + "testTryCatch_87" [label="try ... catch (err)"]; + "testTryCatch_88" [label="a += 1",style=filled,fillcolor="#66A7DB"]; + "testTryCatch_89" [label="dump(err)",style=filled,fillcolor="#66A7DB"]; + "testTryCatch_87" -> "testTryCatch_88"; + "testTryCatch_87" -> "testTryCatch_89"; + } + subgraph "cluster_testLoops" { + label="testLoops"; + "testLoops_90" [label="let sum: Int = 0"]; + "testLoops_91" [label="let i: Int = 0"]; + "testLoops_92" [label="while (i < 10)"]; + "testLoops_93" [label="i = i + 1"]; + "testLoops_94" [label="sum = sum + i"]; + "testLoops_95" [label="until (i <= 0)"]; + "testLoops_96" [label="i = i - 1"]; + "testLoops_97" [label="sum = sum + i"]; + "testLoops_98" [label="repeat (10)"]; + "testLoops_99" [label="i = i + 1"]; + "testLoops_100" [label="sum = sum + i"]; + "testLoops_101" [label="let m: map = emptyMap()"]; + "testLoops_102" [label="m.set(1, 10)"]; + "testLoops_103" [label="m.set(2, 20)"]; + "testLoops_104" [label="m.set(3, 30)"]; + "testLoops_105" [label="foreach ((key, value) of m)"]; + "testLoops_106" [label="sum = sum + value"]; + "testLoops_90" -> "testLoops_91"; + "testLoops_91" -> "testLoops_92"; + "testLoops_92" -> "testLoops_93"; + "testLoops_93" -> "testLoops_94"; + "testLoops_94" -> "testLoops_92"; + "testLoops_92" -> "testLoops_95"; + "testLoops_95" -> "testLoops_96"; + "testLoops_96" -> "testLoops_97"; + "testLoops_97" -> "testLoops_95"; + "testLoops_95" -> "testLoops_98"; + "testLoops_98" -> "testLoops_99"; + "testLoops_99" -> "testLoops_100"; + "testLoops_100" -> "testLoops_98"; + "testLoops_98" -> "testLoops_101"; + "testLoops_101" -> "testLoops_102"; + "testLoops_102" -> "testLoops_103"; + "testLoops_103" -> "testLoops_104"; + "testLoops_104" -> "testLoops_105"; + "testLoops_105" -> "testLoops_106"; + "testLoops_106" -> "testLoops_105"; + } subgraph "cluster_TestContract__getter" { label="TestContract__getter"; - "TestContract__getter_87" [label="return 0",style=filled,fillcolor="#66A7DB"]; + "TestContract__getter_107" [label="return 0",style=filled,fillcolor="#66A7DB"]; } subgraph "cluster_TestContractF__test" { label="TestContractF__test"; } subgraph "cluster_TestContractT__test" { label="TestContractT__test"; - "TestContractT__test_88" [label="return self.getA()",style=filled,fillcolor="#66A7DB"]; + "TestContractT__test_108" [label="return self.getA()",style=filled,fillcolor="#66A7DB"]; } subgraph "cluster_TestContractT__receive_external_fallback_1722" { label="TestContractT__receive_external_fallback_1722"; } + subgraph "cluster_EffectTest__init_1971" { + label="EffectTest__init_1971"; + "EffectTest__init_1971_109" [label="self.addr = sender()",style=filled,fillcolor="#66A7DB"]; + } + subgraph "cluster_EffectTest__funcWithSend" { + label="EffectTest__funcWithSend"; + "EffectTest__funcWithSend_110" [label="let amount: Int = 100"]; + "EffectTest__funcWithSend_111" [label="send(SendParameters{to: self.addr, value: amount})",style=filled,fillcolor="#66A7DB"]; + "EffectTest__funcWithSend_110" -> "EffectTest__funcWithSend_111"; + } + subgraph "cluster_EffectTest__funcWithStateRead" { + label="EffectTest__funcWithStateRead"; + "EffectTest__funcWithStateRead_112" [label="let value = self.addr",style=filled,fillcolor="#66A7DB"]; + } + subgraph "cluster_EffectTest__funcWithStateWrite" { + label="EffectTest__funcWithStateWrite"; + "EffectTest__funcWithStateWrite_113" [label="self.addr = newAddress(0, 0)",style=filled,fillcolor="#66A7DB"]; + } + subgraph "cluster_EffectTest__funcWithMultipleEffects" { + label="EffectTest__funcWithMultipleEffects"; + "EffectTest__funcWithMultipleEffects_114" [label="let currentTime: Int = now()"]; + "EffectTest__funcWithMultipleEffects_115" [label="let randValue: Int = random(1, 100)"]; + "EffectTest__funcWithMultipleEffects_116" [label="if (1 > 42)"]; + "EffectTest__funcWithMultipleEffects_117" [label="nativeRandomizeLt()",style=filled,fillcolor="#66A7DB"]; + "EffectTest__funcWithMultipleEffects_114" -> "EffectTest__funcWithMultipleEffects_115"; + "EffectTest__funcWithMultipleEffects_115" -> "EffectTest__funcWithMultipleEffects_116"; + "EffectTest__funcWithMultipleEffects_116" -> "EffectTest__funcWithMultipleEffects_117"; + } +"115" -> "26"; } diff --git a/test/all/syntax.expected.cfg.json b/test/all/syntax.expected.cfg.json index b45bd0b2..4e6f75bf 100644 --- a/test/all/syntax.expected.cfg.json +++ b/test/all/syntax.expected.cfg.json @@ -313,6 +313,334 @@ } ] } + }, + { + "name": "testTryCatch", + "cfg": { + "nodes": [ + { + "id": 87, + "stmtID": 1817, + "srcEdges": [], + "dstEdges": [ + 78, + 79 + ] + }, + { + "id": 88, + "stmtID": 1811, + "srcEdges": [ + 78 + ], + "dstEdges": [] + }, + { + "id": 89, + "stmtID": 1816, + "srcEdges": [ + 79 + ], + "dstEdges": [] + } + ], + "edges": [ + { + "id": 78, + "src": 87, + "dst": 88 + }, + { + "id": 79, + "src": 87, + "dst": 89 + } + ] + } + }, + { + "name": "testLoops", + "cfg": { + "nodes": [ + { + "id": 90, + "stmtID": 1823, + "srcEdges": [], + "dstEdges": [ + 80 + ] + }, + { + "id": 91, + "stmtID": 1827, + "srcEdges": [ + 80 + ], + "dstEdges": [ + 81 + ] + }, + { + "id": 92, + "stmtID": 1841, + "srcEdges": [ + 81, + 84 + ], + "dstEdges": [ + 82, + 85 + ] + }, + { + "id": 93, + "stmtID": 1835, + "srcEdges": [ + 82 + ], + "dstEdges": [ + 83 + ] + }, + { + "id": 94, + "stmtID": 1840, + "srcEdges": [ + 83 + ], + "dstEdges": [ + 84 + ] + }, + { + "id": 95, + "stmtID": 1855, + "srcEdges": [ + 85, + 88 + ], + "dstEdges": [ + 86, + 89 + ] + }, + { + "id": 96, + "stmtID": 1849, + "srcEdges": [ + 86 + ], + "dstEdges": [ + 87 + ] + }, + { + "id": 97, + "stmtID": 1854, + "srcEdges": [ + 87 + ], + "dstEdges": [ + 88 + ] + }, + { + "id": 98, + "stmtID": 1867, + "srcEdges": [ + 89, + 92 + ], + "dstEdges": [ + 90, + 93 + ] + }, + { + "id": 99, + "stmtID": 1861, + "srcEdges": [ + 90 + ], + "dstEdges": [ + 91 + ] + }, + { + "id": 100, + "stmtID": 1866, + "srcEdges": [ + 91 + ], + "dstEdges": [ + 92 + ] + }, + { + "id": 101, + "stmtID": 1874, + "srcEdges": [ + 93 + ], + "dstEdges": [ + 94 + ] + }, + { + "id": 102, + "stmtID": 1880, + "srcEdges": [ + 94 + ], + "dstEdges": [ + 95 + ] + }, + { + "id": 103, + "stmtID": 1886, + "srcEdges": [ + 95 + ], + "dstEdges": [ + 96 + ] + }, + { + "id": 104, + "stmtID": 1892, + "srcEdges": [ + 96 + ], + "dstEdges": [ + 97 + ] + }, + { + "id": 105, + "stmtID": 1901, + "srcEdges": [ + 97, + 99 + ], + "dstEdges": [ + 98 + ] + }, + { + "id": 106, + "stmtID": 1900, + "srcEdges": [ + 98 + ], + "dstEdges": [ + 99 + ] + } + ], + "edges": [ + { + "id": 80, + "src": 90, + "dst": 91 + }, + { + "id": 81, + "src": 91, + "dst": 92 + }, + { + "id": 82, + "src": 92, + "dst": 93 + }, + { + "id": 83, + "src": 93, + "dst": 94 + }, + { + "id": 84, + "src": 94, + "dst": 92 + }, + { + "id": 85, + "src": 92, + "dst": 95 + }, + { + "id": 86, + "src": 95, + "dst": 96 + }, + { + "id": 87, + "src": 96, + "dst": 97 + }, + { + "id": 88, + "src": 97, + "dst": 95 + }, + { + "id": 89, + "src": 95, + "dst": 98 + }, + { + "id": 90, + "src": 98, + "dst": 99 + }, + { + "id": 91, + "src": 99, + "dst": 100 + }, + { + "id": 92, + "src": 100, + "dst": 98 + }, + { + "id": 93, + "src": 98, + "dst": 101 + }, + { + "id": 94, + "src": 101, + "dst": 102 + }, + { + "id": 95, + "src": 102, + "dst": 103 + }, + { + "id": 96, + "src": 103, + "dst": 104 + }, + { + "id": 97, + "src": 104, + "dst": 105 + }, + { + "id": 98, + "src": 105, + "dst": 106 + }, + { + "id": 99, + "src": 106, + "dst": 105 + } + ] + } } ], "contracts": [ @@ -324,7 +652,7 @@ "cfg": { "nodes": [ { - "id": 87, + "id": 107, "stmtID": 1662, "srcEdges": [], "dstEdges": [] @@ -355,7 +683,7 @@ "cfg": { "nodes": [ { - "id": 88, + "id": 108, "stmtID": 1720, "srcEdges": [], "dstEdges": [] @@ -372,6 +700,143 @@ } } ] + }, + { + "name": "EffectTest", + "methods": [ + { + "name": "EffectTest.init_1971", + "cfg": { + "nodes": [ + { + "id": 109, + "stmtID": 1912, + "srcEdges": [], + "dstEdges": [] + } + ], + "edges": [] + } + }, + { + "name": "EffectTest.funcWithSend", + "cfg": { + "nodes": [ + { + "id": 110, + "stmtID": 1918, + "srcEdges": [], + "dstEdges": [ + 100 + ] + }, + { + "id": 111, + "stmtID": 1931, + "srcEdges": [ + 100 + ], + "dstEdges": [] + } + ], + "edges": [ + { + "id": 100, + "src": 110, + "dst": 111 + } + ] + } + }, + { + "name": "EffectTest.funcWithStateRead", + "cfg": { + "nodes": [ + { + "id": 112, + "stmtID": 1938, + "srcEdges": [], + "dstEdges": [] + } + ], + "edges": [] + } + }, + { + "name": "EffectTest.funcWithStateWrite", + "cfg": { + "nodes": [ + { + "id": 113, + "stmtID": 1948, + "srcEdges": [], + "dstEdges": [] + } + ], + "edges": [] + } + }, + { + "name": "EffectTest.funcWithMultipleEffects", + "cfg": { + "nodes": [ + { + "id": 114, + "stmtID": 1955, + "srcEdges": [], + "dstEdges": [ + 101 + ] + }, + { + "id": 115, + "stmtID": 1962, + "srcEdges": [ + 101 + ], + "dstEdges": [ + 102 + ] + }, + { + "id": 116, + "stmtID": 1969, + "srcEdges": [ + 102 + ], + "dstEdges": [ + 103 + ] + }, + { + "id": 117, + "stmtID": 1968, + "srcEdges": [ + 103 + ], + "dstEdges": [] + } + ], + "edges": [ + { + "id": 101, + "src": 114, + "dst": 115 + }, + { + "id": 102, + "src": 115, + "dst": 116 + }, + { + "id": 103, + "src": 116, + "dst": 117 + } + ] + } + } + ] } ] } \ No newline at end of file diff --git a/test/all/syntax.expected.cfg.mmd b/test/all/syntax.expected.cfg.mmd index 34217d53..1195903c 100644 --- a/test/all/syntax.expected.cfg.mmd +++ b/test/all/syntax.expected.cfg.mmd @@ -43,13 +43,82 @@ subgraph test_loops test_loops_85 --> test_loops_86 test_loops_86 --> test_loops_86 end +subgraph testTryCatch + testTryCatch_87["try ... catch (err)"] + testTryCatch_88["a += 1"]:::exitNode + testTryCatch_89["dump(err)"]:::exitNode + testTryCatch_87 --> testTryCatch_88 + testTryCatch_87 --> testTryCatch_89 +end +subgraph testLoops + testLoops_90["let sum: Int = 0"] + testLoops_91["let i: Int = 0"] + testLoops_92["while (i < 10)"] + testLoops_93["i = i + 1"] + testLoops_94["sum = sum + i"] + testLoops_95["until (i <= 0)"] + testLoops_96["i = i - 1"] + testLoops_97["sum = sum + i"] + testLoops_98["repeat (10)"] + testLoops_99["i = i + 1"] + testLoops_100["sum = sum + i"] + testLoops_101["let m: map<Int, Int> = emptyMap()"] + testLoops_102["m.set(1, 10)"] + testLoops_103["m.set(2, 20)"] + testLoops_104["m.set(3, 30)"] + testLoops_105["foreach ((key, value) of m)"] + testLoops_106["sum = sum + value"] + testLoops_90 --> testLoops_91 + testLoops_91 --> testLoops_92 + testLoops_92 --> testLoops_93 + testLoops_93 --> testLoops_94 + testLoops_94 --> testLoops_92 + testLoops_92 --> testLoops_95 + testLoops_95 --> testLoops_96 + testLoops_96 --> testLoops_97 + testLoops_97 --> testLoops_95 + testLoops_95 --> testLoops_98 + testLoops_98 --> testLoops_99 + testLoops_99 --> testLoops_100 + testLoops_100 --> testLoops_98 + testLoops_98 --> testLoops_101 + testLoops_101 --> testLoops_102 + testLoops_102 --> testLoops_103 + testLoops_103 --> testLoops_104 + testLoops_104 --> testLoops_105 + testLoops_105 --> testLoops_106 + testLoops_106 --> testLoops_105 +end subgraph TestContract__getter - TestContract__getter_87["return 0"]:::exitNode + TestContract__getter_107["return 0"]:::exitNode end subgraph TestContractF__test end subgraph TestContractT__test - TestContractT__test_88["return self.getA()"]:::exitNode + TestContractT__test_108["return self.getA()"]:::exitNode end subgraph TestContractT__receive_external_fallback_1722 end +subgraph EffectTest__init_1971 + EffectTest__init_1971_109["self.addr = sender()"]:::exitNode +end +subgraph EffectTest__funcWithSend + EffectTest__funcWithSend_110["let amount: Int = 100"] + EffectTest__funcWithSend_111["send(SendParameters{to: self.addr, value: amount})"]:::exitNode + EffectTest__funcWithSend_110 --> EffectTest__funcWithSend_111 +end +subgraph EffectTest__funcWithStateRead + EffectTest__funcWithStateRead_112["let value = self.addr"]:::exitNode +end +subgraph EffectTest__funcWithStateWrite + EffectTest__funcWithStateWrite_113["self.addr = newAddress(0, 0)"]:::exitNode +end +subgraph EffectTest__funcWithMultipleEffects + EffectTest__funcWithMultipleEffects_114["let currentTime: Int = now()"] + EffectTest__funcWithMultipleEffects_115["let randValue: Int = random(1, 100)"] + EffectTest__funcWithMultipleEffects_116["if (1 > 42)"] + EffectTest__funcWithMultipleEffects_117["nativeRandomizeLt()"]:::exitNode + EffectTest__funcWithMultipleEffects_114 --> EffectTest__funcWithMultipleEffects_115 + EffectTest__funcWithMultipleEffects_115 --> EffectTest__funcWithMultipleEffects_116 + EffectTest__funcWithMultipleEffects_116 --> EffectTest__funcWithMultipleEffects_117 +end diff --git a/test/all/syntax.expected.out b/test/all/syntax.expected.out index 5161159e..e97d137f 100644 --- a/test/all/syntax.expected.out +++ b/test/all/syntax.expected.out @@ -1,3 +1,48 @@ +[MEDIUM] NeverAccessedVariables: Variable value is never accessed +test/all/syntax.tact:114:9: + 113 | fun funcWithStateRead() { +> 114 | let value = self.addr; + ^ + 115 | } +Help: Consider removing the variable +See: https://nowarp.io/tools/misti/docs/detectors/NeverAccessedVariables + +[MEDIUM] NeverAccessedVariables: Variable currentTime is never accessed +test/all/syntax.tact:125:9: + 124 | fun funcWithMultipleEffects() { +> 125 | let currentTime: Int = now(); + ^ + 126 | let randValue: Int = random(1, 100); +Help: Consider removing the variable +See: https://nowarp.io/tools/misti/docs/detectors/NeverAccessedVariables + +[MEDIUM] NeverAccessedVariables: Variable randValue is never accessed +test/all/syntax.tact:126:9: + 125 | let currentTime: Int = now(); +> 126 | let randValue: Int = random(1, 100); + ^ + 127 | if (1 > 42) { +Help: Consider removing the variable +See: https://nowarp.io/tools/misti/docs/detectors/NeverAccessedVariables + +[MEDIUM] FalseCondition: Condition always evaluates to false +test/all/syntax.tact:127:13: + 126 | let randValue: Int = random(1, 100); +> 127 | if (1 > 42) { + ^ + 128 | nativeRandomizeLt(); +Help: Consider removing it if there is no logic error +See: https://nowarp.io/tools/misti/docs/detectors/FalseCondition + +[LOW] ZeroAddress: Using zero address +test/all/syntax.tact:119:35: + 118 | fun funcWithStateWrite() { +> 119 | self.addr = newAddress(0, 0); + ^ + 120 | } +Help: Consider changing code to avoid using it. For example, you could pass the address during the deployment. +See: https://nowarp.io/tools/misti/docs/detectors/ZeroAddress + [INFO] DumpIsUsed: Found `dump` usage test/all/syntax.tact:41:35: 40 | try { /* empty */ } @@ -7,6 +52,15 @@ test/all/syntax.tact:41:35: Help: Using `dump` in production code can sometimes indicate complex code that requires additional review See: https://nowarp.io/tools/misti/docs/detectors/DumpIsUsed +[INFO] DumpIsUsed: Found `dump` usage +test/all/syntax.tact:67:9: + 66 | } catch (err) { +> 67 | dump(err); + ^ + 68 | } +Help: Using `dump` in production code can sometimes indicate complex code that requires additional review +See: https://nowarp.io/tools/misti/docs/detectors/DumpIsUsed + [INFO] PreferAugmentedAssign: Prefer augmented assignment: i += 1 test/all/syntax.tact:48:7: 47 | while (i < 10) { @@ -59,4 +113,67 @@ test/all/syntax.tact:57:7: ^ 58 | } Help: Consider using augmented assignment instead: sum += i +See: https://nowarp.io/tools/misti/docs/detectors/PreferAugmentedAssign + +[INFO] PreferAugmentedAssign: Prefer augmented assignment: i += 1 +test/all/syntax.tact:76:9: + 75 | while (i < 10) { +> 76 | i = i + 1; + ^ + 77 | sum = sum + i; +Help: Consider using augmented assignment instead: i += 1 +See: https://nowarp.io/tools/misti/docs/detectors/PreferAugmentedAssign + +[INFO] PreferAugmentedAssign: Prefer augmented assignment: sum += i +test/all/syntax.tact:77:9: + 76 | i = i + 1; +> 77 | sum = sum + i; + ^ + 78 | } +Help: Consider using augmented assignment instead: sum += i +See: https://nowarp.io/tools/misti/docs/detectors/PreferAugmentedAssign + +[INFO] PreferAugmentedAssign: Prefer augmented assignment: i -= 1 +test/all/syntax.tact:81:9: + 80 | do { +> 81 | i = i - 1; + ^ + 82 | sum = sum + i; +Help: Consider using augmented assignment instead: i -= 1 +See: https://nowarp.io/tools/misti/docs/detectors/PreferAugmentedAssign + +[INFO] PreferAugmentedAssign: Prefer augmented assignment: sum += i +test/all/syntax.tact:82:9: + 81 | i = i - 1; +> 82 | sum = sum + i; + ^ + 83 | } until (i <= 0); +Help: Consider using augmented assignment instead: sum += i +See: https://nowarp.io/tools/misti/docs/detectors/PreferAugmentedAssign + +[INFO] PreferAugmentedAssign: Prefer augmented assignment: i += 1 +test/all/syntax.tact:86:9: + 85 | repeat (10) { +> 86 | i = i + 1; + ^ + 87 | sum = sum + i; +Help: Consider using augmented assignment instead: i += 1 +See: https://nowarp.io/tools/misti/docs/detectors/PreferAugmentedAssign + +[INFO] PreferAugmentedAssign: Prefer augmented assignment: sum += i +test/all/syntax.tact:87:9: + 86 | i = i + 1; +> 87 | sum = sum + i; + ^ + 88 | } +Help: Consider using augmented assignment instead: sum += i +See: https://nowarp.io/tools/misti/docs/detectors/PreferAugmentedAssign + +[INFO] PreferAugmentedAssign: Prefer augmented assignment: sum += value +test/all/syntax.tact:96:9: + 95 | foreach (key, value in m) { +> 96 | sum = sum + value; + ^ + 97 | } +Help: Consider using augmented assignment instead: sum += value See: https://nowarp.io/tools/misti/docs/detectors/PreferAugmentedAssign \ No newline at end of file diff --git a/test/all/syntax.tact b/test/all/syntax.tact index 816aa9ac..427ab840 100644 --- a/test/all/syntax.tact +++ b/test/all/syntax.tact @@ -59,3 +59,73 @@ fun test_loops() { let m: map = emptyMap(); foreach (k, v in m) {} } + +fun testTryCatch(a: Int) { + try { + a += 1; + } catch (err) { + dump(err); + } +} + +fun testLoops() { + let sum: Int = 0; + let i: Int = 0; + + while (i < 10) { + i = i + 1; + sum = sum + i; + } + + do { + i = i - 1; + sum = sum + i; + } until (i <= 0); + + repeat (10) { + i = i + 1; + sum = sum + i; + } + + let m: map = emptyMap(); + m.set(1, 10); + m.set(2, 20); + m.set(3, 30); + + foreach (key, value in m) { + sum = sum + value; + } +} + +// Test callgraph effects +contract EffectTest { + addr: Address; + // Effect.StateWrite + init() { self.addr = sender(); } + + // Effect.Send + Effect.StateRead + fun funcWithSend() { + let amount: Int = 100; + send(SendParameters{ to: self.addr, value: amount }); + } + + // Effect.StateRead + fun funcWithStateRead() { + let value = self.addr; + } + + // Effect.StateWrite + fun funcWithStateWrite() { + self.addr = newAddress(0, 0); + } + + // Multiple effects: + // Effect.AccessDatetime + Effect.PrgUse + Effect.SeedInit + fun funcWithMultipleEffects() { + let currentTime: Int = now(); + let randValue: Int = random(1, 100); + if (1 > 42) { + nativeRandomizeLt(); + } + } +}