Merge pull request #2376 from crytic/feat/virtual-override-with-refs

This PR improves the reference API to account for virtual inheritance and abstract contracts. It treats each virtual override defintion and contract definition that inherits from an interface or abstract contract as an "implementation". This lays the groundwork for LSP features and unused definition/ import detector.
pull/2385/head
alpharush 8 months ago committed by GitHub
commit 2ad318c7c0
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
  1. 4
      setup.py
  2. 29
      slither/core/declarations/contract.py
  3. 49
      slither/core/declarations/function.py
  4. 55
      slither/core/slither_core.py
  5. 12
      slither/printers/summary/declaration.py
  6. 5
      slither/slithir/convert.py
  7. 17
      slither/solc_parsing/declarations/contract.py
  8. 30
      slither/solc_parsing/declarations/function.py
  9. 24
      slither/solc_parsing/slither_compilation_unit_solc.py
  10. 49
      slither/utils/source_mapping.py
  11. 22
      tests/unit/core/test_contract_declaration.py
  12. 16
      tests/unit/core/test_data/src_mapping/TopLevelReferences.sol
  13. 65
      tests/unit/core/test_data/virtual_overrides.sol
  14. 88
      tests/unit/core/test_source_mapping.py
  15. 151
      tests/unit/core/test_virtual_overrides.py

@ -15,8 +15,8 @@ setup(
"packaging",
"prettytable>=3.3.0",
"pycryptodome>=3.4.6",
"crytic-compile>=0.3.5,<0.4.0",
# "crytic-compile@git+https://github.com/crytic/crytic-compile.git@master#egg=crytic-compile",
# "crytic-compile>=0.3.5,<0.4.0",
"crytic-compile@git+https://github.com/crytic/crytic-compile.git@master#egg=crytic-compile",
"web3>=6.0.0",
"eth-abi>=4.0.0",
"eth-typing>=3.0.0",

@ -93,6 +93,7 @@ class Contract(SourceMapping): # pylint: disable=too-many-public-methods
self._is_interface: bool = False
self._is_library: bool = False
self._is_fully_implemented: bool = False
self._is_abstract: bool = False
self._signatures: Optional[List[str]] = None
self._signatures_declared: Optional[List[str]] = None
@ -203,12 +204,34 @@ class Contract(SourceMapping): # pylint: disable=too-many-public-methods
@property
def is_fully_implemented(self) -> bool:
"""
bool: True if the contract defines all functions.
In modern Solidity, virtual functions can lack an implementation.
Prior to Solidity 0.6.0, functions like the following would be not fully implemented:
```solidity
contract ImplicitAbstract{
function f() public;
}
```
"""
return self._is_fully_implemented
@is_fully_implemented.setter
def is_fully_implemented(self, is_fully_implemented: bool):
self._is_fully_implemented = is_fully_implemented
@property
def is_abstract(self) -> bool:
"""
Note for Solidity < 0.6.0 it will always be false
bool: True if the contract is abstract.
"""
return self._is_abstract
@is_abstract.setter
def is_abstract(self, is_abstract: bool):
self._is_abstract = is_abstract
# endregion
###################################################################################
###################################################################################
@ -996,16 +1019,14 @@ class Contract(SourceMapping): # pylint: disable=too-many-public-methods
def get_functions_overridden_by(self, function: "Function") -> List["Function"]:
"""
Return the list of functions overriden by the function
Return the list of functions overridden by the function
Args:
(core.Function)
Returns:
list(core.Function)
"""
candidatess = [c.functions_declared for c in self.inheritance]
candidates = [candidate for sublist in candidatess for candidate in sublist]
return [f for f in candidates if f.full_name == function.full_name]
return function.overrides
# endregion
###################################################################################

@ -37,7 +37,7 @@ if TYPE_CHECKING:
HighLevelCallType,
LibraryCallType,
)
from slither.core.declarations import Contract
from slither.core.declarations import Contract, FunctionContract
from slither.core.cfg.node import Node, NodeType
from slither.core.variables.variable import Variable
from slither.slithir.variables.variable import SlithIRVariable
@ -46,7 +46,6 @@ if TYPE_CHECKING:
from slither.slithir.operations import Operation
from slither.core.compilation_unit import SlitherCompilationUnit
from slither.core.scope.scope import FileScope
from slither.slithir.variables.state_variable import StateIRVariable
LOGGER = logging.getLogger("Function")
ReacheableNode = namedtuple("ReacheableNode", ["node", "ir"])
@ -126,6 +125,9 @@ class Function(SourceMapping, metaclass=ABCMeta): # pylint: disable=too-many-pu
self._pure: bool = False
self._payable: bool = False
self._visibility: Optional[str] = None
self._virtual: bool = False
self._overrides: List["FunctionContract"] = []
self._overridden_by: List["FunctionContract"] = []
self._is_implemented: Optional[bool] = None
self._is_empty: Optional[bool] = None
@ -441,6 +443,49 @@ class Function(SourceMapping, metaclass=ABCMeta): # pylint: disable=too-many-pu
def payable(self, p: bool):
self._payable = p
# endregion
###################################################################################
###################################################################################
# region Virtual
###################################################################################
###################################################################################
@property
def is_virtual(self) -> bool:
"""
Note for Solidity < 0.6.0 it will always be false
bool: True if the function is virtual
"""
return self._virtual
@is_virtual.setter
def is_virtual(self, v: bool):
self._virtual = v
@property
def is_override(self) -> bool:
"""
Note for Solidity < 0.6.0 it will always be false
bool: True if the function overrides a base function
"""
return len(self._overrides) > 0
@property
def overridden_by(self) -> List["FunctionContract"]:
"""
List["FunctionContract"]: List of functions in child contracts that override this function
This may include distinct instances of the same function due to inheritance
"""
return self._overridden_by
@property
def overrides(self) -> List["FunctionContract"]:
"""
List["FunctionContract"]: List of functions in parent contracts that this function overrides
This may include distinct instances of the same function due to inheritance
"""
return self._overrides
# endregion
###################################################################################
###################################################################################

