diff --git a/slither/core/declarations/contract.py b/slither/core/declarations/contract.py index 7ee3dcc062..56f2733eb1 100644 --- a/slither/core/declarations/contract.py +++ b/slither/core/declarations/contract.py @@ -1489,6 +1489,7 @@ def add_constructor_variables(self) -> None: next_node.add_father(prev_node) prev_node = next_node counter += 1 + self._rewrite_ternary_in_constructor_variables(constructor_variable, counter) break for idx, variable_candidate in enumerate(self.state_variables): @@ -1550,6 +1551,97 @@ def _create_node( node.add_expression(expression) return node + @staticmethod + def _rewrite_ternary_in_constructor_variables(func: Function, counter: int) -> None: + """Rewrite ternary expressions in constructor variable initialization nodes. + + State variable initializers are processed outside the normal function parsing + pipeline, so ternary expressions are not rewritten by _rewrite_ternary_as_if_else. + This method performs the equivalent transformation for constructor variable nodes. + """ + from slither.core.cfg.node import Node, NodeType + from slither.visitors.expression.has_conditional import HasConditional + from slither.utils.expression_manipulations import SplitTernaryExpression + + ternary_found = True + while ternary_found: + ternary_found = False + for node in list(func.nodes): + if not node.expression: + continue + + has_cond = HasConditional(node.expression) + if not has_cond.result(): + continue + + st = SplitTernaryExpression(node.expression) + condition = st.condition + if not condition: + continue + + true_expr = st.true_expression + false_expr = st.false_expression + + # Create IF node + condition_node = Node(NodeType.IF, counter, node.scope, func.file_scope) + counter += 1 + condition_node.set_offset(node.source_mapping, func.compilation_unit) + condition_node.set_function(func) + condition_node.add_expression(condition) + + # Create true branch + true_node = Node(NodeType.EXPRESSION, counter, node.scope, func.file_scope) + counter += 1 + true_node.set_offset(node.source_mapping, func.compilation_unit) + true_node.set_function(func) + true_node.add_expression(true_expr) + + # Create false branch + false_node = Node(NodeType.EXPRESSION, counter, node.scope, func.file_scope) + counter += 1 + false_node.set_offset(node.source_mapping, func.compilation_unit) + false_node.set_function(func) + false_node.add_expression(false_expr) + + # Create ENDIF + endif_node = Node(NodeType.ENDIF, counter, node.scope, func.file_scope) + counter += 1 + endif_node.set_offset(node.source_mapping, func.compilation_unit) + endif_node.set_function(func) + + # Rewire CFG: connect fathers to condition node + for father in node.fathers: + father.replace_son(node, condition_node) + condition_node.add_father(father) + + # Rewire CFG: connect sons to endif node + for son in node.sons: + son.remove_father(node) + son.add_father(endif_node) + endif_node.add_son(son) + + # Wire IF -> true/false branches -> ENDIF + condition_node.add_son(true_node) + true_node.add_father(condition_node) + condition_node.add_son(false_node) + false_node.add_father(condition_node) + true_node.add_son(endif_node) + endif_node.add_father(true_node) + false_node.add_son(endif_node) + endif_node.add_father(false_node) + + # Update entry point if the rewritten node was the entry + if func.entry_point == node: + func.entry_point = condition_node + + # Replace old node with new nodes + new_nodes = [n for n in func.nodes if n.node_id != node.node_id] + new_nodes.extend([condition_node, true_node, false_node, endif_node]) + func.nodes = new_nodes + + ternary_found = True + break + # endregion ################################################################################### ################################################################################### diff --git a/tests/unit/core/test_data/ternary_in_state_var/ternary_state_var.sol b/tests/unit/core/test_data/ternary_in_state_var/ternary_state_var.sol new file mode 100644 index 0000000000..d6906695d8 --- /dev/null +++ b/tests/unit/core/test_data/ternary_in_state_var/ternary_state_var.sol @@ -0,0 +1,18 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.0; + +contract A { + struct S { + uint32 user; + } +} + +// Ternary expression in state variable initializer with struct member access. +// Before the fix, this caused: +// SlithIRError: Ternary operator are not convertible to SlithIR +// because constructor variable nodes were not processed by ternary rewriting. +contract B { + A.S a; + A.S b; + uint32 result = (true ? a.user : b.user); +} diff --git a/tests/unit/core/test_ternary_state_var.py b/tests/unit/core/test_ternary_state_var.py new file mode 100644 index 0000000000..453dd5c040 --- /dev/null +++ b/tests/unit/core/test_ternary_state_var.py @@ -0,0 +1,33 @@ +"""Test that ternary expressions in state variable initializers are handled correctly. + +Regression test for https://github.com/crytic/slither/issues/2836 +""" + +from pathlib import Path + +from crytic_compile import CryticCompile +from crytic_compile.platform.solc_standard_json import SolcStandardJson + +from slither import Slither + +TEST_DATA_DIR = Path(__file__).resolve().parent / "test_data" / "ternary_in_state_var" + + +def test_ternary_in_state_variable_initializer(solc_binary_path) -> None: + """Ensure ternary expressions in state variable initializers don't crash. + + Before the fix, a ConditionalExpression in a state variable initializer + would reach SlithIR conversion without being split, causing SlithIRError. + """ + solc_path = solc_binary_path("0.8.0") + standard_json = SolcStandardJson() + for source_file in TEST_DATA_DIR.rglob("**/*.sol"): + standard_json.add_source_file(Path(source_file).as_posix()) + compilation = CryticCompile(standard_json, solc=solc_path) + sl = Slither(compilation) + + # Verify contract B was parsed successfully + contract_b = next(c for c in sl.contracts if c.name == "B") + result_var = next(v for v in contract_b.state_variables if v.name == "result") + assert result_var is not None + assert "uint32" in str(result_var.type)