@ -22,7 +22,7 @@ from slither.core.source_mapping.source_mapping import SourceMapping, Source
from slither.slithir.variables import Constant
from slither.utils.colors import red
from slither.utils.sarif import read_triage_info
from slither.utils.source_mapping import get_definition, get_references, get_implementation
from slither.utils.source_mapping import get_definition, get_references, get_all_implementations
logger = logging.getLogger("Slither")
logging.basicConfig()
@ -204,41 +204,53 @@ class SlitherCore(Context):
def _compute_offsets_from_thing(self, thing: SourceMapping):
definition = get_definition(thing, self.crytic_compile)
references = get_references(thing)
implementation = get_implementation(thing)
implementations = get_all_implementations(thing, self.contracts)
for offset in range(definition.start, definition.end + 1):
if (
isinstance(thing, TopLevel)
isinstance(thing, (TopLevel, Contract))
or (
isinstance(thing, FunctionContract)
and thing.contract_declarer == thing.contract
)
or (isinstance(thing, ContractLevel) and not isinstance(thing, FunctionContract))
):
self._offset_to_objects[definition.filename][offset].add(thing)
self._offset_to_definitions[definition.filename][offset].add(definition)
self._offset_to_implementations[definition.filename][offset].add(implementation)
self._offset_to_implementations[definition.filename][offset].update(implementations)
self._offset_to_references[definition.filename][offset] |= set(references)
for ref in references:
for offset in range(ref.start, ref.end + 1):
is_declared_function = (
isinstance(thing, FunctionContract)
and thing.contract_declarer == thing.contract
)
if (
isinstance(thing, TopLevel)
or (
isinstance(thing, FunctionContract)
and thing.contract_declarer == thing.contract
)
or is_declared_function
or (
isinstance(thing, ContractLevel) and not isinstance(thing, FunctionContract)
)
):
self._offset_to_objects[definition.filename][offset].add(thing)
self._offset_to_definitions[ref.filename][offset].add(definition)
self._offset_to_implementations[ref.filename][offset].add(implementation)
if is_declared_function:
# Only show the nearest lexical definition for declared contract-level functions
if (
thing.contract.source_mapping.start
< offset
< thing.contract.source_mapping.end
):
self._offset_to_definitions[ref.filename][offset].add(definition)
else:
self._offset_to_definitions[ref.filename][offset].add(definition)
self._offset_to_implementations[ref.filename][offset].update(implementations)
self._offset_to_references[ref.filename][offset] |= set(references)
def _compute_offsets_to_ref_impl_decl(self): # pylint: disable=too-many-branches
@ -251,15 +263,18 @@ class SlitherCore(Context):
for contract in compilation_unit.contracts:
self._compute_offsets_from_thing(contract)
for function in contract.functions:
for function in contract.functions_declared:
self._compute_offsets_from_thing(function)
for variable in function.local_variables:
self._compute_offsets_from_thing(variable)
for modifier in contract.modifiers:
for modifier in contract.modifiers_declared:
self._compute_offsets_from_thing(modifier)
for variable in modifier.local_variables:
self._compute_offsets_from_thing(variable)
for var in contract.state_variables:
self._compute_offsets_from_thing(var)
for st in contract.structures:
self._compute_offsets_from_thing(st)
@ -268,6 +283,10 @@ class SlitherCore(Context):
for event in contract.events:
self._compute_offsets_from_thing(event)
for typ in contract.type_aliases:
self._compute_offsets_from_thing(typ)
for enum in compilation_unit.enums_top_level:
self._compute_offsets_from_thing(enum)
for event in compilation_unit.events_top_level:
@ -276,6 +295,14 @@ class SlitherCore(Context):
self._compute_offsets_from_thing(function)
for st in compilation_unit.structures_top_level:
self._compute_offsets_from_thing(st)
for var in compilation_unit.variables_top_level:
self._compute_offsets_from_thing(var)
for typ in compilation_unit.type_aliases.values():
self._compute_offsets_from_thing(typ)
for err in compilation_unit.custom_errors:
self._compute_offsets_from_thing(err)
for event in compilation_unit.events_top_level:
self._compute_offsets_from_thing(event)
for import_directive in compilation_unit.import_directives:
self._compute_offsets_from_thing(import_directive)
for pragma in compilation_unit.pragma_directives:

@ -21,18 +21,20 @@ class Declaration(AbstractPrinter):
txt += "\n# Contracts\n"
for contract in compilation_unit.contracts:
txt += f"# {contract.name}\n"
txt += f"\t- Declaration: {get_definition(contract, compilation_unit.core.crytic_compile).to_detailed_str()}\n"
txt += f"\t- Implementation: {get_implementation(contract).to_detailed_str()}\n"
contract_def = get_definition(contract, compilation_unit.core.crytic_compile)
txt += f"\t- Declaration: {contract_def.to_detailed_str()}\n"
txt += f"\t- Implementation(s): {[x.to_detailed_str() for x in list(self.slither.offset_to_implementations(contract.source_mapping.filename.absolute, contract_def.start))]}\n"
txt += (
f"\t- References: {[x.to_detailed_str() for x in get_references(contract)]}\n"
)
txt += "\n\t## Function\n"
for func in contract.functions:
for func in contract.functions_declared:
txt += f"\t\t- {func.canonical_name}\n"
txt += f"\t\t\t- Declaration: {get_definition(func, compilation_unit.core.crytic_compile).to_detailed_str()}\n"
txt += f"\t\t\t- Implementation: {get_implementation(func).to_detailed_str()}\n"
function_def = get_definition(func, compilation_unit.core.crytic_compile)
txt += f"\t\t\t- Declaration: {function_def.to_detailed_str()}\n"
txt += f"\t\t\t- Implementation(s): {[x.to_detailed_str() for x in list(self.slither.offset_to_implementations(func.source_mapping.filename.absolute, function_def.start))]}\n"
txt += f"\t\t\t- References: {[x.to_detailed_str() for x in get_references(func)]}\n"
txt += "\n\t## State variables\n"

@ -1209,7 +1209,7 @@ def extract_tmp_call(ins: TmpCall, contract: Optional[Contract]) -> Union[Call,
internalcall.set_expression(ins.expression)
return internalcall
raise Exception(f"Not extracted {type(ins.called)} {ins}")
raise SlithIRError(f"Not extracted {type(ins.called)} {ins}")
# endregion
@ -2014,6 +2014,9 @@ def _find_source_mapping_references(irs: List[Operation]) -> None:
if isinstance(ir, NewContract):
ir.contract_created.references.append(ir.expression.source_mapping)
if isinstance(ir, HighLevelCall):
ir.function.references.append(ir.expression.source_mapping)
# endregion
###################################################################################

@ -1,6 +1,6 @@
import logging
import re
from typing import Any, List, Dict, Callable, TYPE_CHECKING, Union, Set, Sequence
from typing import Any, List, Dict, Callable, TYPE_CHECKING, Union, Set, Sequence, Tuple
from slither.core.declarations import (
Modifier,
@ -64,7 +64,8 @@ class ContractSolc(CallerContextExpression):
# use to remap inheritance id
self._remapping: Dict[str, str] = {}
self.baseContracts: List[str] = []
# (referencedDeclaration, offset)
self.baseContracts: List[Tuple[int, str]] = []
self.baseConstructorContractsCalled: List[str] = []
self._linearized_base_contracts: List[int]
@ -174,6 +175,9 @@ class ContractSolc(CallerContextExpression):
self._contract.is_fully_implemented = attributes["fullyImplemented"]
self._linearized_base_contracts = attributes["linearizedBaseContracts"]
if "abstract" in attributes:
self._contract.is_abstract = attributes["abstract"]
# Parse base contract information
self._parse_base_contract_info()
@ -201,7 +205,9 @@ class ContractSolc(CallerContextExpression):
# Obtain our contract reference and add it to our base contract list
referencedDeclaration = base_contract["baseName"]["referencedDeclaration"]
self.baseContracts.append(referencedDeclaration)
self.baseContracts.append(
(referencedDeclaration, base_contract["baseName"]["src"])
)
# If we have defined arguments in our arguments object, this is a constructor invocation.
# (note: 'arguments' can be [], which is not the same as None. [] implies a constructor was
@ -233,7 +239,10 @@ class ContractSolc(CallerContextExpression):
referencedDeclaration = base_contract_items[0]["attributes"][
"referencedDeclaration"
]
self.baseContracts.append(referencedDeclaration)
self.baseContracts.append(
(referencedDeclaration, base_contract_items[0]["src"])
)
# If we have an 'attributes'->'arguments' which is None, this is not a constructor call.
if (

@ -208,8 +208,6 @@ class FunctionSolc(CallerContextExpression):
else:
attributes = self._functionNotParsed["attributes"]
if "payable" in attributes:
self._function.payable = attributes["payable"]
if "stateMutability" in attributes:
if attributes["stateMutability"] == "payable":
self._function.payable = True
@ -243,6 +241,34 @@ class FunctionSolc(CallerContextExpression):
if "payable" in attributes:
self._function.payable = attributes["payable"]
if "baseFunctions" in attributes:
overrides_ids = attributes["baseFunctions"]
if len(overrides_ids) > 0:
for f_id in overrides_ids:
funcs = self.slither_parser.functions_by_id[f_id]
for f in funcs:
# Do not consider leaf contracts as overrides.
# B is A { function a() override {} } and C is A { function a() override {} } override A.a(), not each other.
if (
f.contract == self._function.contract
or f.contract in self._function.contract.inheritance
):
self._function.overrides.append(f)
f.overridden_by.append(self._function)
# Attaches reference to override specifier e.g. X is referenced by `function a() override(X)`
if "overrides" in attributes and isinstance(attributes["overrides"], dict):
for override in attributes["overrides"].get("overrides", []):
refId = override["referencedDeclaration"]
overridden_contract = self.slither_parser.contracts_by_id.get(refId, None)
if overridden_contract:
overridden_contract.add_reference_from_raw_source(
override["src"], self.compilation_unit
)
if "virtual" in attributes:
self._function.is_virtual = attributes["virtual"]
def analyze_params(self) -> None:
# Can be re-analyzed due to inheritance
if self._params_was_analyzed:

@ -1,3 +1,4 @@
from collections import defaultdict
import json
import logging
import os
@ -7,7 +8,7 @@ from typing import List, Dict
from slither.analyses.data_dependency.data_dependency import compute_dependency
from slither.core.compilation_unit import SlitherCompilationUnit
from slither.core.declarations import Contract
from slither.core.declarations import Contract, Function
from slither.core.declarations.custom_error_top_level import CustomErrorTopLevel
from slither.core.declarations.enum_top_level import EnumTopLevel
from slither.core.declarations.event_top_level import EventTopLevel
@ -79,7 +80,8 @@ class SlitherCompilationUnitSolc(CallerContextExpression):
self._compilation_unit: SlitherCompilationUnit = compilation_unit
self._contracts_by_id: Dict[int, ContractSolc] = {}
self._contracts_by_id: Dict[int, Contract] = {}
self._functions_by_id: Dict[int, List[Function]] = defaultdict(list)
self._parsed = False
self._analyzed = False
self._is_compact_ast = False
@ -105,6 +107,7 @@ class SlitherCompilationUnitSolc(CallerContextExpression):
def add_function_or_modifier_parser(self, f: FunctionSolc) -> None:
self._all_functions_and_modifier_parser.append(f)
self._functions_by_id[f.underlying_function.id].append(f.underlying_function)
@property
def underlying_contract_to_parser(self) -> Dict[Contract, ContractSolc]:
@ -114,6 +117,14 @@ class SlitherCompilationUnitSolc(CallerContextExpression):
def slither_parser(self) -> "SlitherCompilationUnitSolc":
return self
@property
def contracts_by_id(self) -> Dict[int, Contract]:
return self._contracts_by_id
@property
def functions_by_id(self) -> Dict[int, List[Function]]:
return self._functions_by_id
###################################################################################
###################################################################################
# region AST
@ -480,13 +491,16 @@ Please rename it, this name is reserved for Slither's internals"""
else:
missing_inheritance = i
# Resolve immediate base contracts.
for i in contract_parser.baseContracts:
# Resolve immediate base contracts and attach references.
for (i, src) in contract_parser.baseContracts:
if i in contract_parser.remapping:
target = resolve_remapping_and_renaming(contract_parser, i)
fathers.append(target)
target.add_reference_from_raw_source(src, self.compilation_unit)
elif i in self._contracts_by_id:
fathers.append(self._contracts_by_id[i])
target = self._contracts_by_id[i]
fathers.append(target)
target.add_reference_from_raw_source(src, self.compilation_unit)
else:
missing_inheritance = i

@ -1,7 +1,17 @@
from typing import List
from typing import List, Set
from crytic_compile import CryticCompile
from slither.core.declarations import Contract, Function, Enum, Event, Import, Pragma, Structure
from slither.core.solidity_types.type import Type
from slither.core.declarations import (
Contract,
Function,
Enum,
Event,
Import,
Pragma,
Structure,
CustomError,
FunctionContract,
)
from slither.core.solidity_types import Type, TypeAlias
from slither.core.source_mapping.source_mapping import Source, SourceMapping
from slither.core.variables.variable import Variable
from slither.exceptions import SlitherError
@ -15,6 +25,10 @@ def get_definition(target: SourceMapping, crytic_compile: CryticCompile) -> Sour
pattern = "import"
elif isinstance(target, Pragma):
pattern = "pragma" # todo maybe return with the while pragma statement
elif isinstance(target, CustomError):
pattern = "error"
elif isinstance(target, TypeAlias):
pattern = "type"
elif isinstance(target, Type):
raise SlitherError("get_definition_generic not implemented for types")
else:
@ -52,5 +66,34 @@ def get_implementation(target: SourceMapping) -> Source:
return target.source_mapping
def get_all_implementations(target: SourceMapping, contracts: List[Contract]) -> Set[Source]:
"""
Get all implementations of a contract or function, accounting for inheritance and overrides
"""
implementations = set()
# Abstract contracts and interfaces are implemented by their children
if isinstance(target, Contract):
is_interface = target.is_interface
is_implicitly_abstract = not target.is_fully_implemented
is_explicitly_abstract = target.is_abstract
if is_interface or is_implicitly_abstract or is_explicitly_abstract:
for contract in contracts:
if target in contract.immediate_inheritance:
implementations.add(contract.source_mapping)
# Parent's virtual functions may be overridden by children
elif isinstance(target, FunctionContract):
for over in target.overridden_by:
implementations.add(over.source_mapping)
# Only show implemented virtual functions
if not target.is_virtual or target.is_implemented:
implementations.add(get_implementation(target))
else:
implementations.add(get_implementation(target))
return implementations
def get_references(target: SourceMapping) -> List[Source]:
return target.references

@ -11,26 +11,36 @@ CONTRACT_DECL_TEST_ROOT = Path(TEST_DATA_DIR, "contract_declaration")
def test_abstract_contract(solc_binary_path) -> None:
solc_path = solc_binary_path("0.8.0")
slither = Slither(Path(CONTRACT_DECL_TEST_ROOT, "abstract.sol").as_posix(), solc=solc_path)
assert not slither.contracts[0].is_fully_implemented
explicit_abstract = slither.contracts[0]
assert not explicit_abstract.is_fully_implemented
assert explicit_abstract.is_abstract
solc_path = solc_binary_path("0.5.0")
slither = Slither(
Path(CONTRACT_DECL_TEST_ROOT, "implicit_abstract.sol").as_posix(), solc=solc_path
)
assert not slither.contracts[0].is_fully_implemented
implicit_abstract = slither.get_contract_from_name("ImplicitAbstract")[0]
assert not implicit_abstract.is_fully_implemented
# This only is expected to work for newer versions of Solidity
assert not implicit_abstract.is_abstract
slither = Slither(
Path(CONTRACT_DECL_TEST_ROOT, "implicit_abstract.sol").as_posix(),
solc_force_legacy_json=True,
solc=solc_path,
)
assert not slither.contracts[0].is_fully_implemented
implicit_abstract = slither.get_contract_from_name("ImplicitAbstract")[0]
assert not implicit_abstract.is_fully_implemented
# This only is expected to work for newer versions of Solidity
assert not implicit_abstract.is_abstract
def test_concrete_contract(solc_binary_path) -> None:
solc_path = solc_binary_path("0.8.0")
slither = Slither(Path(CONTRACT_DECL_TEST_ROOT, "concrete.sol").as_posix(), solc=solc_path)
assert slither.contracts[0].is_fully_implemented
concrete = slither.get_contract_from_name("Concrete")[0]
assert concrete.is_fully_implemented
assert not concrete.is_abstract
solc_path = solc_binary_path("0.5.0")
slither = Slither(
@ -38,7 +48,9 @@ def test_concrete_contract(solc_binary_path) -> None:
solc_force_legacy_json=True,
solc=solc_path,
)
assert slither.contracts[0].is_fully_implemented
concrete_old = slither.get_contract_from_name("ConcreteOld")[0]
assert concrete_old.is_fully_implemented
assert not concrete_old.is_abstract
def test_private_variable(solc_binary_path) -> None:

@ -0,0 +1,16 @@
type T is uint256;
uint constant U = 1;
error V(T);
event W(T);
contract E {
type X is int256;
function f() public {
T t = T.wrap(U);
if (T.unwrap(t) == 0) {
revert V(t);
}
emit W(t);
X x = X.wrap(1);
}
}

@ -0,0 +1,65 @@
contract Test {
function myVirtualFunction() virtual external {
}
}
contract A is Test {
function myVirtualFunction() virtual override external {
}
}
contract B is A {
function myVirtualFunction() override external {
}
}
contract C is Test {
function myVirtualFunction() override external {
}
}
contract X is Test {
function myVirtualFunction() virtual override external {
}
}
contract Y {
function myVirtualFunction() virtual external {
}
}
contract Z is Y, X{
function myVirtualFunction() virtual override(Y, X) external {
}
}
abstract contract Name {
constructor() {
}
}
contract Name2 is Name {
constructor() {
}
}
abstract contract Test2 {
function f() virtual public;
}
contract A2 is Test2 {
function f() virtual override public {
}
}
abstract contract I {
function a() public virtual {}
}
contract J is I {}
contract K is J {
function a() public override {}
}

@ -1,17 +1,27 @@
from pathlib import Path
import pytest
from slither import Slither
from slither.core.declarations import Function
from slither.core.declarations import Function, CustomErrorTopLevel, EventTopLevel
from slither.core.solidity_types.type_alias import TypeAliasTopLevel, TypeAliasContract
from slither.core.variables.top_level_variable import TopLevelVariable
TEST_DATA_DIR = Path(__file__).resolve().parent / "test_data"
SRC_MAPPING_TEST_ROOT = Path(TEST_DATA_DIR, "src_mapping")
def test_source_mapping(solc_binary_path):
solc_path = solc_binary_path("0.6.12")
# Ensure issue fixed in https://github.com/crytic/crytic-compile/pull/554 does not regress in Slither's reference lookup.
@pytest.mark.parametrize("solc_version", ["0.6.12", "0.8.7", "0.8.8"])
def test_source_mapping_inheritance(solc_binary_path, solc_version):
solc_path = solc_binary_path(solc_version)
file = Path(SRC_MAPPING_TEST_ROOT, "inheritance.sol").as_posix()
slither = Slither(file, solc=solc_path)
# 3 reference to A in inheritance `contract $ is A`
assert {(x.start, x.end) for x in slither.offset_to_references(file, 9)} == {
(121, 122),
(185, 186),
(299, 300),
}
# Check if A.f() is at the offset 27
functions = slither.offset_to_objects(file, 27)
assert len(functions) == 1
@ -23,8 +33,12 @@ def test_source_mapping(solc_binary_path):
assert {(x.start, x.end) for x in slither.offset_to_definitions(file, 27)} == {(26, 28)}
# Only one reference for A.f(), in A.test()
assert {(x.start, x.end) for x in slither.offset_to_references(file, 27)} == {(92, 93)}
# Only one implementation for A.f(), in A.test()
assert {(x.start, x.end) for x in slither.offset_to_implementations(file, 27)} == {(17, 53)}
# Three overridden implementation of A.f(), in A.test()
assert {(x.start, x.end) for x in slither.offset_to_implementations(file, 27)} == {
(17, 53),
(129, 166),
(193, 230),
}
# Check if C.f() is at the offset 203
functions = slither.offset_to_objects(file, 203)
@ -52,11 +66,9 @@ def test_source_mapping(solc_binary_path):
assert isinstance(function, Function)
assert function.canonical_name in ["A.f()", "B.f()", "C.f()"]
# There are three definitions possible (in A, B or C)
# There is one definition in the lexical scope of A
assert {(x.start, x.end) for x in slither.offset_to_definitions(file, 93)} == {
(26, 28),
(202, 204),
(138, 140),
}
# There are two references possible (in A.test() or C.test2() )
@ -113,6 +125,62 @@ def test_references_user_defined_types_when_casting(solc_binary_path):
assert lines == [12, 18]
def test_source_mapping_top_level_defs(solc_binary_path):
solc_path = solc_binary_path("0.8.24")
file = Path(SRC_MAPPING_TEST_ROOT, "TopLevelReferences.sol").as_posix()
slither = Slither(file, solc=solc_path)
# Check if T is at the offset 5
types = slither.offset_to_objects(file, 5)
assert len(types) == 1
type_ = types.pop()
assert isinstance(type_, TypeAliasTopLevel)
assert type_.name == "T"
assert {(x.start, x.end) for x in slither.offset_to_references(file, 5)} == {
(48, 49),
(60, 61),
(134, 135),
(140, 141),
(163, 164),
}
# Check if U is at the offset 33
constants = slither.offset_to_objects(file, 33)
assert len(constants) == 1
constant = constants.pop()
assert isinstance(constant, TopLevelVariable)
assert constant.name == "U"
assert {(x.start, x.end) for x in slither.offset_to_references(file, 33)} == {(147, 148)}
# Check if V is at the offset 46
errors = slither.offset_to_objects(file, 46)
assert len(errors) == 1
error = errors.pop()
assert isinstance(error, CustomErrorTopLevel)
assert error.name == "V"
assert {(x.start, x.end) for x in slither.offset_to_references(file, 46)} == {(202, 203)}
# Check if W is at the offset 58
events = slither.offset_to_objects(file, 58)
assert len(events) == 1
event = events.pop()
assert isinstance(event, EventTopLevel)
assert event.name == "W"
assert {(x.start, x.end) for x in slither.offset_to_references(file, 58)} == {(231, 232)}
# Check if X is at the offset 87
types = slither.offset_to_objects(file, 87)
assert len(types) == 1
type_ = types.pop()
assert isinstance(type_, TypeAliasContract)
assert type_.name == "X"
assert {(x.start, x.end) for x in slither.offset_to_references(file, 87)} == {
(245, 246),
(251, 252),
}
def test_references_self_identifier():
"""
Tests that shadowing state variables with local variables does not affect references.

@ -0,0 +1,151 @@
from pathlib import Path
from slither import Slither
TEST_DATA_DIR = Path(__file__).resolve().parent / "test_data"
def test_overrides(solc_binary_path) -> None:
# pylint: disable=too-many-statements,too-many-locals
solc_path = solc_binary_path("0.8.15")
slither = Slither(Path(TEST_DATA_DIR, "virtual_overrides.sol").as_posix(), solc=solc_path)
test = slither.get_contract_from_name("Test")[0]
test_virtual_func = test.get_function_from_full_name("myVirtualFunction()")
assert test_virtual_func.is_virtual
assert not test_virtual_func.is_override
x = test.get_functions_overridden_by(test_virtual_func)
assert len(x) == 0
x = test_virtual_func.overridden_by
assert len(x) == 5
assert set(i.canonical_name for i in x) == set(
["A.myVirtualFunction()", "C.myVirtualFunction()", "X.myVirtualFunction()"]
)
a = slither.get_contract_from_name("A")[0]
a_virtual_func = a.get_function_from_full_name("myVirtualFunction()")
assert a_virtual_func.is_virtual
assert a_virtual_func.is_override
x = a.get_functions_overridden_by(a_virtual_func)
assert len(x) == 2
assert set(i.canonical_name for i in x) == set(["Test.myVirtualFunction()"])
b = slither.get_contract_from_name("B")[0]
b_virtual_func = b.get_function_from_full_name("myVirtualFunction()")
assert not b_virtual_func.is_virtual
assert b_virtual_func.is_override
x = b.get_functions_overridden_by(b_virtual_func)
assert len(x) == 2
assert set(i.canonical_name for i in x) == set(["A.myVirtualFunction()"])
assert len(b_virtual_func.overridden_by) == 0
c = slither.get_contract_from_name("C")[0]
c_virtual_func = c.get_function_from_full_name("myVirtualFunction()")
assert not c_virtual_func.is_virtual
assert c_virtual_func.is_override
x = c.get_functions_overridden_by(c_virtual_func)
assert len(x) == 2
# C should not override B as they are distinct leaves in the inheritance tree
assert set(i.canonical_name for i in x) == set(["Test.myVirtualFunction()"])
y = slither.get_contract_from_name("Y")[0]
y_virtual_func = y.get_function_from_full_name("myVirtualFunction()")
assert y_virtual_func.is_virtual
assert not y_virtual_func.is_override
x = y_virtual_func.overridden_by
assert len(x) == 1
assert x[0].canonical_name == "Z.myVirtualFunction()"
z = slither.get_contract_from_name("Z")[0]
z_virtual_func = z.get_function_from_full_name("myVirtualFunction()")
assert z_virtual_func.is_virtual
assert z_virtual_func.is_override
x = z.get_functions_overridden_by(z_virtual_func)
assert len(x) == 4
assert set(i.canonical_name for i in x) == set(
["Y.myVirtualFunction()", "X.myVirtualFunction()"]
)
k = slither.get_contract_from_name("K")[0]
k_virtual_func = k.get_function_from_full_name("a()")
assert not k_virtual_func.is_virtual
assert k_virtual_func.is_override
assert len(k_virtual_func.overrides) == 3
x = k_virtual_func.overrides
assert set(i.canonical_name for i in x) == set(["I.a()"])
i = slither.get_contract_from_name("I")[0]
i_virtual_func = i.get_function_from_full_name("a()")
assert i_virtual_func.is_virtual
assert not i_virtual_func.is_override
assert len(i_virtual_func.overrides) == 0
x = i_virtual_func.overridden_by
assert len(x) == 1
assert x[0].canonical_name == "K.a()"
def test_virtual_override_references_and_implementations(solc_binary_path) -> None:
solc_path = solc_binary_path("0.8.15")
file = Path(TEST_DATA_DIR, "virtual_overrides.sol").as_posix()
slither = Slither(file, solc=solc_path)
funcs = slither.offset_to_objects(file, 29)
assert len(funcs) == 1
func = funcs.pop()
assert func.canonical_name == "Test.myVirtualFunction()"
assert {(x.start, x.end) for x in slither.offset_to_implementations(file, 29)} == {
(20, 73),
(102, 164),
(274, 328),
(357, 419),
}
funcs = slither.offset_to_objects(file, 111)
assert len(funcs) == 1
func = funcs.pop()
assert func.canonical_name == "A.myVirtualFunction()"
# A.myVirtualFunction() is implemented in A and also overridden in B
assert {(x.start, x.end) for x in slither.offset_to_implementations(file, 111)} == {
(102, 164),
(190, 244),
}
# X is inherited by Z and Z.myVirtualFunction() overrides X.myVirtualFunction()
assert {(x.start, x.end) for x in slither.offset_to_references(file, 341)} == {
(514, 515),
(570, 571),
}
# The reference to X in inheritance specifier is the definition of Z
assert {(x.start, x.end) for x in slither.offset_to_definitions(file, 514)} == {(341, 343)}
# The reference to X in the function override specifier is the definition of Z
assert {(x.start, x.end) for x in slither.offset_to_definitions(file, 570)} == {(341, 343)}
# Y is inherited by Z and Z.myVirtualFunction() overrides Y.myVirtualFunction()
assert {(x.start, x.end) for x in slither.offset_to_references(file, 432)} == {
(511, 512),
(567, 568),
}
# The reference to Y in inheritance specifier is the definition of Z
assert {(x.start, x.end) for x in slither.offset_to_definitions(file, 511)} == {(432, 434)}
# The reference to Y in the function override specifier is the definition of Z
assert {(x.start, x.end) for x in slither.offset_to_definitions(file, 567)} == {(432, 434)}
# Name is abstract and has no implementation. It is inherited and implemented by Name2
assert {(x.start, x.end) for x in slither.offset_to_implementations(file, 612)} == {(657, 718)}
def test_virtual_is_implemented(solc_binary_path):
solc_path = solc_binary_path("0.8.15")
file = Path(TEST_DATA_DIR, "virtual_overrides.sol").as_posix()
slither = Slither(file, solc=solc_path)
test2 = slither.get_contract_from_name("Test2")[0]
f = test2.get_function_from_full_name("f()")
assert f.is_virtual
assert not f.is_implemented
a2 = slither.get_contract_from_name("A2")[0]
f = a2.get_function_from_full_name("f()")
assert f.is_virtual
assert f.is_implemented
# Test.2f() is not implemented, but A2 inherits from Test2 and overrides f()
assert {(x.start, x.end) for x in slither.offset_to_implementations(file, 759)} == {(809, 853)}
Loading…
Cancel
Save