mirror of https://github.com/crytic/slither
commit
e73ad16da9
@ -0,0 +1,32 @@ |
||||
{ |
||||
"problemMatcher": [ |
||||
{ |
||||
"owner": "pylint-error", |
||||
"severity": "error", |
||||
"pattern": [ |
||||
{ |
||||
"regexp": "^(.+):(\\d+):(\\d+):\\s(([EF]\\d{4}):\\s.+)$", |
||||
"file": 1, |
||||
"line": 2, |
||||
"column": 3, |
||||
"message": 4, |
||||
"code": 5 |
||||
} |
||||
] |
||||
}, |
||||
{ |
||||
"owner": "pylint-warning", |
||||
"severity": "warning", |
||||
"pattern": [ |
||||
{ |
||||
"regexp": "^(.+):(\\d+):(\\d+):\\s(([CRW]\\d{4}):\\s.+)$", |
||||
"file": 1, |
||||
"line": 2, |
||||
"column": 3, |
||||
"message": 4, |
||||
"code": 5 |
||||
} |
||||
] |
||||
} |
||||
] |
||||
} |
@ -0,0 +1,22 @@ |
||||
{ |
||||
"problemMatcher": [ |
||||
{ |
||||
"owner": "yamllint", |
||||
"pattern": [ |
||||
{ |
||||
"regexp": "^(.*\\.ya?ml)$", |
||||
"file": 1 |
||||
}, |
||||
{ |
||||
"regexp": "^\\s{2}(\\d+):(\\d+)\\s+(error|warning)\\s+(.*?)\\s+\\((.*)\\)$", |
||||
"line": 1, |
||||
"column": 2, |
||||
"severity": 3, |
||||
"message": 4, |
||||
"code": 5, |
||||
"loop": true |
||||
} |
||||
] |
||||
} |
||||
] |
||||
} |
@ -0,0 +1,6 @@ |
||||
from slither.core.expressions.identifier import Identifier |
||||
|
||||
|
||||
class SelfIdentifier(Identifier): |
||||
def __str__(self): |
||||
return "self." + str(self._value) |
@ -1,2 +1,8 @@ |
||||
from .state_variable import StateVariable |
||||
from .variable import Variable |
||||
from .local_variable_init_from_tuple import LocalVariableInitFromTuple |
||||
from .local_variable import LocalVariable |
||||
from .top_level_variable import TopLevelVariable |
||||
from .event_variable import EventVariable |
||||
from .function_type_variable import FunctionTypeVariable |
||||
from .structure_variable import StructureVariable |
||||
|
@ -0,0 +1,91 @@ |
||||
from typing import List, Optional |
||||
|
||||
from slither.core.declarations import SolidityFunction, Function |
||||
from slither.detectors.abstract_detector import ( |
||||
AbstractDetector, |
||||
DetectorClassification, |
||||
DETECTOR_INFO, |
||||
) |
||||
from slither.slithir.operations import SolidityCall |
||||
from slither.utils.output import Output |
||||
|
||||
|
||||
def _assembly_node(function: Function) -> Optional[SolidityCall]: |
||||
""" |
||||
Check if there is a node that use return in assembly |
||||
|
||||
Args: |
||||
function: |
||||
|
||||
Returns: |
||||
|
||||
""" |
||||
|
||||
for ir in function.all_slithir_operations(): |
||||
if isinstance(ir, SolidityCall) and ir.function == SolidityFunction( |
||||
"return(uint256,uint256)" |
||||
): |
||||
return ir |
||||
return None |
||||
|
||||
|
||||
class IncorrectReturn(AbstractDetector): |
||||
""" |
||||
Check for cases where a return(a,b) is used in an assembly function |
||||
""" |
||||
|
||||
ARGUMENT = "incorrect-return" |
||||
HELP = "If a `return` is incorrectly used in assembly mode." |
||||
IMPACT = DetectorClassification.HIGH |
||||
CONFIDENCE = DetectorClassification.MEDIUM |
||||
|
||||
WIKI = "https://github.com/crytic/slither/wiki/Detector-Documentation#incorrect-assembly-return" |
||||
|
||||
WIKI_TITLE = "Incorrect return in assembly" |
||||
WIKI_DESCRIPTION = "Detect if `return` in an assembly block halts unexpectedly the execution." |
||||
WIKI_EXPLOIT_SCENARIO = """ |
||||
```solidity |
||||
contract C { |
||||
function f() internal returns (uint a, uint b) { |
||||
assembly { |
||||
return (5, 6) |
||||
} |
||||
} |
||||
|
||||
function g() returns (bool){ |
||||
f(); |
||||
return true; |
||||
} |
||||
} |
||||
``` |
||||
The return statement in `f` will cause execution in `g` to halt. |
||||
The function will return 6 bytes starting from offset 5, instead of returning a boolean.""" |
||||
|
||||
WIKI_RECOMMENDATION = "Use the `leave` statement." |
||||
|
||||
# pylint: disable=too-many-nested-blocks |
||||
def _detect(self) -> List[Output]: |
||||
results: List[Output] = [] |
||||
for c in self.contracts: |
||||
for f in c.functions_and_modifiers_declared: |
||||
|
||||
for node in f.nodes: |
||||
if node.sons: |
||||
for function_called in node.internal_calls: |
||||
if isinstance(function_called, Function): |
||||
found = _assembly_node(function_called) |
||||
if found: |
||||
|
||||
info: DETECTOR_INFO = [ |
||||
f, |
||||
" calls ", |
||||
function_called, |
||||
" which halt the execution ", |
||||
found.node, |
||||
"\n", |
||||
] |
||||
json = self.generate_result(info) |
||||
|
||||
results.append(json) |
||||
|
||||
return results |
@ -0,0 +1,68 @@ |
||||
from typing import List |
||||
|
||||
from slither.core.declarations import SolidityFunction, Function |
||||
from slither.detectors.abstract_detector import ( |
||||
AbstractDetector, |
||||
DetectorClassification, |
||||
DETECTOR_INFO, |
||||
) |
||||
from slither.slithir.operations import SolidityCall |
||||
from slither.utils.output import Output |
||||
|
||||
|
||||
class ReturnInsteadOfLeave(AbstractDetector): |
||||
""" |
||||
Check for cases where a return(a,b) is used in an assembly function that also returns two variables |
||||
""" |
||||
|
||||
ARGUMENT = "return-leave" |
||||
HELP = "If a `return` is used instead of a `leave`." |
||||
IMPACT = DetectorClassification.HIGH |
||||
CONFIDENCE = DetectorClassification.MEDIUM |
||||
|
||||
WIKI = "https://github.com/crytic/slither/wiki/Detector-Documentation#incorrect-assembly-return" |
||||
|
||||
WIKI_TITLE = "Return instead of leave in assembly" |
||||
WIKI_DESCRIPTION = "Detect if a `return` is used where a `leave` should be used." |
||||
WIKI_EXPLOIT_SCENARIO = """ |
||||
```solidity |
||||
contract C { |
||||
function f() internal returns (uint a, uint b) { |
||||
assembly { |
||||
return (5, 6) |
||||
} |
||||
} |
||||
|
||||
} |
||||
``` |
||||
The function will halt the execution, instead of returning a two uint.""" |
||||
|
||||
WIKI_RECOMMENDATION = "Use the `leave` statement." |
||||
|
||||
def _check_function(self, f: Function) -> List[Output]: |
||||
results: List[Output] = [] |
||||
|
||||
for node in f.nodes: |
||||
for ir in node.irs: |
||||
if isinstance(ir, SolidityCall) and ir.function == SolidityFunction( |
||||
"return(uint256,uint256)" |
||||
): |
||||
info: DETECTOR_INFO = [f, " contains an incorrect call to return: ", node, "\n"] |
||||
json = self.generate_result(info) |
||||
|
||||
results.append(json) |
||||
return results |
||||
|
||||
def _detect(self) -> List[Output]: |
||||
results: List[Output] = [] |
||||
for c in self.contracts: |
||||
for f in c.functions_declared: |
||||
|
||||
if ( |
||||
len(f.returns) == 2 |
||||
and f.contains_assembly |
||||
and f.visibility not in ["public", "external"] |
||||
): |
||||
results += self._check_function(f) |
||||
|
||||
return results |
@ -0,0 +1,93 @@ |
||||
""" |
||||
Module detecting incorrect operator usage for exponentiation where bitwise xor '^' is used instead of '**' |
||||
""" |
||||
from typing import Tuple, List, Union |
||||
|
||||
from slither.core.cfg.node import Node |
||||
from slither.core.declarations import Contract, Function |
||||
from slither.detectors.abstract_detector import ( |
||||
AbstractDetector, |
||||
DetectorClassification, |
||||
DETECTOR_INFO, |
||||
) |
||||
from slither.slithir.operations import Binary, BinaryType, Operation |
||||
from slither.slithir.utils.utils import RVALUE |
||||
from slither.slithir.variables.constant import Constant |
||||
from slither.utils.output import Output |
||||
|
||||
|
||||
def _is_constant_candidate(var: Union[RVALUE, Function]) -> bool: |
||||
""" |
||||
Check if the variable is a constant. |
||||
Do not consider variable that are expressed with hexadecimal. |
||||
Something like 2^0xf is likely to be a correct bitwise operator |
||||
:param var: |
||||
:return: |
||||
""" |
||||
return isinstance(var, Constant) and not var.original_value.startswith("0x") |
||||
|
||||
|
||||
def _is_bitwise_xor_on_constant(ir: Operation) -> bool: |
||||
return ( |
||||
isinstance(ir, Binary) |
||||
and ir.type == BinaryType.CARET |
||||
and (_is_constant_candidate(ir.variable_left) or _is_constant_candidate(ir.variable_right)) |
||||
) |
||||
|
||||
|
||||
def _detect_incorrect_operator(contract: Contract) -> List[Tuple[Function, Node]]: |
||||
ret: List[Tuple[Function, Node]] = [] |
||||
f: Function |
||||
for f in contract.functions + contract.modifiers: # type:ignore |
||||
# Heuristic: look for binary expressions with ^ operator where at least one of the operands is a constant, and |
||||
# the constant is not in hex, because hex typically is used with bitwise xor and not exponentiation |
||||
nodes = [node for node in f.nodes for ir in node.irs if _is_bitwise_xor_on_constant(ir)] |
||||
for node in nodes: |
||||
ret.append((f, node)) |
||||
return ret |
||||
|
||||
|
||||
# pylint: disable=too-few-public-methods |
||||
class IncorrectOperatorExponentiation(AbstractDetector): |
||||
""" |
||||
Incorrect operator usage of bitwise xor mistaking it for exponentiation |
||||
""" |
||||
|
||||
ARGUMENT = "incorrect-exp" |
||||
HELP = "Incorrect exponentiation" |
||||
IMPACT = DetectorClassification.HIGH |
||||
CONFIDENCE = DetectorClassification.MEDIUM |
||||
|
||||
WIKI = "https://github.com/crytic/slither/wiki/Detector-Documentation#incorrect-exponentiation" |
||||
|
||||
WIKI_TITLE = "Incorrect exponentiation" |
||||
WIKI_DESCRIPTION = "Detect use of bitwise `xor ^` instead of exponential `**`" |
||||
WIKI_EXPLOIT_SCENARIO = """ |
||||
```solidity |
||||
contract Bug{ |
||||
uint UINT_MAX = 2^256 - 1; |
||||
... |
||||
} |
||||
``` |
||||
Alice deploys a contract in which `UINT_MAX` incorrectly uses `^` operator instead of `**` for exponentiation""" |
||||
|
||||
WIKI_RECOMMENDATION = "Use the correct operator `**` for exponentiation." |
||||
|
||||
def _detect(self) -> List[Output]: |
||||
"""Detect the incorrect operator usage for exponentiation where bitwise xor ^ is used instead of ** |
||||
|
||||
Returns: |
||||
list: (function, node) |
||||
""" |
||||
results: List[Output] = [] |
||||
for c in self.compilation_unit.contracts_derived: |
||||
res = _detect_incorrect_operator(c) |
||||
for (func, node) in res: |
||||
info: DETECTOR_INFO = [ |
||||
func, |
||||
" has bitwise-xor operator ^ instead of the exponentiation operator **: \n", |
||||
] |
||||
info += ["\t - ", node, "\n"] |
||||
results.append(self.generate_result(info)) |
||||
|
||||
return results |
@ -0,0 +1,123 @@ |
||||
from typing import List |
||||
|
||||
from slither.core.cfg.node import Node |
||||
from slither.core.declarations import Contract |
||||
from slither.core.declarations.function import Function |
||||
from slither.core.solidity_types import Type |
||||
from slither.detectors.abstract_detector import ( |
||||
AbstractDetector, |
||||
DetectorClassification, |
||||
DETECTOR_INFO, |
||||
) |
||||
from slither.slithir.operations import LowLevelCall, HighLevelCall |
||||
from slither.analyses.data_dependency.data_dependency import is_tainted |
||||
from slither.utils.output import Output |
||||
|
||||
|
||||
class ReturnBomb(AbstractDetector): |
||||
|
||||
ARGUMENT = "return-bomb" |
||||
HELP = "A low level callee may consume all callers gas unexpectedly." |
||||
IMPACT = DetectorClassification.LOW |
||||
CONFIDENCE = DetectorClassification.MEDIUM |
||||
|
||||
WIKI = "https://github.com/crytic/slither/wiki/Detector-Documentation#return-bomb" |
||||
|
||||
WIKI_TITLE = "Return Bomb" |
||||
WIKI_DESCRIPTION = "A low level callee may consume all callers gas unexpectedly." |
||||
WIKI_EXPLOIT_SCENARIO = """ |
||||
```solidity |
||||
//Modified from https://github.com/nomad-xyz/ExcessivelySafeCall |
||||
contract BadGuy { |
||||
function youveActivateMyTrapCard() external pure returns (bytes memory) { |
||||
assembly{ |
||||
revert(0, 1000000) |
||||
} |
||||
} |
||||
} |
||||
|
||||
contract Mark { |
||||
function oops(address badGuy) public{ |
||||
bool success; |
||||
bytes memory ret; |
||||
|
||||
// Mark pays a lot of gas for this copy |
||||
//(success, ret) = badGuy.call{gas:10000}( |
||||
(success, ret) = badGuy.call( |
||||
abi.encodeWithSelector( |
||||
BadGuy.youveActivateMyTrapCard.selector |
||||
) |
||||
); |
||||
|
||||
// Mark may OOG here, preventing local state changes |
||||
//importantCleanup(); |
||||
} |
||||
} |
||||
|
||||
``` |
||||
After Mark calls BadGuy bytes are copied from returndata to memory, the memory expansion cost is paid. This means that when using a standard solidity call, the callee can "returnbomb" the caller, imposing an arbitrary gas cost. |
||||
Callee unexpectedly makes the caller OOG. |
||||
""" |
||||
|
||||
WIKI_RECOMMENDATION = "Avoid unlimited implicit decoding of returndata." |
||||
|
||||
@staticmethod |
||||
def is_dynamic_type(ty: Type) -> bool: |
||||
# ty.is_dynamic ? |
||||
name = str(ty) |
||||
if "[]" in name or name in ("bytes", "string"): |
||||
return True |
||||
return False |
||||
|
||||
def get_nodes_for_function(self, function: Function, contract: Contract) -> List[Node]: |
||||
nodes = [] |
||||
for node in function.nodes: |
||||
for ir in node.irs: |
||||
if isinstance(ir, (HighLevelCall, LowLevelCall)): |
||||
if not is_tainted(ir.destination, contract): # type:ignore |
||||
# Only interested if the target address is controlled/tainted |
||||
continue |
||||
|
||||
if isinstance(ir, HighLevelCall) and isinstance(ir.function, Function): |
||||
# in normal highlevel calls return bombs are _possible_ |
||||
# if the return type is dynamic and the caller tries to copy and decode large data |
||||
has_dyn = False |
||||
if ir.function.return_type: |
||||
has_dyn = any( |
||||
self.is_dynamic_type(ty) for ty in ir.function.return_type |
||||
) |
||||
|
||||
if not has_dyn: |
||||
continue |
||||
|
||||
# If a gas budget was specified then the |
||||
# user may not know about the return bomb |
||||
if ir.call_gas is None: |
||||
# if a gas budget was NOT specified then the caller |
||||
# may already suspect the call may spend all gas? |
||||
continue |
||||
|
||||
nodes.append(node) |
||||
# TODO: check that there is some state change after the call |
||||
|
||||
return nodes |
||||
|
||||
def _detect(self) -> List[Output]: |
||||
results = [] |
||||
|
||||
for contract in self.compilation_unit.contracts: |
||||
for function in contract.functions_declared: |
||||
nodes = self.get_nodes_for_function(function, contract) |
||||
if nodes: |
||||
info: DETECTOR_INFO = [ |
||||
function, |
||||
" tries to limit the gas of an external call that controls implicit decoding\n", |
||||
] |
||||
|
||||
for node in sorted(nodes, key=lambda x: x.node_id): |
||||
info += ["\t", node, "\n"] |
||||
|
||||
res = self.generate_result(info) |
||||
results.append(res) |
||||
|
||||
return results |
@ -0,0 +1,69 @@ |
||||
from typing import List |
||||
from slither.detectors.abstract_detector import ( |
||||
AbstractDetector, |
||||
DetectorClassification, |
||||
DETECTOR_INFO, |
||||
) |
||||
from slither.slithir.operations import ( |
||||
Binary, |
||||
BinaryType, |
||||
) |
||||
|
||||
from slither.core.declarations import Function |
||||
from slither.utils.output import Output |
||||
|
||||
|
||||
class TautologicalCompare(AbstractDetector): |
||||
""" |
||||
Same variable comparison detector |
||||
""" |
||||
|
||||
ARGUMENT = "tautological-compare" |
||||
HELP = "Comparing a variable to itself always returns true or false, depending on comparison" |
||||
IMPACT = DetectorClassification.MEDIUM |
||||
CONFIDENCE = DetectorClassification.HIGH |
||||
|
||||
WIKI = "https://github.com/crytic/slither/wiki/Detector-Documentation#tautological-compare" |
||||
|
||||
WIKI_TITLE = "Tautological compare" |
||||
WIKI_DESCRIPTION = "A variable compared to itself is probably an error as it will always return `true` for `==`, `>=`, `<=` and always `false` for `<`, `>` and `!=`." |
||||
WIKI_EXPLOIT_SCENARIO = """ |
||||
```solidity |
||||
function check(uint a) external returns(bool){ |
||||
return (a >= a); |
||||
} |
||||
``` |
||||
`check` always return true.""" |
||||
|
||||
WIKI_RECOMMENDATION = "Remove comparison or compare to different value." |
||||
|
||||
def _check_function(self, f: Function) -> List[Output]: |
||||
affected_nodes = set() |
||||
for node in f.nodes: |
||||
for ir in node.irs: |
||||
if isinstance(ir, Binary): |
||||
if ir.type in [ |
||||
BinaryType.GREATER, |
||||
BinaryType.GREATER_EQUAL, |
||||
BinaryType.LESS, |
||||
BinaryType.LESS_EQUAL, |
||||
BinaryType.EQUAL, |
||||
BinaryType.NOT_EQUAL, |
||||
]: |
||||
if ir.variable_left == ir.variable_right: |
||||
affected_nodes.add(node) |
||||
|
||||
results = [] |
||||
for n in affected_nodes: |
||||
info: DETECTOR_INFO = [f, " compares a variable to itself:\n\t", n, "\n"] |
||||
res = self.generate_result(info) |
||||
results.append(res) |
||||
return results |
||||
|
||||
def _detect(self): |
||||
results = [] |
||||
|
||||
for f in self.compilation_unit.functions_and_modifiers: |
||||
results.extend(self._check_function(f)) |
||||
|
||||
return results |
@ -0,0 +1,58 @@ |
||||
""" |
||||
CK Metrics are a suite of six software metrics proposed by Chidamber and Kemerer in 1994. |
||||
These metrics are used to measure the complexity of a class. |
||||
https://en.wikipedia.org/wiki/Programming_complexity |
||||
|
||||
- Response For a Class (RFC) is a metric that measures the number of unique method calls within a class. |
||||
- Number of Children (NOC) is a metric that measures the number of children a class has. |
||||
- Depth of Inheritance Tree (DIT) is a metric that measures the number of parent classes a class has. |
||||
- Coupling Between Object Classes (CBO) is a metric that measures the number of classes a class is coupled to. |
||||
|
||||
Not implemented: |
||||
- Lack of Cohesion of Methods (LCOM) is a metric that measures the lack of cohesion in methods. |
||||
- Weighted Methods per Class (WMC) is a metric that measures the complexity of a class. |
||||
|
||||
During the calculation of the metrics above, there are a number of other intermediate metrics that are calculated. |
||||
These are also included in the output: |
||||
- State variables: total number of state variables |
||||
- Constants: total number of constants |
||||
- Immutables: total number of immutables |
||||
- Public: total number of public functions |
||||
- External: total number of external functions |
||||
- Internal: total number of internal functions |
||||
- Private: total number of private functions |
||||
- Mutating: total number of state mutating functions |
||||
- View: total number of view functions |
||||
- Pure: total number of pure functions |
||||
- External mutating: total number of external mutating functions |
||||
- No auth or onlyOwner: total number of functions without auth or onlyOwner modifiers |
||||
- No modifiers: total number of functions without modifiers |
||||
- Ext calls: total number of external calls |
||||
|
||||
""" |
||||
from slither.printers.abstract_printer import AbstractPrinter |
||||
from slither.utils.ck import CKMetrics |
||||
from slither.utils.output import Output |
||||
|
||||
|
||||
class CK(AbstractPrinter): |
||||
ARGUMENT = "ck" |
||||
HELP = "Chidamber and Kemerer (CK) complexity metrics and related function attributes" |
||||
|
||||
WIKI = "https://github.com/trailofbits/slither/wiki/Printer-documentation#ck" |
||||
|
||||
def output(self, _filename: str) -> Output: |
||||
if len(self.contracts) == 0: |
||||
return self.generate_output("No contract found") |
||||
|
||||
ck = CKMetrics(self.contracts) |
||||
|
||||
res = self.generate_output(ck.full_text) |
||||
res.add_pretty_table(ck.auxiliary1.pretty_table, ck.auxiliary1.title) |
||||
res.add_pretty_table(ck.auxiliary2.pretty_table, ck.auxiliary2.title) |
||||
res.add_pretty_table(ck.auxiliary3.pretty_table, ck.auxiliary3.title) |
||||
res.add_pretty_table(ck.auxiliary4.pretty_table, ck.auxiliary4.title) |
||||
res.add_pretty_table(ck.core.pretty_table, ck.core.title) |
||||
self.info(ck.full_text) |
||||
|
||||
return res |
@ -0,0 +1,49 @@ |
||||
""" |
||||
Halstead complexity metrics |
||||
https://en.wikipedia.org/wiki/Halstead_complexity_measures |
||||
|
||||
12 metrics based on the number of unique operators and operands: |
||||
|
||||
Core metrics: |
||||
n1 = the number of distinct operators |
||||
n2 = the number of distinct operands |
||||
N1 = the total number of operators |
||||
N2 = the total number of operands |
||||
|
||||
Extended metrics1: |
||||
n = n1 + n2 # Program vocabulary |
||||
N = N1 + N2 # Program length |
||||
S = n1 * log2(n1) + n2 * log2(n2) # Estimated program length |
||||
V = N * log2(n) # Volume |
||||
|
||||
Extended metrics2: |
||||
D = (n1 / 2) * (N2 / n2) # Difficulty |
||||
E = D * V # Effort |
||||
T = E / 18 seconds # Time required to program |
||||
B = (E^(2/3)) / 3000 # Number of delivered bugs |
||||
|
||||
""" |
||||
from slither.printers.abstract_printer import AbstractPrinter |
||||
from slither.utils.halstead import HalsteadMetrics |
||||
from slither.utils.output import Output |
||||
|
||||
|
||||
class Halstead(AbstractPrinter): |
||||
ARGUMENT = "halstead" |
||||
HELP = "Computes the Halstead complexity metrics for each contract" |
||||
|
||||
WIKI = "https://github.com/trailofbits/slither/wiki/Printer-documentation#halstead" |
||||
|
||||
def output(self, _filename: str) -> Output: |
||||
if len(self.contracts) == 0: |
||||
return self.generate_output("No contract found") |
||||
|
||||
halstead = HalsteadMetrics(self.contracts) |
||||
|
||||
res = self.generate_output(halstead.full_text) |
||||
res.add_pretty_table(halstead.core.pretty_table, halstead.core.title) |
||||
res.add_pretty_table(halstead.extended1.pretty_table, halstead.extended1.title) |
||||
res.add_pretty_table(halstead.extended2.pretty_table, halstead.extended2.title) |
||||
self.info(halstead.full_text) |
||||
|
||||
return res |
@ -0,0 +1,32 @@ |
||||
""" |
||||
Robert "Uncle Bob" Martin - Agile software metrics |
||||
https://en.wikipedia.org/wiki/Software_package_metrics |
||||
|
||||
Efferent Coupling (Ce): Number of contracts that the contract depends on |
||||
Afferent Coupling (Ca): Number of contracts that depend on a contract |
||||
Instability (I): Ratio of efferent coupling to total coupling (Ce / (Ce + Ca)) |
||||
Abstractness (A): Number of abstract contracts / total number of contracts |
||||
Distance from the Main Sequence (D): abs(A + I - 1) |
||||
|
||||
""" |
||||
from slither.printers.abstract_printer import AbstractPrinter |
||||
from slither.utils.martin import MartinMetrics |
||||
from slither.utils.output import Output |
||||
|
||||
|
||||
class Martin(AbstractPrinter): |
||||
ARGUMENT = "martin" |
||||
HELP = "Martin agile software metrics (Ca, Ce, I, A, D)" |
||||
|
||||
WIKI = "https://github.com/trailofbits/slither/wiki/Printer-documentation#martin" |
||||
|
||||
def output(self, _filename: str) -> Output: |
||||
if len(self.contracts) == 0: |
||||
return self.generate_output("No contract found") |
||||
|
||||
martin = MartinMetrics(self.contracts) |
||||
|
||||
res = self.generate_output(martin.full_text) |
||||
res.add_pretty_table(martin.core.pretty_table, martin.core.title) |
||||
self.info(martin.full_text) |
||||
return res |
@ -0,0 +1,348 @@ |
||||
""" |
||||
CK Metrics are a suite of six software metrics proposed by Chidamber and Kemerer in 1994. |
||||
These metrics are used to measure the complexity of a class. |
||||
https://en.wikipedia.org/wiki/Programming_complexity |
||||
|
||||
- Response For a Class (RFC) is a metric that measures the number of unique method calls within a class. |
||||
- Number of Children (NOC) is a metric that measures the number of children a class has. |
||||
- Depth of Inheritance Tree (DIT) is a metric that measures the number of parent classes a class has. |
||||
- Coupling Between Object Classes (CBO) is a metric that measures the number of classes a class is coupled to. |
||||
|
||||
Not implemented: |
||||
- Lack of Cohesion of Methods (LCOM) is a metric that measures the lack of cohesion in methods. |
||||
- Weighted Methods per Class (WMC) is a metric that measures the complexity of a class. |
||||
|
||||
During the calculation of the metrics above, there are a number of other intermediate metrics that are calculated. |
||||
These are also included in the output: |
||||
- State variables: total number of state variables |
||||
- Constants: total number of constants |
||||
- Immutables: total number of immutables |
||||
- Public: total number of public functions |
||||
- External: total number of external functions |
||||
- Internal: total number of internal functions |
||||
- Private: total number of private functions |
||||
- Mutating: total number of state mutating functions |
||||
- View: total number of view functions |
||||
- Pure: total number of pure functions |
||||
- External mutating: total number of external mutating functions |
||||
- No auth or onlyOwner: total number of functions without auth or onlyOwner modifiers |
||||
- No modifiers: total number of functions without modifiers |
||||
- Ext calls: total number of external calls |
||||
|
||||
""" |
||||
from collections import OrderedDict |
||||
from typing import Tuple, List, Dict |
||||
from dataclasses import dataclass, field |
||||
from slither.utils.colors import bold |
||||
from slither.core.declarations import Contract |
||||
from slither.utils.myprettytable import make_pretty_table, MyPrettyTable |
||||
from slither.utils.martin import MartinMetrics |
||||
from slither.slithir.operations.high_level_call import HighLevelCall |
||||
|
||||
|
||||
# Utility functions |
||||
|
||||
|
||||
def compute_dit(contract: Contract, depth: int = 0) -> int: |
||||
""" |
||||
Recursively compute the depth of inheritance tree (DIT) of a contract |
||||
Args: |
||||
contract(core.declarations.contract.Contract): contract to compute DIT for |
||||
depth(int): current depth of the contract |
||||
Returns: |
||||
int: depth of the contract |
||||
""" |
||||
if not contract.inheritance: |
||||
return depth |
||||
max_dit = depth |
||||
for inherited_contract in contract.inheritance: |
||||
dit = compute_dit(inherited_contract, depth + 1) |
||||
max_dit = max(max_dit, dit) |
||||
return max_dit |
||||
|
||||
|
||||
def has_auth(func) -> bool: |
||||
""" |
||||
Check if a function has no auth or only_owner modifiers |
||||
Args: |
||||
func(core.declarations.function.Function): function to check |
||||
Returns: |
||||
bool True if it does have auth or only_owner modifiers |
||||
""" |
||||
for modifier in func.modifiers: |
||||
if "auth" in modifier.name or "only_owner" in modifier.name: |
||||
return True |
||||
return False |
||||
|
||||
|
||||
# Utility classes for calculating CK metrics |
||||
|
||||
|
||||
@dataclass |
||||
# pylint: disable=too-many-instance-attributes |
||||
class CKContractMetrics: |
||||
"""Class to hold the CK metrics for a single contract.""" |
||||
|
||||
contract: Contract |
||||
|
||||
# Used to calculate CBO - should be passed in as a constructor arg |
||||
martin_metrics: Dict |
||||
|
||||
# Used to calculate NOC |
||||
dependents: Dict |
||||
|
||||
state_variables: int = 0 |
||||
constants: int = 0 |
||||
immutables: int = 0 |
||||
public: int = 0 |
||||
external: int = 0 |
||||
internal: int = 0 |
||||
private: int = 0 |
||||
mutating: int = 0 |
||||
view: int = 0 |
||||
pure: int = 0 |
||||
external_mutating: int = 0 |
||||
no_auth_or_only_owner: int = 0 |
||||
no_modifiers: int = 0 |
||||
ext_calls: int = 0 |
||||
rfc: int = 0 |
||||
noc: int = 0 |
||||
dit: int = 0 |
||||
cbo: int = 0 |
||||
|
||||
def __post_init__(self) -> None: |
||||
if not hasattr(self.contract, "functions"): |
||||
return |
||||
self.count_variables() |
||||
self.noc = len(self.dependents[self.contract.name]) |
||||
self.dit = compute_dit(self.contract) |
||||
self.cbo = ( |
||||
self.martin_metrics[self.contract.name].ca + self.martin_metrics[self.contract.name].ce |
||||
) |
||||
self.calculate_metrics() |
||||
|
||||
# pylint: disable=too-many-locals |
||||
# pylint: disable=too-many-branches |
||||
def calculate_metrics(self) -> None: |
||||
"""Calculate the metrics for a contract""" |
||||
rfc = self.public # initialize with public getter count |
||||
for func in self.contract.functions: |
||||
if func.name == "constructor": |
||||
continue |
||||
pure = func.pure |
||||
view = not pure and func.view |
||||
mutating = not pure and not view |
||||
external = func.visibility == "external" |
||||
public = func.visibility == "public" |
||||
internal = func.visibility == "internal" |
||||
private = func.visibility == "private" |
||||
external_public_mutating = external or public and mutating |
||||
external_no_auth = external_public_mutating and not has_auth(func) |
||||
external_no_modifiers = external_public_mutating and len(func.modifiers) == 0 |
||||
if external or public: |
||||
rfc += 1 |
||||
|
||||
high_level_calls = [ |
||||
ir for node in func.nodes for ir in node.irs_ssa if isinstance(ir, HighLevelCall) |
||||
] |
||||
|
||||
# convert irs to string with target function and contract name |
||||
external_calls = [] |
||||
for high_level_call in high_level_calls: |
||||
if isinstance(high_level_call.destination, Contract): |
||||
destination_contract = high_level_call.destination.name |
||||
elif isinstance(high_level_call.destination, str): |
||||
destination_contract = high_level_call.destination |
||||
elif not hasattr(high_level_call.destination, "type"): |
||||
continue |
||||
elif isinstance(high_level_call.destination.type, Contract): |
||||
destination_contract = high_level_call.destination.type.name |
||||
elif isinstance(high_level_call.destination.type, str): |
||||
destination_contract = high_level_call.destination.type |
||||
elif not hasattr(high_level_call.destination.type, "type"): |
||||
continue |
||||
elif isinstance(high_level_call.destination.type.type, Contract): |
||||
destination_contract = high_level_call.destination.type.type.name |
||||
elif isinstance(high_level_call.destination.type.type, str): |
||||
destination_contract = high_level_call.destination.type.type |
||||
else: |
||||
continue |
||||
external_calls.append(f"{high_level_call.function_name}{destination_contract}") |
||||
rfc += len(set(external_calls)) |
||||
|
||||
self.public += public |
||||
self.external += external |
||||
self.internal += internal |
||||
self.private += private |
||||
|
||||
self.mutating += mutating |
||||
self.view += view |
||||
self.pure += pure |
||||
|
||||
self.external_mutating += external_public_mutating |
||||
self.no_auth_or_only_owner += external_no_auth |
||||
self.no_modifiers += external_no_modifiers |
||||
|
||||
self.ext_calls += len(external_calls) |
||||
self.rfc = rfc |
||||
|
||||
def count_variables(self) -> None: |
||||
"""Count the number of variables in a contract""" |
||||
state_variable_count = 0 |
||||
constant_count = 0 |
||||
immutable_count = 0 |
||||
public_getter_count = 0 |
||||
for variable in self.contract.variables: |
||||
if variable.is_constant: |
||||
constant_count += 1 |
||||
elif variable.is_immutable: |
||||
immutable_count += 1 |
||||
else: |
||||
state_variable_count += 1 |
||||
if variable.visibility == "Public": |
||||
public_getter_count += 1 |
||||
self.state_variables = state_variable_count |
||||
self.constants = constant_count |
||||
self.immutables = immutable_count |
||||
|
||||
# initialize RFC with public getter count |
||||
# self.public is used count public functions not public variables |
||||
self.rfc = public_getter_count |
||||
|
||||
def to_dict(self) -> Dict[str, float]: |
||||
"""Return the metrics as a dictionary.""" |
||||
return OrderedDict( |
||||
{ |
||||
"State variables": self.state_variables, |
||||
"Constants": self.constants, |
||||
"Immutables": self.immutables, |
||||
"Public": self.public, |
||||
"External": self.external, |
||||
"Internal": self.internal, |
||||
"Private": self.private, |
||||
"Mutating": self.mutating, |
||||
"View": self.view, |
||||
"Pure": self.pure, |
||||
"External mutating": self.external_mutating, |
||||
"No auth or onlyOwner": self.no_auth_or_only_owner, |
||||
"No modifiers": self.no_modifiers, |
||||
"Ext calls": self.ext_calls, |
||||
"RFC": self.rfc, |
||||
"NOC": self.noc, |
||||
"DIT": self.dit, |
||||
"CBO": self.cbo, |
||||
} |
||||
) |
||||
|
||||
|
||||
@dataclass |
||||
class SectionInfo: |
||||
"""Class to hold the information for a section of the report.""" |
||||
|
||||
title: str |
||||
pretty_table: MyPrettyTable |
||||
txt: str |
||||
|
||||
|
||||
@dataclass |
||||
# pylint: disable=too-many-instance-attributes |
||||
class CKMetrics: |
||||
"""Class to hold the CK metrics for all contracts. Contains methods useful for reporting. |
||||
|
||||
There are 5 sections in the report: |
||||
1. Variable count by type (state, constant, immutable) |
||||
2. Function count by visibility (public, external, internal, private) |
||||
3. Function count by mutability (mutating, view, pure) |
||||
4. External mutating function count by modifier (external mutating, no auth or onlyOwner, no modifiers) |
||||
5. CK metrics (RFC, NOC, DIT, CBO) |
||||
""" |
||||
|
||||
contracts: List[Contract] = field(default_factory=list) |
||||
contract_metrics: OrderedDict = field(default_factory=OrderedDict) |
||||
title: str = "CK complexity metrics" |
||||
full_text: str = "" |
||||
auxiliary1: SectionInfo = field(default=SectionInfo) |
||||
auxiliary2: SectionInfo = field(default=SectionInfo) |
||||
auxiliary3: SectionInfo = field(default=SectionInfo) |
||||
auxiliary4: SectionInfo = field(default=SectionInfo) |
||||
core: SectionInfo = field(default=SectionInfo) |
||||
AUXILIARY1_KEYS = ( |
||||
"State variables", |
||||
"Constants", |
||||
"Immutables", |
||||
) |
||||
AUXILIARY2_KEYS = ( |
||||
"Public", |
||||
"External", |
||||
"Internal", |
||||
"Private", |
||||
) |
||||
AUXILIARY3_KEYS = ( |
||||
"Mutating", |
||||
"View", |
||||
"Pure", |
||||
) |
||||
AUXILIARY4_KEYS = ( |
||||
"External mutating", |
||||
"No auth or onlyOwner", |
||||
"No modifiers", |
||||
) |
||||
CORE_KEYS = ( |
||||
"Ext calls", |
||||
"RFC", |
||||
"NOC", |
||||
"DIT", |
||||
"CBO", |
||||
) |
||||
SECTIONS: Tuple[Tuple[str, str, Tuple[str]]] = ( |
||||
("Variables", "auxiliary1", AUXILIARY1_KEYS), |
||||
("Function visibility", "auxiliary2", AUXILIARY2_KEYS), |
||||
("State mutability", "auxiliary3", AUXILIARY3_KEYS), |
||||
("External mutating functions", "auxiliary4", AUXILIARY4_KEYS), |
||||
("Core", "core", CORE_KEYS), |
||||
) |
||||
|
||||
def __post_init__(self) -> None: |
||||
martin_metrics = MartinMetrics(self.contracts).contract_metrics |
||||
dependents = { |
||||
inherited.name: { |
||||
contract.name |
||||
for contract in self.contracts |
||||
if inherited.name in contract.inheritance |
||||
} |
||||
for inherited in self.contracts |
||||
} |
||||
for contract in self.contracts: |
||||
self.contract_metrics[contract.name] = CKContractMetrics( |
||||
contract=contract, martin_metrics=martin_metrics, dependents=dependents |
||||
) |
||||
|
||||
# Create the table and text for each section. |
||||
data = { |
||||
contract.name: self.contract_metrics[contract.name].to_dict() |
||||
for contract in self.contracts |
||||
} |
||||
|
||||
subtitle = "" |
||||
# Update each section |
||||
for (title, attr, keys) in self.SECTIONS: |
||||
if attr == "core": |
||||
# Special handling for core section |
||||
totals_enabled = False |
||||
subtitle += bold("RFC: Response For a Class\n") |
||||
subtitle += bold("NOC: Number of Children\n") |
||||
subtitle += bold("DIT: Depth of Inheritance Tree\n") |
||||
subtitle += bold("CBO: Coupling Between Object Classes\n") |
||||
else: |
||||
totals_enabled = True |
||||
subtitle = "" |
||||
|
||||
pretty_table = make_pretty_table(["Contract", *keys], data, totals=totals_enabled) |
||||
section_title = f"{self.title} ({title})" |
||||
txt = f"\n\n{section_title}:\n{subtitle}{pretty_table}\n" |
||||
self.full_text += txt |
||||
setattr( |
||||
self, |
||||
attr, |
||||
SectionInfo(title=section_title, pretty_table=pretty_table, txt=txt), |
||||
) |
@ -0,0 +1,202 @@ |
||||
from typing import Union |
||||
|
||||
from slither.core import variables |
||||
from slither.core.declarations import ( |
||||
SolidityVariable, |
||||
SolidityVariableComposed, |
||||
Structure, |
||||
Enum, |
||||
Contract, |
||||
) |
||||
from slither.core import solidity_types |
||||
from slither.slithir import operations |
||||
from slither.slithir import variables as SlitherIRVariable |
||||
|
||||
|
||||
# pylint: disable=too-many-branches |
||||
def ntype(_type: Union[solidity_types.Type, str]) -> str: |
||||
if isinstance(_type, solidity_types.ElementaryType): |
||||
_type = str(_type) |
||||
elif isinstance(_type, solidity_types.ArrayType): |
||||
if isinstance(_type.type, solidity_types.ElementaryType): |
||||
_type = str(_type) |
||||
else: |
||||
_type = "user_defined_array" |
||||
elif isinstance(_type, Structure): |
||||
_type = str(_type) |
||||
elif isinstance(_type, Enum): |
||||
_type = str(_type) |
||||
elif isinstance(_type, solidity_types.MappingType): |
||||
_type = str(_type) |
||||
elif isinstance(_type, solidity_types.UserDefinedType): |
||||
if isinstance(_type.type, Contract): |
||||
_type = f"contract({_type.type.name})" |
||||
elif isinstance(_type.type, Structure): |
||||
_type = f"struct({_type.type.name})" |
||||
elif isinstance(_type.type, Enum): |
||||
_type = f"enum({_type.type.name})" |
||||
else: |
||||
_type = str(_type) |
||||
|
||||
_type = _type.replace(" memory", "") |
||||
_type = _type.replace(" storage ref", "") |
||||
|
||||
if "struct" in _type: |
||||
return "struct" |
||||
if "enum" in _type: |
||||
return "enum" |
||||
if "tuple" in _type: |
||||
return "tuple" |
||||
if "contract" in _type: |
||||
return "contract" |
||||
if "mapping" in _type: |
||||
return "mapping" |
||||
return _type.replace(" ", "_") |
||||
|
||||
|
||||
# pylint: disable=too-many-branches |
||||
def encode_var_for_compare(var: Union[variables.Variable, SolidityVariable]) -> str: |
||||
|
||||
# variables |
||||
if isinstance(var, SlitherIRVariable.Constant): |
||||
return f"constant({ntype(var.type)},{var.value})" |
||||
if isinstance(var, SolidityVariableComposed): |
||||
return f"solidity_variable_composed({var.name})" |
||||
if isinstance(var, SolidityVariable): |
||||
return f"solidity_variable{var.name}" |
||||
if isinstance(var, SlitherIRVariable.TemporaryVariable): |
||||
return "temporary_variable" |
||||
if isinstance(var, SlitherIRVariable.ReferenceVariable): |
||||
return f"reference({ntype(var.type)})" |
||||
if isinstance(var, variables.LocalVariable): |
||||
return f"local_solc_variable({ntype(var.type)},{var.location})" |
||||
if isinstance(var, variables.StateVariable): |
||||
if not (var.is_constant or var.is_immutable): |
||||
try: |
||||
slot, _ = var.contract.compilation_unit.storage_layout_of(var.contract, var) |
||||
except KeyError: |
||||
slot = var.name |
||||
else: |
||||
slot = var.name |
||||
return f"state_solc_variable({ntype(var.type)},{slot})" |
||||
if isinstance(var, variables.LocalVariableInitFromTuple): |
||||
return "local_variable_init_tuple" |
||||
if isinstance(var, SlitherIRVariable.TupleVariable): |
||||
return "tuple_variable" |
||||
|
||||
# default |
||||
return "" |
||||
|
||||
|
||||
# pylint: disable=too-many-branches |
||||
def encode_ir_for_upgradeability_compare(ir: operations.Operation) -> str: |
||||
# operations |
||||
if isinstance(ir, operations.Assignment): |
||||
return f"({encode_var_for_compare(ir.lvalue)}):=({encode_var_for_compare(ir.rvalue)})" |
||||
if isinstance(ir, operations.Index): |
||||
return f"index({ntype(ir.variable_right.type)})" |
||||
if isinstance(ir, operations.Member): |
||||
return "member" # .format(ntype(ir._type)) |
||||
if isinstance(ir, operations.Length): |
||||
return "length" |
||||
if isinstance(ir, operations.Binary): |
||||
return f"binary({encode_var_for_compare(ir.variable_left)}{ir.type}{encode_var_for_compare(ir.variable_right)})" |
||||
if isinstance(ir, operations.Unary): |
||||
return f"unary({str(ir.type)})" |
||||
if isinstance(ir, operations.Condition): |
||||
return f"condition({encode_var_for_compare(ir.value)})" |
||||
if isinstance(ir, operations.NewStructure): |
||||
return "new_structure" |
||||
if isinstance(ir, operations.NewContract): |
||||
return "new_contract" |
||||
if isinstance(ir, operations.NewArray): |
||||
return f"new_array({ntype(ir.array_type)})" |
||||
if isinstance(ir, operations.NewElementaryType): |
||||
return f"new_elementary({ntype(ir.type)})" |
||||
if isinstance(ir, operations.Delete): |
||||
return f"delete({encode_var_for_compare(ir.lvalue)},{encode_var_for_compare(ir.variable)})" |
||||
if isinstance(ir, operations.SolidityCall): |
||||
return f"solidity_call({ir.function.full_name})" |
||||
if isinstance(ir, operations.InternalCall): |
||||
return f"internal_call({ntype(ir.type_call)})" |
||||
if isinstance(ir, operations.EventCall): # is this useful? |
||||
return "event" |
||||
if isinstance(ir, operations.LibraryCall): |
||||
return "library_call" |
||||
if isinstance(ir, operations.InternalDynamicCall): |
||||
return "internal_dynamic_call" |
||||
if isinstance(ir, operations.HighLevelCall): # TODO: improve |
||||
return "high_level_call" |
||||
if isinstance(ir, operations.LowLevelCall): # TODO: improve |
||||
return "low_level_call" |
||||
if isinstance(ir, operations.TypeConversion): |
||||
return f"type_conversion({ntype(ir.type)})" |
||||
if isinstance(ir, operations.Return): # this can be improved using values |
||||
return "return" # .format(ntype(ir.type)) |
||||
if isinstance(ir, operations.Transfer): |
||||
return f"transfer({encode_var_for_compare(ir.call_value)})" |
||||
if isinstance(ir, operations.Send): |
||||
return f"send({encode_var_for_compare(ir.call_value)})" |
||||
if isinstance(ir, operations.Unpack): # TODO: improve |
||||
return "unpack" |
||||
if isinstance(ir, operations.InitArray): # TODO: improve |
||||
return "init_array" |
||||
|
||||
# default |
||||
return "" |
||||
|
||||
|
||||
def encode_ir_for_halstead(ir: operations.Operation) -> str: |
||||
# operations |
||||
if isinstance(ir, operations.Assignment): |
||||
return "assignment" |
||||
if isinstance(ir, operations.Index): |
||||
return "index" |
||||
if isinstance(ir, operations.Member): |
||||
return "member" # .format(ntype(ir._type)) |
||||
if isinstance(ir, operations.Length): |
||||
return "length" |
||||
if isinstance(ir, operations.Binary): |
||||
return f"binary({str(ir.type)})" |
||||
if isinstance(ir, operations.Unary): |
||||
return f"unary({str(ir.type)})" |
||||
if isinstance(ir, operations.Condition): |
||||
return f"condition({encode_var_for_compare(ir.value)})" |
||||
if isinstance(ir, operations.NewStructure): |
||||
return "new_structure" |
||||
if isinstance(ir, operations.NewContract): |
||||
return "new_contract" |
||||
if isinstance(ir, operations.NewArray): |
||||
return f"new_array({ntype(ir.array_type)})" |
||||
if isinstance(ir, operations.NewElementaryType): |
||||
return f"new_elementary({ntype(ir.type)})" |
||||
if isinstance(ir, operations.Delete): |
||||
return "delete" |
||||
if isinstance(ir, operations.SolidityCall): |
||||
return f"solidity_call({ir.function.full_name})" |
||||
if isinstance(ir, operations.InternalCall): |
||||
return f"internal_call({ntype(ir.type_call)})" |
||||
if isinstance(ir, operations.EventCall): # is this useful? |
||||
return "event" |
||||
if isinstance(ir, operations.LibraryCall): |
||||
return "library_call" |
||||
if isinstance(ir, operations.InternalDynamicCall): |
||||
return "internal_dynamic_call" |
||||
if isinstance(ir, operations.HighLevelCall): # TODO: improve |
||||
return "high_level_call" |
||||
if isinstance(ir, operations.LowLevelCall): # TODO: improve |
||||
return "low_level_call" |
||||
if isinstance(ir, operations.TypeConversion): |
||||
return f"type_conversion({ntype(ir.type)})" |
||||
if isinstance(ir, operations.Return): # this can be improved using values |
||||
return "return" # .format(ntype(ir.type)) |
||||
if isinstance(ir, operations.Transfer): |
||||
return "transfer" |
||||
if isinstance(ir, operations.Send): |
||||
return "send" |
||||
if isinstance(ir, operations.Unpack): # TODO: improve |
||||
return "unpack" |
||||
if isinstance(ir, operations.InitArray): # TODO: improve |
||||
return "init_array" |
||||
# default |
||||
raise NotImplementedError(f"encode_ir_for_halstead: {ir}") |
@ -0,0 +1,233 @@ |
||||
""" |
||||
Halstead complexity metrics |
||||
https://en.wikipedia.org/wiki/Halstead_complexity_measures |
||||
|
||||
12 metrics based on the number of unique operators and operands: |
||||
|
||||
Core metrics: |
||||
n1 = the number of distinct operators |
||||
n2 = the number of distinct operands |
||||
N1 = the total number of operators |
||||
N2 = the total number of operands |
||||
|
||||
Extended metrics1: |
||||
n = n1 + n2 # Program vocabulary |
||||
N = N1 + N2 # Program length |
||||
S = n1 * log2(n1) + n2 * log2(n2) # Estimated program length |
||||
V = N * log2(n) # Volume |
||||
|
||||
Extended metrics2: |
||||
D = (n1 / 2) * (N2 / n2) # Difficulty |
||||
E = D * V # Effort |
||||
T = E / 18 seconds # Time required to program |
||||
B = (E^(2/3)) / 3000 # Number of delivered bugs |
||||
|
||||
|
||||
""" |
||||
import math |
||||
from collections import OrderedDict |
||||
from dataclasses import dataclass, field |
||||
from typing import Tuple, List, Dict |
||||
|
||||
from slither.core.declarations import Contract |
||||
from slither.slithir.variables.temporary import TemporaryVariable |
||||
from slither.utils.encoding import encode_ir_for_halstead |
||||
from slither.utils.myprettytable import make_pretty_table, MyPrettyTable |
||||
|
||||
|
||||
# pylint: disable=too-many-branches |
||||
|
||||
|
||||
@dataclass |
||||
# pylint: disable=too-many-instance-attributes |
||||
class HalsteadContractMetrics: |
||||
"""Class to hold the Halstead metrics for a single contract.""" |
||||
|
||||
contract: Contract |
||||
all_operators: List[str] = field(default_factory=list) |
||||
all_operands: List[str] = field(default_factory=list) |
||||
n1: int = 0 |
||||
n2: int = 0 |
||||
N1: int = 0 |
||||
N2: int = 0 |
||||
n: int = 0 |
||||
N: int = 0 |
||||
S: float = 0 |
||||
V: float = 0 |
||||
D: float = 0 |
||||
E: float = 0 |
||||
T: float = 0 |
||||
B: float = 0 |
||||
|
||||
def __post_init__(self) -> None: |
||||
"""Operators and operands can be passed in as constructor args to avoid computing |
||||
them based on the contract. Useful for computing metrics for ALL_CONTRACTS""" |
||||
|
||||
if len(self.all_operators) == 0: |
||||
if not hasattr(self.contract, "functions"): |
||||
return |
||||
self.populate_operators_and_operands() |
||||
if len(self.all_operators) > 0: |
||||
self.compute_metrics() |
||||
|
||||
def to_dict(self) -> Dict[str, float]: |
||||
"""Return the metrics as a dictionary.""" |
||||
return OrderedDict( |
||||
{ |
||||
"Total Operators": self.N1, |
||||
"Unique Operators": self.n1, |
||||
"Total Operands": self.N2, |
||||
"Unique Operands": self.n2, |
||||
"Vocabulary": str(self.n1 + self.n2), |
||||
"Program Length": str(self.N1 + self.N2), |
||||
"Estimated Length": f"{self.S:.0f}", |
||||
"Volume": f"{self.V:.0f}", |
||||
"Difficulty": f"{self.D:.0f}", |
||||
"Effort": f"{self.E:.0f}", |
||||
"Time": f"{self.T:.0f}", |
||||
"Estimated Bugs": f"{self.B:.3f}", |
||||
} |
||||
) |
||||
|
||||
def populate_operators_and_operands(self) -> None: |
||||
"""Populate the operators and operands lists.""" |
||||
operators = [] |
||||
operands = [] |
||||
|
||||
for func in self.contract.functions: |
||||
for node in func.nodes: |
||||
for operation in node.irs: |
||||
# use operation.expression.type to get the unique operator type |
||||
encoded_operator = encode_ir_for_halstead(operation) |
||||
operators.append(encoded_operator) |
||||
|
||||
# use operation.used to get the operands of the operation ignoring the temporary variables |
||||
operands.extend( |
||||
[op for op in operation.used if not isinstance(op, TemporaryVariable)] |
||||
) |
||||
self.all_operators.extend(operators) |
||||
self.all_operands.extend(operands) |
||||
|
||||
def compute_metrics(self, all_operators=None, all_operands=None) -> None: |
||||
"""Compute the Halstead metrics.""" |
||||
if all_operators is None: |
||||
all_operators = self.all_operators |
||||
all_operands = self.all_operands |
||||
|
||||
# core metrics |
||||
self.n1 = len(set(all_operators)) |
||||
self.n2 = len(set(all_operands)) |
||||
self.N1 = len(all_operators) |
||||
self.N2 = len(all_operands) |
||||
if any(number <= 0 for number in [self.n1, self.n2, self.N1, self.N2]): |
||||
raise ValueError("n1 and n2 must be greater than 0") |
||||
|
||||
# extended metrics 1 |
||||
self.n = self.n1 + self.n2 |
||||
self.N = self.N1 + self.N2 |
||||
self.S = self.n1 * math.log2(self.n1) + self.n2 * math.log2(self.n2) |
||||
self.V = self.N * math.log2(self.n) |
||||
|
||||
# extended metrics 2 |
||||
self.D = (self.n1 / 2) * (self.N2 / self.n2) |
||||
self.E = self.D * self.V |
||||
self.T = self.E / 18 |
||||
self.B = (self.E ** (2 / 3)) / 3000 |
||||
|
||||
|
||||
@dataclass |
||||
class SectionInfo: |
||||
"""Class to hold the information for a section of the report.""" |
||||
|
||||
title: str |
||||
pretty_table: MyPrettyTable |
||||
txt: str |
||||
|
||||
|
||||
@dataclass |
||||
# pylint: disable=too-many-instance-attributes |
||||
class HalsteadMetrics: |
||||
"""Class to hold the Halstead metrics for all contracts. Contains methods useful for reporting. |
||||
|
||||
There are 3 sections in the report: |
||||
1. Core metrics (n1, n2, N1, N2) |
||||
2. Extended metrics 1 (n, N, S, V) |
||||
3. Extended metrics 2 (D, E, T, B) |
||||
|
||||
""" |
||||
|
||||
contracts: List[Contract] = field(default_factory=list) |
||||
contract_metrics: OrderedDict = field(default_factory=OrderedDict) |
||||
title: str = "Halstead complexity metrics" |
||||
full_text: str = "" |
||||
core: SectionInfo = field(default=SectionInfo) |
||||
extended1: SectionInfo = field(default=SectionInfo) |
||||
extended2: SectionInfo = field(default=SectionInfo) |
||||
CORE_KEYS = ( |
||||
"Total Operators", |
||||
"Unique Operators", |
||||
"Total Operands", |
||||
"Unique Operands", |
||||
) |
||||
EXTENDED1_KEYS = ( |
||||
"Vocabulary", |
||||
"Program Length", |
||||
"Estimated Length", |
||||
"Volume", |
||||
) |
||||
EXTENDED2_KEYS = ( |
||||
"Difficulty", |
||||
"Effort", |
||||
"Time", |
||||
"Estimated Bugs", |
||||
) |
||||
SECTIONS: Tuple[Tuple[str, str, Tuple[str]]] = ( |
||||
("Core", "core", CORE_KEYS), |
||||
("Extended 1/2", "extended1", EXTENDED1_KEYS), |
||||
("Extended 2/2", "extended2", EXTENDED2_KEYS), |
||||
) |
||||
|
||||
def __post_init__(self) -> None: |
||||
# Compute the metrics for each contract and for all contracts. |
||||
self.update_contract_metrics() |
||||
self.add_all_contracts_metrics() |
||||
self.update_reporting_sections() |
||||
|
||||
def update_contract_metrics(self) -> None: |
||||
for contract in self.contracts: |
||||
self.contract_metrics[contract.name] = HalsteadContractMetrics(contract=contract) |
||||
|
||||
def add_all_contracts_metrics(self) -> None: |
||||
# If there are more than 1 contract, compute the metrics for all contracts. |
||||
if len(self.contracts) <= 1: |
||||
return |
||||
all_operators = [ |
||||
operator |
||||
for contract in self.contracts |
||||
for operator in self.contract_metrics[contract.name].all_operators |
||||
] |
||||
all_operands = [ |
||||
operand |
||||
for contract in self.contracts |
||||
for operand in self.contract_metrics[contract.name].all_operands |
||||
] |
||||
self.contract_metrics["ALL CONTRACTS"] = HalsteadContractMetrics( |
||||
None, all_operators=all_operators, all_operands=all_operands |
||||
) |
||||
|
||||
def update_reporting_sections(self) -> None: |
||||
# Create the table and text for each section. |
||||
data = { |
||||
contract.name: self.contract_metrics[contract.name].to_dict() |
||||
for contract in self.contracts |
||||
} |
||||
for (title, attr, keys) in self.SECTIONS: |
||||
pretty_table = make_pretty_table(["Contract", *keys], data, False) |
||||
section_title = f"{self.title} ({title})" |
||||
txt = f"\n\n{section_title}:\n{pretty_table}\n" |
||||
self.full_text += txt |
||||
setattr( |
||||
self, |
||||
attr, |
||||
SectionInfo(title=section_title, pretty_table=pretty_table, txt=txt), |
||||
) |
@ -0,0 +1,157 @@ |
||||
""" |
||||
Robert "Uncle Bob" Martin - Agile software metrics |
||||
https://en.wikipedia.org/wiki/Software_package_metrics |
||||
|
||||
Efferent Coupling (Ce): Number of contracts that the contract depends on |
||||
Afferent Coupling (Ca): Number of contracts that depend on a contract |
||||
Instability (I): Ratio of efferent coupling to total coupling (Ce / (Ce + Ca)) |
||||
Abstractness (A): Number of abstract contracts / total number of contracts |
||||
Distance from the Main Sequence (D): abs(A + I - 1) |
||||
|
||||
""" |
||||
from typing import Tuple, List, Dict |
||||
from dataclasses import dataclass, field |
||||
from collections import OrderedDict |
||||
from slither.slithir.operations.high_level_call import HighLevelCall |
||||
from slither.core.declarations import Contract |
||||
from slither.utils.myprettytable import make_pretty_table, MyPrettyTable |
||||
|
||||
|
||||
@dataclass |
||||
class MartinContractMetrics: |
||||
contract: Contract |
||||
ca: int |
||||
ce: int |
||||
abstractness: float |
||||
i: float = 0.0 |
||||
d: float = 0.0 |
||||
|
||||
def __post_init__(self) -> None: |
||||
if self.ce + self.ca > 0: |
||||
self.i = float(self.ce / (self.ce + self.ca)) |
||||
self.d = float(abs(self.i - self.abstractness)) |
||||
|
||||
def to_dict(self) -> Dict: |
||||
return { |
||||
"Dependents": self.ca, |
||||
"Dependencies": self.ce, |
||||
"Instability": f"{self.i:.2f}", |
||||
"Distance from main sequence": f"{self.d:.2f}", |
||||
} |
||||
|
||||
|
||||
@dataclass |
||||
class SectionInfo: |
||||
"""Class to hold the information for a section of the report.""" |
||||
|
||||
title: str |
||||
pretty_table: MyPrettyTable |
||||
txt: str |
||||
|
||||
|
||||
@dataclass |
||||
class MartinMetrics: |
||||
contracts: List[Contract] = field(default_factory=list) |
||||
abstractness: float = 0.0 |
||||
contract_metrics: OrderedDict = field(default_factory=OrderedDict) |
||||
title: str = "Martin complexity metrics" |
||||
full_text: str = "" |
||||
core: SectionInfo = field(default=SectionInfo) |
||||
CORE_KEYS = ( |
||||
"Dependents", |
||||
"Dependencies", |
||||
"Instability", |
||||
"Distance from main sequence", |
||||
) |
||||
SECTIONS: Tuple[Tuple[str, str, Tuple[str]]] = (("Core", "core", CORE_KEYS),) |
||||
|
||||
def __post_init__(self) -> None: |
||||
self.update_abstractness() |
||||
self.update_coupling() |
||||
self.update_reporting_sections() |
||||
|
||||
def update_reporting_sections(self) -> None: |
||||
# Create the table and text for each section. |
||||
data = { |
||||
contract.name: self.contract_metrics[contract.name].to_dict() |
||||
for contract in self.contracts |
||||
} |
||||
for (title, attr, keys) in self.SECTIONS: |
||||
pretty_table = make_pretty_table(["Contract", *keys], data, False) |
||||
section_title = f"{self.title} ({title})" |
||||
txt = f"\n\n{section_title}:\n" |
||||
txt = "Martin agile software metrics\n" |
||||
txt += "Efferent Coupling (Ce) - Number of contracts that a contract depends on\n" |
||||
txt += "Afferent Coupling (Ca) - Number of contracts that depend on the contract\n" |
||||
txt += ( |
||||
"Instability (I) - Ratio of efferent coupling to total coupling (Ce / (Ce + Ca))\n" |
||||
) |
||||
txt += "Abstractness (A) - Number of abstract contracts / total number of contracts\n" |
||||
txt += "Distance from the Main Sequence (D) - abs(A + I - 1)\n" |
||||
txt += "\n" |
||||
txt += f"Abstractness (overall): {round(self.abstractness, 2)}\n" |
||||
txt += f"{pretty_table}\n" |
||||
self.full_text += txt |
||||
setattr( |
||||
self, |
||||
attr, |
||||
SectionInfo(title=section_title, pretty_table=pretty_table, txt=txt), |
||||
) |
||||
|
||||
def update_abstractness(self) -> None: |
||||
abstract_contract_count = 0 |
||||
for c in self.contracts: |
||||
if not c.is_fully_implemented: |
||||
abstract_contract_count += 1 |
||||
self.abstractness = float(abstract_contract_count / len(self.contracts)) |
||||
|
||||
# pylint: disable=too-many-branches |
||||
def update_coupling(self) -> None: |
||||
dependencies = {} |
||||
for contract in self.contracts: |
||||
external_calls = [] |
||||
for func in contract.functions: |
||||
high_level_calls = [ |
||||
ir |
||||
for node in func.nodes |
||||
for ir in node.irs_ssa |
||||
if isinstance(ir, HighLevelCall) |
||||
] |
||||
# convert irs to string with target function and contract name |
||||
# Get the target contract name for each high level call |
||||
new_external_calls = [] |
||||
for high_level_call in high_level_calls: |
||||
if isinstance(high_level_call.destination, Contract): |
||||
new_external_call = high_level_call.destination.name |
||||
elif isinstance(high_level_call.destination, str): |
||||
new_external_call = high_level_call.destination |
||||
elif not hasattr(high_level_call.destination, "type"): |
||||
continue |
||||
elif isinstance(high_level_call.destination.type, Contract): |
||||
new_external_call = high_level_call.destination.type.name |
||||
elif isinstance(high_level_call.destination.type, str): |
||||
new_external_call = high_level_call.destination.type |
||||
elif not hasattr(high_level_call.destination.type, "type"): |
||||
continue |
||||
elif isinstance(high_level_call.destination.type.type, Contract): |
||||
new_external_call = high_level_call.destination.type.type.name |
||||
elif isinstance(high_level_call.destination.type.type, str): |
||||
new_external_call = high_level_call.destination.type.type |
||||
else: |
||||
continue |
||||
new_external_calls.append(new_external_call) |
||||
external_calls.extend(new_external_calls) |
||||
dependencies[contract.name] = set(external_calls) |
||||
dependents = {} |
||||
for contract, deps in dependencies.items(): |
||||
for dep in deps: |
||||
if dep not in dependents: |
||||
dependents[dep] = set() |
||||
dependents[dep].add(contract) |
||||
|
||||
for contract in self.contracts: |
||||
ce = len(dependencies.get(contract.name, [])) |
||||
ca = len(dependents.get(contract.name, [])) |
||||
self.contract_metrics[contract.name] = MartinContractMetrics( |
||||
contract, ca, ce, self.abstractness |
||||
) |
@ -0,0 +1,71 @@ |
||||
""" |
||||
Various utils for sarif/vscode |
||||
""" |
||||
import json |
||||
from pathlib import Path |
||||
from typing import List, Dict, Optional, Tuple, Any |
||||
|
||||
|
||||
def _parse_index(key: str) -> Optional[Tuple[int, int]]: |
||||
if key.count(":") != 2: |
||||
return None |
||||
|
||||
try: |
||||
run = int(key[key.find(":") + 1 : key.rfind(":")]) |
||||
index = int(key[key.rfind(":") + 1 :]) |
||||
return run, index |
||||
except ValueError: |
||||
return None |
||||
|
||||
|
||||
def _get_indexes(path_to_triage: Path) -> List[Tuple[int, int]]: |
||||
try: |
||||
with open(path_to_triage, encoding="utf8") as file_desc: |
||||
triage = json.load(file_desc) |
||||
except json.decoder.JSONDecodeError: |
||||
return [] |
||||
|
||||
resultIdToNotes: Dict[str, Dict] = triage.get("resultIdToNotes", {}) |
||||
|
||||
indexes: List[Tuple[int, int]] = [] |
||||
for key, data in resultIdToNotes.items(): |
||||
if "status" in data and data["status"] == 1: |
||||
parsed = _parse_index(key) |
||||
if parsed: |
||||
indexes.append(parsed) |
||||
|
||||
return indexes |
||||
|
||||
|
||||
def read_triage_info(path_to_sarif: Path, path_to_triage: Path) -> List[str]: |
||||
try: |
||||
with open(path_to_sarif, encoding="utf8") as file_desc: |
||||
sarif = json.load(file_desc) |
||||
except json.decoder.JSONDecodeError: |
||||
return [] |
||||
|
||||
runs: List[Dict[str, Any]] = sarif.get("runs", []) |
||||
|
||||
# Don't support multiple runs for now |
||||
if len(runs) != 1: |
||||
return [] |
||||
|
||||
run_results: List[Dict] = runs[0].get("results", []) |
||||
|
||||
indexes = _get_indexes(path_to_triage) |
||||
|
||||
ids: List[str] = [] |
||||
for run, index in indexes: |
||||
|
||||
# We dont support multiple runs for now |
||||
if run != 0: |
||||
continue |
||||
try: |
||||
elem = run_results[index] |
||||
except KeyError: |
||||
continue |
||||
if "partialFingerprints" in elem: |
||||
if "id" in elem["partialFingerprints"]: |
||||
ids.append(elem["partialFingerprints"]["id"]) |
||||
|
||||
return ids |
@ -0,0 +1,466 @@ |
||||
from typing import Dict, Callable, List |
||||
from slither.vyper_parsing.ast.types import ( |
||||
ASTNode, |
||||
Module, |
||||
ImportFrom, |
||||
EventDef, |
||||
AnnAssign, |
||||
Name, |
||||
Call, |
||||
StructDef, |
||||
VariableDecl, |
||||
Subscript, |
||||
Index, |
||||
Hex, |
||||
Int, |
||||
Str, |
||||
Tuple, |
||||
FunctionDef, |
||||
Assign, |
||||
Raise, |
||||
Attribute, |
||||
Assert, |
||||
Keyword, |
||||
Arguments, |
||||
Arg, |
||||
UnaryOp, |
||||
BinOp, |
||||
Expr, |
||||
Log, |
||||
Return, |
||||
VyDict, |
||||
VyList, |
||||
NameConstant, |
||||
If, |
||||
Compare, |
||||
For, |
||||
Break, |
||||
Continue, |
||||
Pass, |
||||
InterfaceDef, |
||||
EnumDef, |
||||
Bytes, |
||||
AugAssign, |
||||
BoolOp, |
||||
) |
||||
|
||||
|
||||
class ParsingError(Exception): |
||||
pass |
||||
|
||||
|
||||
def _extract_base_props(raw: Dict) -> Dict: |
||||
return { |
||||
"src": raw["src"], |
||||
"node_id": raw["node_id"], |
||||
} |
||||
|
||||
|
||||
def _extract_decl_props(raw: Dict) -> Dict: |
||||
return { |
||||
"doc_string": parse_doc_str(raw["doc_string"]) if raw["doc_string"] else None, |
||||
**_extract_base_props(raw), |
||||
} |
||||
|
||||
|
||||
def parse_module(raw: Dict) -> Module: |
||||
nodes_parsed: List[ASTNode] = [] |
||||
|
||||
for node in raw["body"]: |
||||
nodes_parsed.append(parse(node)) |
||||
|
||||
return Module(name=raw["name"], body=nodes_parsed, **_extract_decl_props(raw)) |
||||
|
||||
|
||||
def parse_import_from(raw: Dict) -> ImportFrom: |
||||
return ImportFrom( |
||||
module=raw["module"], |
||||
name=raw["name"], |
||||
alias=raw["alias"], |
||||
**_extract_base_props(raw), |
||||
) |
||||
|
||||
|
||||
def parse_event_def(raw: Dict) -> EventDef: |
||||
body_parsed: List[ASTNode] = [] |
||||
for node in raw["body"]: |
||||
body_parsed.append(parse(node)) |
||||
|
||||
return EventDef( |
||||
name=raw["name"], |
||||
body=body_parsed, |
||||
*_extract_base_props(raw), |
||||
) |
||||
|
||||
|
||||
def parse_ann_assign(raw: Dict) -> AnnAssign: |
||||
return AnnAssign( |
||||
target=parse(raw["target"]), |
||||
annotation=parse(raw["annotation"]), |
||||
value=parse(raw["value"]) if raw["value"] else None, |
||||
**_extract_base_props(raw), |
||||
) |
||||
|
||||
|
||||
def parse_name(raw: Dict) -> Name: |
||||
return Name( |
||||
id=raw["id"], |
||||
**_extract_base_props(raw), |
||||
) |
||||
|
||||
|
||||
def parse_call(raw: Dict) -> Call: |
||||
return Call( |
||||
func=parse(raw["func"]), |
||||
args=[parse(arg) for arg in raw["args"]], |
||||
keyword=parse(raw["keyword"]) if raw["keyword"] else None, |
||||
keywords=[parse(keyword) for keyword in raw["keywords"]], |
||||
**_extract_base_props(raw), |
||||
) |
||||
|
||||
|
||||
def parse_struct_def(raw: Dict) -> StructDef: |
||||
body_parsed: List[ASTNode] = [] |
||||
for node in raw["body"]: |
||||
body_parsed.append(parse(node)) |
||||
|
||||
return StructDef( |
||||
name=raw["name"], |
||||
body=body_parsed, |
||||
**_extract_base_props(raw), |
||||
) |
||||
|
||||
|
||||
def parse_variable_decl(raw: Dict) -> VariableDecl: |
||||
return VariableDecl( |
||||
annotation=parse(raw["annotation"]), |
||||
value=parse(raw["value"]) if raw["value"] else None, |
||||
target=parse(raw["target"]), |
||||
is_constant=raw["is_constant"], |
||||
is_immutable=raw["is_immutable"], |
||||
is_public=raw["is_public"], |
||||
**_extract_base_props(raw), |
||||
) |
||||
|
||||
|
||||
def parse_subscript(raw: Dict) -> Subscript: |
||||
return Subscript( |
||||
value=parse(raw["value"]), |
||||
slice=parse(raw["slice"]), |
||||
**_extract_base_props(raw), |
||||
) |
||||
|
||||
|
||||
def parse_index(raw: Dict) -> Index: |
||||
return Index(value=parse(raw["value"]), **_extract_base_props(raw)) |
||||
|
||||
|
||||
def parse_bytes(raw: Dict) -> Bytes: |
||||
return Bytes(value=raw["value"], **_extract_base_props(raw)) |
||||
|
||||
|
||||
def parse_hex(raw: Dict) -> Hex: |
||||
return Hex(value=raw["value"], **_extract_base_props(raw)) |
||||
|
||||
|
||||
def parse_int(raw: Dict) -> Int: |
||||
return Int(value=raw["value"], **_extract_base_props(raw)) |
||||
|
||||
|
||||
def parse_str(raw: Dict) -> Str: |
||||
return Str(value=raw["value"], **_extract_base_props(raw)) |
||||
|
||||
|
||||
def parse_tuple(raw: Dict) -> ASTNode: |
||||
return Tuple(elements=[parse(elem) for elem in raw["elements"]], **_extract_base_props(raw)) |
||||
|
||||
|
||||
def parse_function_def(raw: Dict) -> FunctionDef: |
||||
body_parsed: List[ASTNode] = [] |
||||
for node in raw["body"]: |
||||
body_parsed.append(parse(node)) |
||||
|
||||
decorators_parsed: List[ASTNode] = [] |
||||
for node in raw["decorator_list"]: |
||||
decorators_parsed.append(parse(node)) |
||||
|
||||
return FunctionDef( |
||||
name=raw["name"], |
||||
args=parse_arguments(raw["args"]), |
||||
returns=parse(raw["returns"]) if raw["returns"] else None, |
||||
body=body_parsed, |
||||
pos=raw["pos"], |
||||
decorators=decorators_parsed, |
||||
**_extract_decl_props(raw), |
||||
) |
||||
|
||||
|
||||
def parse_assign(raw: Dict) -> Assign: |
||||
return Assign( |
||||
target=parse(raw["target"]), |
||||
value=parse(raw["value"]), |
||||
**_extract_base_props(raw), |
||||
) |
||||
|
||||
|
||||
def parse_attribute(raw: Dict) -> Attribute: |
||||
return Attribute( |
||||
value=parse(raw["value"]), |
||||
attr=raw["attr"], |
||||
**_extract_base_props(raw), |
||||
) |
||||
|
||||
|
||||
def parse_arguments(raw: Dict) -> Arguments: |
||||
return Arguments( |
||||
args=[parse_arg(arg) for arg in raw["args"]], |
||||
default=parse(raw["default"]) if raw["default"] else None, |
||||
defaults=[parse(x) for x in raw["defaults"]], |
||||
**_extract_base_props(raw), |
||||
) |
||||
|
||||
|
||||
def parse_arg(raw: Dict) -> Arg: |
||||
return Arg(arg=raw["arg"], annotation=parse(raw["annotation"]), **_extract_base_props(raw)) |
||||
|
||||
|
||||
def parse_assert(raw: Dict) -> Assert: |
||||
return Assert( |
||||
test=parse(raw["test"]), |
||||
msg=parse(raw["msg"]) if raw["msg"] else None, |
||||
**_extract_base_props(raw), |
||||
) |
||||
|
||||
|
||||
def parse_raise(raw: Dict) -> Raise: |
||||
return Raise(exc=parse(raw["exc"]) if raw["exc"] else None, **_extract_base_props(raw)) |
||||
|
||||
|
||||
def parse_expr(raw: Dict) -> Expr: |
||||
return Expr(value=parse(raw["value"]), **_extract_base_props(raw)) |
||||
|
||||
|
||||
# This is done for convenience so we can call `UnaryOperationType.get_type` during expression parsing. |
||||
unop_ast_type_to_op_symbol = {"Not": "!", "USub": "-"} |
||||
|
||||
|
||||
def parse_unary_op(raw: Dict) -> UnaryOp: |
||||
unop_str = unop_ast_type_to_op_symbol[raw["op"]["ast_type"]] |
||||
return UnaryOp(op=unop_str, operand=parse(raw["operand"]), **_extract_base_props(raw)) |
||||
|
||||
|
||||
# This is done for convenience so we can call `BinaryOperationType.get_type` during expression parsing. |
||||
binop_ast_type_to_op_symbol = { |
||||
"Add": "+", |
||||
"Mult": "*", |
||||
"Sub": "-", |
||||
"Div": "/", |
||||
"Pow": "**", |
||||
"Mod": "%", |
||||
"BitAnd": "&", |
||||
"BitOr": "|", |
||||
"Shr": "<<", |
||||
"Shl": ">>", |
||||
"NotEq": "!=", |
||||
"Eq": "==", |
||||
"LtE": "<=", |
||||
"GtE": ">=", |
||||
"Lt": "<", |
||||
"Gt": ">", |
||||
"In": "In", |
||||
"NotIn": "NotIn", |
||||
} |
||||
|
||||
|
||||
def parse_bin_op(raw: Dict) -> BinOp: |
||||
arith_op_str = binop_ast_type_to_op_symbol[raw["op"]["ast_type"]] |
||||
return BinOp( |
||||
left=parse(raw["left"]), |
||||
op=arith_op_str, |
||||
right=parse(raw["right"]), |
||||
**_extract_base_props(raw), |
||||
) |
||||
|
||||
|
||||
def parse_compare(raw: Dict) -> Compare: |
||||
logical_op_str = binop_ast_type_to_op_symbol[raw["op"]["ast_type"]] |
||||
return Compare( |
||||
left=parse(raw["left"]), |
||||
op=logical_op_str, |
||||
right=parse(raw["right"]), |
||||
**_extract_base_props(raw), |
||||
) |
||||
|
||||
|
||||
def parse_keyword(raw: Dict) -> Keyword: |
||||
return Keyword(arg=raw["arg"], value=parse(raw["value"]), **_extract_base_props(raw)) |
||||
|
||||
|
||||
def parse_log(raw: Dict) -> Log: |
||||
return Log(value=parse(raw["value"]), **_extract_base_props(raw)) |
||||
|
||||
|
||||
def parse_return(raw: Dict) -> Return: |
||||
return Return(value=parse(raw["value"]) if raw["value"] else None, **_extract_base_props(raw)) |
||||
|
||||
|
||||
def parse_dict(raw: Dict) -> ASTNode: |
||||
return VyDict( |
||||
keys=[parse(x) for x in raw["keys"]], |
||||
values=[parse(x) for x in raw["values"]], |
||||
**_extract_base_props(raw), |
||||
) |
||||
|
||||
|
||||
def parse_list(raw: Dict) -> VyList: |
||||
return VyList(elements=[parse(x) for x in raw["elements"]], **_extract_base_props(raw)) |
||||
|
||||
|
||||
def parse_name_constant(raw: Dict) -> NameConstant: |
||||
return NameConstant(value=raw["value"], **_extract_base_props(raw)) |
||||
|
||||
|
||||
def parse_doc_str(raw: Dict) -> str: |
||||
assert isinstance(raw["value"], str) |
||||
return raw["value"] |
||||
|
||||
|
||||
def parse_if(raw: Dict) -> ASTNode: |
||||
return If( |
||||
test=parse(raw["test"]), |
||||
body=[parse(x) for x in raw["body"]], |
||||
orelse=[parse(x) for x in raw["orelse"]], |
||||
**_extract_base_props(raw), |
||||
) |
||||
|
||||
|
||||
def parse_for(raw: Dict) -> For: |
||||
return For( |
||||
target=parse(raw["target"]), |
||||
iter=parse(raw["iter"]), |
||||
body=[parse(x) for x in raw["body"]], |
||||
**_extract_base_props(raw), |
||||
) |
||||
|
||||
|
||||
def parse_break(raw: Dict) -> Break: |
||||
return Break(**_extract_base_props(raw)) |
||||
|
||||
|
||||
def parse_continue(raw: Dict) -> Continue: |
||||
return Continue(**_extract_base_props(raw)) |
||||
|
||||
|
||||
def parse_pass(raw: Dict) -> Pass: |
||||
return Pass( |
||||
**_extract_base_props(raw), |
||||
) |
||||
|
||||
|
||||
def parse_interface_def(raw: Dict) -> InterfaceDef: |
||||
nodes_parsed: List[ASTNode] = [] |
||||
|
||||
for node in raw["body"]: |
||||
nodes_parsed.append(parse(node)) |
||||
return InterfaceDef(name=raw["name"], body=nodes_parsed, **_extract_base_props(raw)) |
||||
|
||||
|
||||
def parse_enum_def(raw: Dict) -> EnumDef: |
||||
nodes_parsed: List[ASTNode] = [] |
||||
|
||||
for node in raw["body"]: |
||||
nodes_parsed.append(parse(node)) |
||||
|
||||
return EnumDef(name=raw["name"], body=nodes_parsed, **_extract_base_props(raw)) |
||||
|
||||
|
||||
aug_assign_ast_type_to_op_symbol = { |
||||
"Add": "+=", |
||||
"Mult": "*=", |
||||
"Sub": "-=", |
||||
"Div": "-=", |
||||
"Pow": "**=", |
||||
"Mod": "%=", |
||||
"BitAnd": "&=", |
||||
"BitOr": "|=", |
||||
"Shr": "<<=", |
||||
"Shl": ">>=", |
||||
} |
||||
|
||||
|
||||
def parse_aug_assign(raw: Dict) -> AugAssign: |
||||
op_str = aug_assign_ast_type_to_op_symbol[raw["op"]["ast_type"]] |
||||
return AugAssign( |
||||
target=parse(raw["target"]), |
||||
op=op_str, |
||||
value=parse(raw["value"]), |
||||
**_extract_base_props(raw), |
||||
) |
||||
|
||||
|
||||
def parse_unsupported(raw: Dict) -> ASTNode: |
||||
raise ParsingError("unsupported Vyper node", raw["ast_type"], raw.keys(), raw) |
||||
|
||||
|
||||
bool_op_ast_type_to_op_symbol = {"And": "&&", "Or": "||"} |
||||
|
||||
|
||||
def parse_bool_op(raw: Dict) -> BoolOp: |
||||
op_str = bool_op_ast_type_to_op_symbol[raw["op"]["ast_type"]] |
||||
return BoolOp(op=op_str, values=[parse(x) for x in raw["values"]], **_extract_base_props(raw)) |
||||
|
||||
|
||||
def parse(raw: Dict) -> ASTNode: |
||||
try: |
||||
return PARSERS.get(raw["ast_type"], parse_unsupported)(raw) |
||||
except ParsingError as e: |
||||
raise e |
||||
except Exception as e: |
||||
raise e |
||||
# raise ParsingError("failed to parse Vyper node", raw["ast_type"], e, raw.keys(), raw) |
||||
|
||||
|
||||
PARSERS: Dict[str, Callable[[Dict], ASTNode]] = { |
||||
"Module": parse_module, |
||||
"ImportFrom": parse_import_from, |
||||
"EventDef": parse_event_def, |
||||
"AnnAssign": parse_ann_assign, |
||||
"Name": parse_name, |
||||
"Call": parse_call, |
||||
"Pass": parse_pass, |
||||
"StructDef": parse_struct_def, |
||||
"VariableDecl": parse_variable_decl, |
||||
"Subscript": parse_subscript, |
||||
"Index": parse_index, |
||||
"Hex": parse_hex, |
||||
"Int": parse_int, |
||||
"Str": parse_str, |
||||
"DocStr": parse_doc_str, |
||||
"Tuple": parse_tuple, |
||||
"FunctionDef": parse_function_def, |
||||
"Assign": parse_assign, |
||||
"Raise": parse_raise, |
||||
"Attribute": parse_attribute, |
||||
"Assert": parse_assert, |
||||
"keyword": parse_keyword, |
||||
"arguments": parse_arguments, |
||||
"arg": parse_arg, |
||||
"UnaryOp": parse_unary_op, |
||||
"BinOp": parse_bin_op, |
||||
"Expr": parse_expr, |
||||
"Log": parse_log, |
||||
"Return": parse_return, |
||||
"If": parse_if, |
||||
"Dict": parse_dict, |
||||
"List": parse_list, |
||||
"Compare": parse_compare, |
||||
"NameConstant": parse_name_constant, |
||||
"For": parse_for, |
||||
"Break": parse_break, |
||||
"Continue": parse_continue, |
||||
"InterfaceDef": parse_interface_def, |
||||
"EnumDef": parse_enum_def, |
||||
"Bytes": parse_bytes, |
||||
"AugAssign": parse_aug_assign, |
||||
"BoolOp": parse_bool_op, |
||||
} |
@ -0,0 +1,262 @@ |
||||
from __future__ import annotations |
||||
from typing import List, Optional, Union |
||||
from dataclasses import dataclass |
||||
|
||||
|
||||
@dataclass |
||||
class ASTNode: |
||||
src: str |
||||
node_id: int |
||||
|
||||
|
||||
@dataclass |
||||
class Definition(ASTNode): |
||||
doc_string: Optional[str] |
||||
|
||||
|
||||
@dataclass |
||||
class Module(Definition): |
||||
body: List[ASTNode] |
||||
name: str |
||||
|
||||
|
||||
@dataclass |
||||
class ImportFrom(ASTNode): |
||||
module: str |
||||
name: str |
||||
alias: Optional[str] |
||||
|
||||
|
||||
@dataclass |
||||
class EventDef(ASTNode): |
||||
name: str |
||||
body: List[AnnAssign] |
||||
|
||||
|
||||
@dataclass |
||||
class AnnAssign(ASTNode): |
||||
target: Name |
||||
annotation: Union[Subscript, Name, Call] |
||||
value: Optional[ASTNode] |
||||
|
||||
|
||||
@dataclass |
||||
class Name(ASTNode): # type or identifier |
||||
id: str |
||||
|
||||
|
||||
@dataclass |
||||
class Call(ASTNode): |
||||
func: ASTNode |
||||
args: List[ASTNode] |
||||
keyword: Optional[ASTNode] |
||||
keywords: List[ASTNode] |
||||
|
||||
|
||||
@dataclass |
||||
class Pass(ASTNode): |
||||
pass |
||||
|
||||
|
||||
@dataclass |
||||
class StructDef(ASTNode): |
||||
name: str |
||||
body: List[AnnAssign] |
||||
|
||||
|
||||
@dataclass |
||||
class VariableDecl(ASTNode): |
||||
annotation: ASTNode |
||||
target: ASTNode |
||||
value: Optional[ASTNode] |
||||
is_constant: bool |
||||
is_immutable: bool |
||||
is_public: bool |
||||
|
||||
|
||||
@dataclass |
||||
class Subscript(ASTNode): |
||||
value: ASTNode |
||||
slice: ASTNode |
||||
|
||||
|
||||
@dataclass |
||||
class Index(ASTNode): |
||||
value: ASTNode |
||||
|
||||
|
||||
@dataclass |
||||
class Bytes(ASTNode): |
||||
value: bytes |
||||
|
||||
|
||||
@dataclass |
||||
class Hex(ASTNode): |
||||
value: str |
||||
|
||||
|
||||
@dataclass |
||||
class Int(ASTNode): |
||||
value: int |
||||
|
||||
|
||||
@dataclass |
||||
class Str(ASTNode): |
||||
value: str |
||||
|
||||
|
||||
@dataclass |
||||
class VyList(ASTNode): |
||||
elements: List[ASTNode] |
||||
|
||||
|
||||
@dataclass |
||||
class VyDict(ASTNode): |
||||
keys: List[ASTNode] |
||||
values: List[ASTNode] |
||||
|
||||
|
||||
@dataclass |
||||
class Tuple(ASTNode): |
||||
elements: List[ASTNode] |
||||
|
||||
|
||||
@dataclass |
||||
class FunctionDef(Definition): |
||||
name: str |
||||
args: Optional[Arguments] |
||||
returns: Optional[List[ASTNode]] |
||||
body: List[ASTNode] |
||||
decorators: Optional[List[ASTNode]] |
||||
pos: Optional[any] # not sure what this is |
||||
|
||||
|
||||
@dataclass |
||||
class Assign(ASTNode): |
||||
target: ASTNode |
||||
value: ASTNode |
||||
|
||||
|
||||
@dataclass |
||||
class Attribute(ASTNode): |
||||
value: ASTNode |
||||
attr: str |
||||
|
||||
|
||||
@dataclass |
||||
class Arguments(ASTNode): |
||||
args: List[Arg] |
||||
default: Optional[ASTNode] |
||||
defaults: List[ASTNode] |
||||
|
||||
|
||||
@dataclass |
||||
class Arg(ASTNode): |
||||
arg: str |
||||
annotation: Optional[ASTNode] |
||||
|
||||
|
||||
@dataclass |
||||
class Assert(ASTNode): |
||||
test: ASTNode |
||||
msg: Optional[Str] |
||||
|
||||
|
||||
@dataclass |
||||
class Raise(ASTNode): |
||||
exc: ASTNode |
||||
|
||||
|
||||
@dataclass |
||||
class Expr(ASTNode): |
||||
value: ASTNode |
||||
|
||||
|
||||
@dataclass |
||||
class UnaryOp(ASTNode): |
||||
op: ASTNode |
||||
operand: ASTNode |
||||
|
||||
|
||||
@dataclass |
||||
class BinOp(ASTNode): |
||||
left: ASTNode |
||||
op: str |
||||
right: ASTNode |
||||
|
||||
|
||||
@dataclass |
||||
class Keyword(ASTNode): |
||||
arg: str |
||||
value: ASTNode |
||||
|
||||
|
||||
@dataclass |
||||
class Log(ASTNode): |
||||
value: ASTNode |
||||
|
||||
|
||||
@dataclass |
||||
class Return(ASTNode): |
||||
value: Optional[ASTNode] |
||||
|
||||
|
||||
@dataclass |
||||
class If(ASTNode): |
||||
test: ASTNode |
||||
body: List[ASTNode] |
||||
orelse: List[ASTNode] |
||||
|
||||
|
||||
@dataclass |
||||
class Compare(ASTNode): |
||||
left: ASTNode |
||||
op: ASTNode |
||||
right: ASTNode |
||||
|
||||
|
||||
@dataclass |
||||
class NameConstant(ASTNode): |
||||
value: bool |
||||
|
||||
|
||||
@dataclass |
||||
class For(ASTNode): |
||||
target: ASTNode |
||||
iter: ASTNode |
||||
body: List[ASTNode] |
||||
|
||||
|
||||
@dataclass |
||||
class Continue(ASTNode): |
||||
pass |
||||
|
||||
|
||||
@dataclass |
||||
class Break(ASTNode): |
||||
pass |
||||
|
||||
|
||||
@dataclass |
||||
class InterfaceDef(ASTNode): |
||||
name: str |
||||
body: List[ASTNode] |
||||
|
||||
|
||||
@dataclass |
||||
class EnumDef(ASTNode): |
||||
name: str |
||||
body: List[ASTNode] |
||||
|
||||
|
||||
@dataclass |
||||
class AugAssign(ASTNode): |
||||
target: ASTNode |
||||
op: ASTNode |
||||
value: ASTNode |
||||
|
||||
|
||||
@dataclass |
||||
class BoolOp(ASTNode): |
||||
op: ASTNode |
||||
values: List[ASTNode] |
@ -0,0 +1,66 @@ |
||||
from typing import Optional, Dict |
||||
|
||||
from slither.core.cfg.node import Node |
||||
from slither.core.cfg.node import NodeType |
||||
from slither.core.expressions.assignment_operation import ( |
||||
AssignmentOperation, |
||||
AssignmentOperationType, |
||||
) |
||||
from slither.core.expressions.identifier import Identifier |
||||
from slither.vyper_parsing.expressions.expression_parsing import parse_expression |
||||
from slither.visitors.expression.find_calls import FindCalls |
||||
from slither.visitors.expression.read_var import ReadVar |
||||
from slither.visitors.expression.write_var import WriteVar |
||||
|
||||
|
||||
class NodeVyper: |
||||
def __init__(self, node: Node) -> None: |
||||
self._unparsed_expression: Optional[Dict] = None |
||||
self._node = node |
||||
|
||||
@property |
||||
def underlying_node(self) -> Node: |
||||
return self._node |
||||
|
||||
def add_unparsed_expression(self, expression: Dict) -> None: |
||||
assert self._unparsed_expression is None |
||||
self._unparsed_expression = expression |
||||
|
||||
def analyze_expressions(self, caller_context) -> None: |
||||
if self._node.type == NodeType.VARIABLE and not self._node.expression: |
||||
self._node.add_expression(self._node.variable_declaration.expression) |
||||
if self._unparsed_expression: |
||||
expression = parse_expression(self._unparsed_expression, caller_context) |
||||
self._node.add_expression(expression) |
||||
self._unparsed_expression = None |
||||
|
||||
if self._node.expression: |
||||
|
||||
if self._node.type == NodeType.VARIABLE: |
||||
# Update the expression to be an assignement to the variable |
||||
_expression = AssignmentOperation( |
||||
Identifier(self._node.variable_declaration), |
||||
self._node.expression, |
||||
AssignmentOperationType.ASSIGN, |
||||
self._node.variable_declaration.type, |
||||
) |
||||
_expression.set_offset( |
||||
self._node.expression.source_mapping, self._node.compilation_unit |
||||
) |
||||
self._node.add_expression(_expression, bypass_verif_empty=True) |
||||
|
||||
expression = self._node.expression |
||||
read_var = ReadVar(expression) |
||||
self._node.variables_read_as_expression = read_var.result() |
||||
|
||||
write_var = WriteVar(expression) |
||||
self._node.variables_written_as_expression = write_var.result() |
||||
|
||||
find_call = FindCalls(expression) |
||||
self._node.calls_as_expression = find_call.result() |
||||
self._node.external_calls_as_expressions = [ |
||||
c for c in self._node.calls_as_expression if not isinstance(c.called, Identifier) |
||||
] |
||||
self._node.internal_calls_as_expressions = [ |
||||
c for c in self._node.calls_as_expression if isinstance(c.called, Identifier) |
||||
] |
@ -0,0 +1,524 @@ |
||||
from pathlib import Path |
||||
from typing import List, TYPE_CHECKING |
||||
from slither.vyper_parsing.ast.types import ( |
||||
Module, |
||||
FunctionDef, |
||||
EventDef, |
||||
EnumDef, |
||||
StructDef, |
||||
VariableDecl, |
||||
ImportFrom, |
||||
InterfaceDef, |
||||
AnnAssign, |
||||
Expr, |
||||
Name, |
||||
Arguments, |
||||
Index, |
||||
Subscript, |
||||
Int, |
||||
Arg, |
||||
) |
||||
|
||||
from slither.vyper_parsing.declarations.event import EventVyper |
||||
from slither.vyper_parsing.declarations.struct import StructVyper |
||||
from slither.vyper_parsing.variables.state_variable import StateVariableVyper |
||||
from slither.vyper_parsing.declarations.function import FunctionVyper |
||||
from slither.core.declarations.function_contract import FunctionContract |
||||
from slither.core.declarations import Contract, StructureContract, EnumContract, Event |
||||
|
||||
from slither.core.variables.state_variable import StateVariable |
||||
|
||||
if TYPE_CHECKING: |
||||
from slither.vyper_parsing.vyper_compilation_unit import VyperCompilationUnit |
||||
|
||||
|
||||
class ContractVyper: # pylint: disable=too-many-instance-attributes |
||||
def __init__( |
||||
self, slither_parser: "VyperCompilationUnit", contract: Contract, module: Module |
||||
) -> None: |
||||
|
||||
self._contract: Contract = contract |
||||
self._slither_parser: "VyperCompilationUnit" = slither_parser |
||||
self._data = module |
||||
# Vyper models only have one contract (aside from interfaces) and the name is the file path |
||||
# We use the stem to make it a more user friendly name that is easy to query via canonical name |
||||
self._contract.name = Path(module.name).stem |
||||
self._contract.id = module.node_id |
||||
self._is_analyzed: bool = False |
||||
|
||||
self._enumsNotParsed: List[EnumDef] = [] |
||||
self._structuresNotParsed: List[StructDef] = [] |
||||
self._variablesNotParsed: List[VariableDecl] = [] |
||||
self._eventsNotParsed: List[EventDef] = [] |
||||
self._functionsNotParsed: List[FunctionDef] = [] |
||||
|
||||
self._structures_parser: List[StructVyper] = [] |
||||
self._variables_parser: List[StateVariableVyper] = [] |
||||
self._events_parser: List[EventVyper] = [] |
||||
self._functions_parser: List[FunctionVyper] = [] |
||||
|
||||
self._parse_contract_items() |
||||
|
||||
@property |
||||
def is_analyzed(self) -> bool: |
||||
return self._is_analyzed |
||||
|
||||
def set_is_analyzed(self, is_analyzed: bool) -> None: |
||||
self._is_analyzed = is_analyzed |
||||
|
||||
@property |
||||
def underlying_contract(self) -> Contract: |
||||
return self._contract |
||||
|
||||
def _parse_contract_items(self) -> None: |
||||
for node in self._data.body: |
||||
if isinstance(node, FunctionDef): |
||||
self._functionsNotParsed.append(node) |
||||
elif isinstance(node, EventDef): |
||||
self._eventsNotParsed.append(node) |
||||
elif isinstance(node, VariableDecl): |
||||
self._variablesNotParsed.append(node) |
||||
elif isinstance(node, EnumDef): |
||||
self._enumsNotParsed.append(node) |
||||
elif isinstance(node, StructDef): |
||||
self._structuresNotParsed.append(node) |
||||
elif isinstance(node, ImportFrom): |
||||
# TOOD aliases |
||||
# We create an `InterfaceDef` sense the compilatuion unit does not contain the actual interface |
||||
# https://github.com/vyperlang/vyper/tree/master/vyper/builtins/interfaces |
||||
if node.module == "vyper.interfaces": |
||||
interfaces = { |
||||
"ERC20Detailed": InterfaceDef( |
||||
src="-1:-1:-1", |
||||
node_id=-1, |
||||
name="ERC20Detailed", |
||||
body=[ |
||||
FunctionDef( |
||||
src="-1:-1:-1", |
||||
node_id=-1, |
||||
doc_string=None, |
||||
name="name", |
||||
args=Arguments( |
||||
src="-1:-1:-1", |
||||
node_id=-1, |
||||
args=[], |
||||
default=None, |
||||
defaults=[], |
||||
), |
||||
returns=Subscript( |
||||
src="-1:-1:-1", |
||||
node_id=-1, |
||||
value=Name(src="-1:-1:-1", node_id=-1, id="String"), |
||||
slice=Index( |
||||
src="-1:-1:-1", |
||||
node_id=-1, |
||||
value=Int(src="-1:-1:-1", node_id=-1, value=1), |
||||
), |
||||
), |
||||
body=[ |
||||
Expr( |
||||
src="-1:-1:-1", |
||||
node_id=-1, |
||||
value=Name(src="-1:-1:-1", node_id=-1, id="view"), |
||||
) |
||||
], |
||||
decorators=[], |
||||
pos=None, |
||||
), |
||||
FunctionDef( |
||||
src="-1:-1:-1", |
||||
node_id=-1, |
||||
doc_string=None, |
||||
name="symbol", |
||||
args=Arguments( |
||||
src="-1:-1:-1", |
||||
node_id=-1, |
||||
args=[], |
||||
default=None, |
||||
defaults=[], |
||||
), |
||||
returns=Subscript( |
||||
src="-1:-1:-1", |
||||
node_id=-1, |
||||
value=Name(src="-1:-1:-1", node_id=-1, id="String"), |
||||
slice=Index( |
||||
src="-1:-1:-1", |
||||
node_id=-1, |
||||
value=Int(src="-1:-1:-1", node_id=-1, value=1), |
||||
), |
||||
), |
||||
body=[ |
||||
Expr( |
||||
src="-1:-1:-1", |
||||
node_id=-1, |
||||
value=Name(src="-1:-1:-1", node_id=-1, id="view"), |
||||
) |
||||
], |
||||
decorators=[], |
||||
pos=None, |
||||
), |
||||
FunctionDef( |
||||
src="-1:-1:-1", |
||||
node_id=-1, |
||||
doc_string=None, |
||||
name="decimals", |
||||
args=Arguments( |
||||
src="-1:-1:-1", |
||||
node_id=-1, |
||||
args=[], |
||||
default=None, |
||||
defaults=[], |
||||
), |
||||
returns=Name(src="-1:-1:-1", node_id=-1, id="uint8"), |
||||
body=[ |
||||
Expr( |
||||
src="-1:-1:-1", |
||||
node_id=-1, |
||||
value=Name(src="-1:-1:-1", node_id=-1, id="view"), |
||||
) |
||||
], |
||||
decorators=[], |
||||
pos=None, |
||||
), |
||||
], |
||||
), |
||||
"ERC20": InterfaceDef( |
||||
src="-1:-1:-1", |
||||
node_id=1, |
||||
name="ERC20", |
||||
body=[ |
||||
FunctionDef( |
||||
src="-1:-1:-1", |
||||
node_id=2, |
||||
doc_string=None, |
||||
name="totalSupply", |
||||
args=Arguments( |
||||
src="-1:-1:-1", |
||||
node_id=3, |
||||
args=[], |
||||
default=None, |
||||
defaults=[], |
||||
), |
||||
returns=Name(src="-1:-1:-1", node_id=7, id="uint256"), |
||||
body=[ |
||||
Expr( |
||||
src="-1:-1:-1", |
||||
node_id=4, |
||||
value=Name(src="-1:-1:-1", node_id=5, id="view"), |
||||
) |
||||
], |
||||
decorators=[], |
||||
pos=None, |
||||
), |
||||
FunctionDef( |
||||
src="-1:-1:-1", |
||||
node_id=9, |
||||
doc_string=None, |
||||
name="balanceOf", |
||||
args=Arguments( |
||||
src="-1:-1:-1", |
||||
node_id=10, |
||||
args=[ |
||||
Arg( |
||||
src="-1:-1:-1", |
||||
node_id=11, |
||||
arg="_owner", |
||||
annotation=Name( |
||||
src="-1:-1:-1", node_id=12, id="address" |
||||
), |
||||
) |
||||
], |
||||
default=None, |
||||
defaults=[], |
||||
), |
||||
returns=Name(src="-1:-1:-1", node_id=17, id="uint256"), |
||||
body=[ |
||||
Expr( |
||||
src="-1:-1:-1", |
||||
node_id=14, |
||||
value=Name(src="-1:-1:-1", node_id=15, id="view"), |
||||
) |
||||
], |
||||
decorators=[], |
||||
pos=None, |
||||
), |
||||
FunctionDef( |
||||
src="-1:-1:-1", |
||||
node_id=19, |
||||
doc_string=None, |
||||
name="allowance", |
||||
args=Arguments( |
||||
src="-1:-1:-1", |
||||
node_id=20, |
||||
args=[ |
||||
Arg( |
||||
src="-1:-1:-1", |
||||
node_id=21, |
||||
arg="_owner", |
||||
annotation=Name( |
||||
src="-1:-1:-1", node_id=22, id="address" |
||||
), |
||||
), |
||||
Arg( |
||||
src="-1:-1:-1", |
||||
node_id=24, |
||||
arg="_spender", |
||||
annotation=Name( |
||||
src="-1:-1:-1", node_id=25, id="address" |
||||
), |
||||
), |
||||
], |
||||
default=None, |
||||
defaults=[], |
||||
), |
||||
returns=Name(src="-1:-1:-1", node_id=30, id="uint256"), |
||||
body=[ |
||||
Expr( |
||||
src="-1:-1:-1", |
||||
node_id=27, |
||||
value=Name(src="-1:-1:-1", node_id=28, id="view"), |
||||
) |
||||
], |
||||
decorators=[], |
||||
pos=None, |
||||
), |
||||
FunctionDef( |
||||
src="-1:-1:-1", |
||||
node_id=32, |
||||
doc_string=None, |
||||
name="transfer", |
||||
args=Arguments( |
||||
src="-1:-1:-1", |
||||
node_id=33, |
||||
args=[ |
||||
Arg( |
||||
src="-1:-1:-1", |
||||
node_id=34, |
||||
arg="_to", |
||||
annotation=Name( |
||||
src="-1:-1:-1", node_id=35, id="address" |
||||
), |
||||
), |
||||
Arg( |
||||
src="-1:-1:-1", |
||||
node_id=37, |
||||
arg="_value", |
||||
annotation=Name( |
||||
src="-1:-1:-1", node_id=38, id="uint256" |
||||
), |
||||
), |
||||
], |
||||
default=None, |
||||
defaults=[], |
||||
), |
||||
returns=Name(src="-1:-1:-1", node_id=43, id="bool"), |
||||
body=[ |
||||
Expr( |
||||
src="-1:-1:-1", |
||||
node_id=40, |
||||
value=Name(src="-1:-1:-1", node_id=41, id="nonpayable"), |
||||
) |
||||
], |
||||
decorators=[], |
||||
pos=None, |
||||
), |
||||
FunctionDef( |
||||
src="-1:-1:-1", |
||||
node_id=45, |
||||
doc_string=None, |
||||
name="transferFrom", |
||||
args=Arguments( |
||||
src="-1:-1:-1", |
||||
node_id=46, |
||||
args=[ |
||||
Arg( |
||||
src="-1:-1:-1", |
||||
node_id=47, |
||||
arg="_from", |
||||
annotation=Name( |
||||
src="-1:-1:-1", node_id=48, id="address" |
||||
), |
||||
), |
||||
Arg( |
||||
src="-1:-1:-1", |
||||
node_id=50, |
||||
arg="_to", |
||||
annotation=Name( |
||||
src="-1:-1:-1", node_id=51, id="address" |
||||
), |
||||
), |
||||
Arg( |
||||
src="-1:-1:-1", |
||||
node_id=53, |
||||
arg="_value", |
||||
annotation=Name( |
||||
src="-1:-1:-1", node_id=54, id="uint256" |
||||
), |
||||
), |
||||
], |
||||
default=None, |
||||
defaults=[], |
||||
), |
||||
returns=Name(src="-1:-1:-1", node_id=59, id="bool"), |
||||
body=[ |
||||
Expr( |
||||
src="-1:-1:-1", |
||||
node_id=56, |
||||
value=Name(src="-1:-1:-1", node_id=57, id="nonpayable"), |
||||
) |
||||
], |
||||
decorators=[], |
||||
pos=None, |
||||
), |
||||
FunctionDef( |
||||
src="-1:-1:-1", |
||||
node_id=61, |
||||
doc_string=None, |
||||
name="approve", |
||||
args=Arguments( |
||||
src="-1:-1:-1", |
||||
node_id=62, |
||||
args=[ |
||||
Arg( |
||||
src="-1:-1:-1", |
||||
node_id=63, |
||||
arg="_spender", |
||||
annotation=Name( |
||||
src="-1:-1:-1", node_id=64, id="address" |
||||
), |
||||
), |
||||
Arg( |
||||
src="-1:-1:-1", |
||||
node_id=66, |
||||
arg="_value", |
||||
annotation=Name( |
||||
src="-1:-1:-1", node_id=67, id="uint256" |
||||
), |
||||
), |
||||
], |
||||
default=None, |
||||
defaults=[], |
||||
), |
||||
returns=Name(src="-1:-1:-1", node_id=72, id="bool"), |
||||
body=[ |
||||
Expr( |
||||
src="-1:-1:-1", |
||||
node_id=69, |
||||
value=Name(src="-1:-1:-1", node_id=70, id="nonpayable"), |
||||
) |
||||
], |
||||
decorators=[], |
||||
pos=None, |
||||
), |
||||
], |
||||
), |
||||
"ERC165": [], |
||||
"ERC721": [], |
||||
"ERC4626": [], |
||||
} |
||||
self._data.body.append(interfaces[node.name]) |
||||
|
||||
elif isinstance(node, InterfaceDef): |
||||
# This needs to be done lazily as interfaces can refer to constant state variables |
||||
contract = Contract(self._contract.compilation_unit, self._contract.file_scope) |
||||
contract.set_offset(node.src, self._contract.compilation_unit) |
||||
contract.is_interface = True |
||||
|
||||
contract_parser = ContractVyper(self._slither_parser, contract, node) |
||||
self._contract.file_scope.contracts[contract.name] = contract |
||||
# pylint: disable=protected-access |
||||
self._slither_parser._underlying_contract_to_parser[contract] = contract_parser |
||||
|
||||
elif isinstance(node, AnnAssign): # implements: ERC20 |
||||
pass # TODO |
||||
else: |
||||
raise ValueError("Unknown contract node: ", node) |
||||
|
||||
def parse_enums(self) -> None: |
||||
for enum in self._enumsNotParsed: |
||||
name = enum.name |
||||
canonicalName = self._contract.name + "." + enum.name |
||||
values = [x.value.id for x in enum.body] |
||||
new_enum = EnumContract(name, canonicalName, values) |
||||
new_enum.set_contract(self._contract) |
||||
new_enum.set_offset(enum.src, self._contract.compilation_unit) |
||||
self._contract.enums_as_dict[name] = new_enum # TODO solidity using canonicalName |
||||
self._enumsNotParsed = [] |
||||
|
||||
def parse_structs(self) -> None: |
||||
for struct in self._structuresNotParsed: |
||||
st = StructureContract(self._contract.compilation_unit) |
||||
st.set_contract(self._contract) |
||||
st.set_offset(struct.src, self._contract.compilation_unit) |
||||
|
||||
st_parser = StructVyper(st, struct) |
||||
self._contract.structures_as_dict[st.name] = st |
||||
self._structures_parser.append(st_parser) |
||||
# Interfaces can refer to struct defs |
||||
self._contract.file_scope.structures[st.name] = st |
||||
|
||||
self._structuresNotParsed = [] |
||||
|
||||
def parse_state_variables(self) -> None: |
||||
for varNotParsed in self._variablesNotParsed: |
||||
var = StateVariable() |
||||
var.set_contract(self._contract) |
||||
var.set_offset(varNotParsed.src, self._contract.compilation_unit) |
||||
|
||||
var_parser = StateVariableVyper(var, varNotParsed) |
||||
self._variables_parser.append(var_parser) |
||||
|
||||
assert var.name |
||||
self._contract.variables_as_dict[var.name] = var |
||||
self._contract.add_variables_ordered([var]) |
||||
# Interfaces can refer to constants |
||||
self._contract.file_scope.variables[var.name] = var |
||||
|
||||
self._variablesNotParsed = [] |
||||
|
||||
def parse_events(self) -> None: |
||||
for event_to_parse in self._eventsNotParsed: |
||||
event = Event() |
||||
event.set_contract(self._contract) |
||||
event.set_offset(event_to_parse.src, self._contract.compilation_unit) |
||||
|
||||
event_parser = EventVyper(event, event_to_parse) |
||||
self._events_parser.append(event_parser) |
||||
self._contract.events_as_dict[event.full_name] = event |
||||
|
||||
def parse_functions(self) -> None: |
||||
|
||||
for function in self._functionsNotParsed: |
||||
func = FunctionContract(self._contract.compilation_unit) |
||||
func.set_offset(function.src, self._contract.compilation_unit) |
||||
func.set_contract(self._contract) |
||||
func.set_contract_declarer(self._contract) |
||||
|
||||
func_parser = FunctionVyper(func, function, self) |
||||
self._contract.add_function(func) |
||||
self._contract.compilation_unit.add_function(func) |
||||
self._functions_parser.append(func_parser) |
||||
|
||||
self._functionsNotParsed = [] |
||||
|
||||
def analyze_state_variables(self): |
||||
# Struct defs can refer to constant state variables |
||||
for var_parser in self._variables_parser: |
||||
var_parser.analyze(self._contract) |
||||
|
||||
def analyze(self) -> None: |
||||
|
||||
for struct_parser in self._structures_parser: |
||||
struct_parser.analyze(self._contract) |
||||
|
||||
for event_parser in self._events_parser: |
||||
event_parser.analyze(self._contract) |
||||
|
||||
for function in self._functions_parser: |
||||
function.analyze_params() |
||||
|
||||
for function in self._functions_parser: |
||||
function.analyze_content() |
||||
|
||||
def __hash__(self) -> int: |
||||
return self._contract.id |
@ -0,0 +1,39 @@ |
||||
""" |
||||
Event module |
||||
""" |
||||
|
||||
from slither.core.variables.event_variable import EventVariable |
||||
from slither.vyper_parsing.variables.event_variable import EventVariableVyper |
||||
from slither.core.declarations.event import Event |
||||
from slither.vyper_parsing.ast.types import AnnAssign, Pass |
||||
|
||||
|
||||
from slither.vyper_parsing.ast.types import EventDef |
||||
|
||||
|
||||
class EventVyper: # pylint: disable=too-few-public-methods |
||||
""" |
||||
Event class |
||||
""" |
||||
|
||||
def __init__(self, event: Event, event_def: EventDef) -> None: |
||||
|
||||
self._event = event |
||||
self._event.name = event_def.name |
||||
self._elemsNotParsed = event_def.body |
||||
|
||||
def analyze(self, contract) -> None: |
||||
for elem_to_parse in self._elemsNotParsed: |
||||
if not isinstance(elem_to_parse, AnnAssign): |
||||
assert isinstance(elem_to_parse, Pass) |
||||
continue |
||||
|
||||
elem = EventVariable() |
||||
|
||||
elem.set_offset(elem_to_parse.src, self._event.contract.compilation_unit) |
||||
event_parser = EventVariableVyper(elem, elem_to_parse) |
||||
event_parser.analyze(contract) |
||||
|
||||
self._event.elems.append(elem) |
||||
|
||||
self._elemsNotParsed = [] |
@ -0,0 +1,563 @@ |
||||
from typing import Dict, Union, List, TYPE_CHECKING |
||||
|
||||
from slither.core.cfg.node import NodeType, link_nodes, Node |
||||
from slither.core.cfg.scope import Scope |
||||
from slither.core.declarations.function import ( |
||||
Function, |
||||
FunctionType, |
||||
) |
||||
from slither.core.declarations.function import ModifierStatements |
||||
from slither.core.declarations.modifier import Modifier |
||||
from slither.core.source_mapping.source_mapping import Source |
||||
from slither.core.variables.local_variable import LocalVariable |
||||
from slither.vyper_parsing.cfg.node import NodeVyper |
||||
from slither.solc_parsing.exceptions import ParsingError |
||||
from slither.vyper_parsing.variables.local_variable import LocalVariableVyper |
||||
from slither.vyper_parsing.ast.types import ( |
||||
Int, |
||||
Call, |
||||
Attribute, |
||||
Name, |
||||
Tuple as TupleVyper, |
||||
ASTNode, |
||||
AnnAssign, |
||||
FunctionDef, |
||||
Return, |
||||
Assert, |
||||
Compare, |
||||
Log, |
||||
Subscript, |
||||
If, |
||||
Pass, |
||||
Assign, |
||||
AugAssign, |
||||
Raise, |
||||
Expr, |
||||
For, |
||||
Index, |
||||
Arg, |
||||
Arguments, |
||||
Continue, |
||||
Break, |
||||
) |
||||
|
||||
if TYPE_CHECKING: |
||||
from slither.core.compilation_unit import SlitherCompilationUnit |
||||
from slither.vyper_parsing.declarations.contract import ContractVyper |
||||
|
||||
|
||||
def link_underlying_nodes(node1: NodeVyper, node2: NodeVyper): |
||||
link_nodes(node1.underlying_node, node2.underlying_node) |
||||
|
||||
|
||||
class FunctionVyper: # pylint: disable=too-many-instance-attributes |
||||
def __init__( |
||||
self, |
||||
function: Function, |
||||
function_data: FunctionDef, |
||||
contract_parser: "ContractVyper", |
||||
) -> None: |
||||
|
||||
self._function = function |
||||
self._function.name = function_data.name |
||||
self._function.id = function_data.node_id |
||||
self._functionNotParsed = function_data |
||||
self._decoratorNotParsed = None |
||||
self._local_variables_parser: List[LocalVariableVyper] = [] |
||||
self._variables_renamed = [] |
||||
self._contract_parser = contract_parser |
||||
self._node_to_NodeVyper: Dict[Node, NodeVyper] = {} |
||||
|
||||
for decorator in function_data.decorators: |
||||
if isinstance(decorator, Call): |
||||
# TODO handle multiple |
||||
self._decoratorNotParsed = decorator |
||||
elif isinstance(decorator, Name): |
||||
if decorator.id in ["external", "public", "internal"]: |
||||
self._function.visibility = decorator.id |
||||
elif decorator.id == "view": |
||||
self._function.view = True |
||||
elif decorator.id == "pure": |
||||
self._function.pure = True |
||||
elif decorator.id == "payable": |
||||
self._function.payable = True |
||||
elif decorator.id == "nonpayable": |
||||
self._function.payable = False |
||||
else: |
||||
raise ValueError(f"Unknown decorator {decorator.id}") |
||||
|
||||
# Interfaces do not have decorators and are external |
||||
if self._function._visibility is None: |
||||
self._function.visibility = "external" |
||||
|
||||
self._params_was_analyzed = False |
||||
self._content_was_analyzed = False |
||||
self._counter_scope_local_variables = 0 |
||||
|
||||
if function_data.doc_string is not None: |
||||
function.has_documentation = True |
||||
|
||||
self._analyze_function_type() |
||||
|
||||
@property |
||||
def underlying_function(self) -> Function: |
||||
return self._function |
||||
|
||||
@property |
||||
def compilation_unit(self) -> "SlitherCompilationUnit": |
||||
return self._function.compilation_unit |
||||
|
||||
################################################################################### |
||||
################################################################################### |
||||
# region Variables |
||||
################################################################################### |
||||
################################################################################### |
||||
|
||||
@property |
||||
def variables_renamed( |
||||
self, |
||||
) -> Dict[int, LocalVariableVyper]: |
||||
return self._variables_renamed |
||||
|
||||
def _add_local_variable(self, local_var_parser: LocalVariableVyper) -> None: |
||||
# Ensure variables name are unique for SSA conversion |
||||
# This should not apply to actual Vyper variables currently |
||||
# but is necessary if we have nested loops where we've created artificial variables e.g. counter_var |
||||
if local_var_parser.underlying_variable.name: |
||||
known_variables = [v.name for v in self._function.variables] |
||||
while local_var_parser.underlying_variable.name in known_variables: |
||||
local_var_parser.underlying_variable.name += ( |
||||
f"_scope_{self._counter_scope_local_variables}" |
||||
) |
||||
self._counter_scope_local_variables += 1 |
||||
known_variables = [v.name for v in self._function.variables] |
||||
# TODO no reference ID |
||||
# if local_var_parser.reference_id is not None: |
||||
# self._variables_renamed[local_var_parser.reference_id] = local_var_parser |
||||
self._function.variables_as_dict[ |
||||
local_var_parser.underlying_variable.name |
||||
] = local_var_parser.underlying_variable |
||||
self._local_variables_parser.append(local_var_parser) |
||||
|
||||
# endregion |
||||
################################################################################### |
||||
################################################################################### |
||||
# region Analyses |
||||
################################################################################### |
||||
################################################################################### |
||||
|
||||
@property |
||||
def function_not_parsed(self) -> Dict: |
||||
return self._functionNotParsed |
||||
|
||||
def _analyze_function_type(self) -> None: |
||||
if self._function.name == "__init__": |
||||
self._function.function_type = FunctionType.CONSTRUCTOR |
||||
elif self._function.name == "__default__": |
||||
self._function.function_type = FunctionType.FALLBACK |
||||
else: |
||||
self._function.function_type = FunctionType.NORMAL |
||||
|
||||
def analyze_params(self) -> None: |
||||
if self._params_was_analyzed: |
||||
return |
||||
|
||||
self._params_was_analyzed = True |
||||
|
||||
params = self._functionNotParsed.args |
||||
returns = self._functionNotParsed.returns |
||||
|
||||
if params: |
||||
self._parse_params(params) |
||||
if returns: |
||||
self._parse_returns(returns) |
||||
|
||||
def analyze_content(self) -> None: |
||||
if self._content_was_analyzed: |
||||
return |
||||
|
||||
self._content_was_analyzed = True |
||||
|
||||
body = self._functionNotParsed.body |
||||
|
||||
if body and not isinstance(body[0], Pass): |
||||
self._function.is_implemented = True |
||||
self._function.is_empty = False |
||||
self._parse_cfg(body) |
||||
else: |
||||
self._function.is_implemented = False |
||||
self._function.is_empty = True |
||||
|
||||
for local_var_parser in self._local_variables_parser: |
||||
local_var_parser.analyze(self._function) |
||||
|
||||
for node_parser in self._node_to_NodeVyper.values(): |
||||
node_parser.analyze_expressions(self._function) |
||||
|
||||
self._analyze_decorator() |
||||
|
||||
def _analyze_decorator(self) -> None: |
||||
if not self._decoratorNotParsed: |
||||
return |
||||
|
||||
decorator = self._decoratorNotParsed |
||||
if decorator.args: |
||||
name = f"{decorator.func.id}({decorator.args[0].value})" |
||||
else: |
||||
name = decorator.func.id |
||||
|
||||
contract = self._contract_parser.underlying_contract |
||||
compilation_unit = self._contract_parser.underlying_contract.compilation_unit |
||||
modifier = Modifier(compilation_unit) |
||||
modifier.name = name |
||||
modifier.set_offset(decorator.src, compilation_unit) |
||||
modifier.set_contract(contract) |
||||
modifier.set_contract_declarer(contract) |
||||
latest_entry_point = self._function.entry_point |
||||
self._function.add_modifier( |
||||
ModifierStatements( |
||||
modifier=modifier, |
||||
entry_point=latest_entry_point, |
||||
nodes=[latest_entry_point], |
||||
) |
||||
) |
||||
|
||||
# endregion |
||||
################################################################################### |
||||
################################################################################### |
||||
# region Nodes |
||||
################################################################################### |
||||
################################################################################### |
||||
|
||||
def _new_node( |
||||
self, node_type: NodeType, src: Union[str, Source], scope: Union[Scope, "Function"] |
||||
) -> NodeVyper: |
||||
node = self._function.new_node(node_type, src, scope) |
||||
node_parser = NodeVyper(node) |
||||
self._node_to_NodeVyper[node] = node_parser |
||||
return node_parser |
||||
|
||||
# endregion |
||||
################################################################################### |
||||
################################################################################### |
||||
# region Parsing function |
||||
################################################################################### |
||||
################################################################################### |
||||
|
||||
# pylint: disable=too-many-branches,too-many-statements,protected-access,too-many-locals |
||||
def _parse_cfg(self, cfg: List[ASTNode]) -> None: |
||||
|
||||
entry_node = self._new_node(NodeType.ENTRYPOINT, "-1:-1:-1", self.underlying_function) |
||||
self._function.entry_point = entry_node.underlying_node |
||||
scope = Scope(True, False, self.underlying_function) |
||||
|
||||
def parse_statement( |
||||
curr_node: NodeVyper, |
||||
expr: ASTNode, |
||||
continue_destination=None, |
||||
break_destination=None, |
||||
) -> NodeVyper: |
||||
if isinstance(expr, AnnAssign): |
||||
local_var = LocalVariable() |
||||
local_var.set_function(self._function) |
||||
local_var.set_offset(expr.src, self._function.compilation_unit) |
||||
|
||||
local_var_parser = LocalVariableVyper(local_var, expr) |
||||
self._add_local_variable(local_var_parser) |
||||
|
||||
new_node = self._new_node(NodeType.VARIABLE, expr.src, scope) |
||||
if expr.value is not None: |
||||
local_var.initialized = True |
||||
new_node.add_unparsed_expression(expr.value) |
||||
new_node.underlying_node.add_variable_declaration(local_var) |
||||
link_underlying_nodes(curr_node, new_node) |
||||
|
||||
curr_node = new_node |
||||
|
||||
elif isinstance(expr, (AugAssign, Assign)): |
||||
new_node = self._new_node(NodeType.EXPRESSION, expr.src, scope) |
||||
new_node.add_unparsed_expression(expr) |
||||
link_underlying_nodes(curr_node, new_node) |
||||
|
||||
curr_node = new_node |
||||
|
||||
elif isinstance(expr, Expr): |
||||
# TODO This is a workaround to handle Vyper putting payable/view in the function body... https://github.com/vyperlang/vyper/issues/3578 |
||||
if not isinstance(expr.value, Name): |
||||
new_node = self._new_node(NodeType.EXPRESSION, expr.src, scope) |
||||
new_node.add_unparsed_expression(expr.value) |
||||
link_underlying_nodes(curr_node, new_node) |
||||
|
||||
curr_node = new_node |
||||
|
||||
elif isinstance(expr, For): |
||||
|
||||
node_startLoop = self._new_node(NodeType.STARTLOOP, expr.src, scope) |
||||
node_endLoop = self._new_node(NodeType.ENDLOOP, expr.src, scope) |
||||
|
||||
link_underlying_nodes(curr_node, node_startLoop) |
||||
|
||||
local_var = LocalVariable() |
||||
local_var.set_function(self._function) |
||||
local_var.set_offset(expr.src, self._function.compilation_unit) |
||||
|
||||
counter_var = AnnAssign( |
||||
expr.target.src, |
||||
expr.target.node_id, |
||||
target=Name("-1:-1:-1", -1, "counter_var"), |
||||
annotation=Name("-1:-1:-1", -1, "uint256"), |
||||
value=Int("-1:-1:-1", -1, 0), |
||||
) |
||||
local_var_parser = LocalVariableVyper(local_var, counter_var) |
||||
self._add_local_variable(local_var_parser) |
||||
counter_node = self._new_node(NodeType.VARIABLE, expr.src, scope) |
||||
local_var.initialized = True |
||||
counter_node.add_unparsed_expression(counter_var.value) |
||||
counter_node.underlying_node.add_variable_declaration(local_var) |
||||
|
||||
link_underlying_nodes(node_startLoop, counter_node) |
||||
|
||||
node_condition = None |
||||
if isinstance(expr.iter, (Attribute, Name)): |
||||
# HACK |
||||
# The loop variable is not annotated so we infer its type by looking at the type of the iterator |
||||
if isinstance(expr.iter, Attribute): # state variable |
||||
iter_expr = expr.iter |
||||
loop_iterator = list( |
||||
filter( |
||||
lambda x: x._variable.name == iter_expr.attr, |
||||
self._contract_parser._variables_parser, |
||||
) |
||||
)[0] |
||||
|
||||
else: # local variable |
||||
iter_expr = expr.iter |
||||
loop_iterator = list( |
||||
filter( |
||||
lambda x: x._variable.name == iter_expr.id, |
||||
self._local_variables_parser, |
||||
) |
||||
)[0] |
||||
|
||||
# TODO use expr.src instead of -1:-1:1? |
||||
cond_expr = Compare( |
||||
"-1:-1:-1", |
||||
-1, |
||||
left=Name("-1:-1:-1", -1, "counter_var"), |
||||
op="<=", |
||||
right=Call( |
||||
"-1:-1:-1", |
||||
-1, |
||||
func=Name("-1:-1:-1", -1, "len"), |
||||
args=[iter_expr], |
||||
keywords=[], |
||||
keyword=None, |
||||
), |
||||
) |
||||
node_condition = self._new_node(NodeType.IFLOOP, expr.src, scope) |
||||
node_condition.add_unparsed_expression(cond_expr) |
||||
|
||||
if loop_iterator._elem_to_parse.value.id == "DynArray": |
||||
loop_var_annotation = loop_iterator._elem_to_parse.slice.value.elements[0] |
||||
else: |
||||
loop_var_annotation = loop_iterator._elem_to_parse.value |
||||
|
||||
value = Subscript( |
||||
"-1:-1:-1", |
||||
-1, |
||||
value=Name("-1:-1:-1", -1, loop_iterator._variable.name), |
||||
slice=Index("-1:-1:-1", -1, value=Name("-1:-1:-1", -1, "counter_var")), |
||||
) |
||||
loop_var = AnnAssign( |
||||
expr.target.src, |
||||
expr.target.node_id, |
||||
target=expr.target, |
||||
annotation=loop_var_annotation, |
||||
value=value, |
||||
) |
||||
|
||||
elif isinstance(expr.iter, Call): # range |
||||
range_val = expr.iter.args[0] |
||||
cond_expr = Compare( |
||||
"-1:-1:-1", |
||||
-1, |
||||
left=Name("-1:-1:-1", -1, "counter_var"), |
||||
op="<=", |
||||
right=range_val, |
||||
) |
||||
node_condition = self._new_node(NodeType.IFLOOP, expr.src, scope) |
||||
node_condition.add_unparsed_expression(cond_expr) |
||||
loop_var = AnnAssign( |
||||
expr.target.src, |
||||
expr.target.node_id, |
||||
target=expr.target, |
||||
annotation=Name("-1:-1:-1", -1, "uint256"), |
||||
value=Name("-1:-1:-1", -1, "counter_var"), |
||||
) |
||||
|
||||
else: |
||||
raise NotImplementedError |
||||
|
||||
# After creating condition node, we link it declaration of the loop variable |
||||
link_underlying_nodes(counter_node, node_condition) |
||||
|
||||
# Create an expression for the loop increment (counter_var += 1) |
||||
loop_increment = AugAssign( |
||||
"-1:-1:-1", |
||||
-1, |
||||
target=Name("-1:-1:-1", -1, "counter_var"), |
||||
op="+=", |
||||
value=Int("-1:-1:-1", -1, 1), |
||||
) |
||||
node_increment = self._new_node(NodeType.EXPRESSION, expr.src, scope) |
||||
node_increment.add_unparsed_expression(loop_increment) |
||||
link_underlying_nodes(node_increment, node_condition) |
||||
|
||||
continue_destination = node_increment |
||||
break_destination = node_endLoop |
||||
|
||||
# We assign the index variable or range variable in the loop body on each iteration |
||||
expr.body.insert(0, loop_var) |
||||
body_node = None |
||||
new_node = node_condition |
||||
for stmt in expr.body: |
||||
body_node = parse_statement( |
||||
new_node, stmt, continue_destination, break_destination |
||||
) |
||||
new_node = body_node |
||||
|
||||
if body_node is not None: |
||||
link_underlying_nodes(body_node, node_increment) |
||||
|
||||
link_underlying_nodes(node_condition, node_endLoop) |
||||
|
||||
curr_node = node_endLoop |
||||
|
||||
elif isinstance(expr, Continue): |
||||
new_node = self._new_node(NodeType.CONTINUE, expr.src, scope) |
||||
link_underlying_nodes(curr_node, new_node) |
||||
link_underlying_nodes(new_node, continue_destination) |
||||
|
||||
elif isinstance(expr, Break): |
||||
new_node = self._new_node(NodeType.BREAK, expr.src, scope) |
||||
link_underlying_nodes(curr_node, new_node) |
||||
link_underlying_nodes(new_node, break_destination) |
||||
|
||||
elif isinstance(expr, Return): |
||||
new_node = self._new_node(NodeType.RETURN, expr.src, scope) |
||||
if expr.value is not None: |
||||
new_node.add_unparsed_expression(expr.value) |
||||
|
||||
link_underlying_nodes(curr_node, new_node) |
||||
curr_node = new_node |
||||
|
||||
elif isinstance(expr, Assert): |
||||
new_node = self._new_node(NodeType.EXPRESSION, expr.src, scope) |
||||
new_node.add_unparsed_expression(expr) |
||||
|
||||
link_underlying_nodes(curr_node, new_node) |
||||
curr_node = new_node |
||||
|
||||
elif isinstance(expr, Log): |
||||
new_node = self._new_node(NodeType.EXPRESSION, expr.src, scope) |
||||
new_node.add_unparsed_expression(expr.value) |
||||
|
||||
link_underlying_nodes(curr_node, new_node) |
||||
curr_node = new_node |
||||
|
||||
elif isinstance(expr, If): |
||||
condition_node = self._new_node(NodeType.IF, expr.test.src, scope) |
||||
condition_node.add_unparsed_expression(expr.test) |
||||
|
||||
endIf_node = self._new_node(NodeType.ENDIF, expr.src, scope) |
||||
|
||||
true_node = None |
||||
new_node = condition_node |
||||
for stmt in expr.body: |
||||
true_node = parse_statement( |
||||
new_node, stmt, continue_destination, break_destination |
||||
) |
||||
new_node = true_node |
||||
|
||||
link_underlying_nodes(true_node, endIf_node) |
||||
|
||||
false_node = None |
||||
new_node = condition_node |
||||
for stmt in expr.orelse: |
||||
false_node = parse_statement( |
||||
new_node, stmt, continue_destination, break_destination |
||||
) |
||||
new_node = false_node |
||||
|
||||
if false_node is not None: |
||||
link_underlying_nodes(false_node, endIf_node) |
||||
|
||||
else: |
||||
link_underlying_nodes(condition_node, endIf_node) |
||||
|
||||
link_underlying_nodes(curr_node, condition_node) |
||||
curr_node = endIf_node |
||||
|
||||
elif isinstance(expr, Pass): |
||||
pass |
||||
elif isinstance(expr, Raise): |
||||
new_node = self._new_node(NodeType.EXPRESSION, expr.src, scope) |
||||
new_node.add_unparsed_expression(expr) |
||||
link_underlying_nodes(curr_node, new_node) |
||||
curr_node = new_node |
||||
|
||||
else: |
||||
raise ParsingError(f"Statement not parsed {expr.__class__.__name__} {expr}") |
||||
|
||||
return curr_node |
||||
|
||||
curr_node = entry_node |
||||
for expr in cfg: |
||||
curr_node = parse_statement(curr_node, expr) |
||||
|
||||
# endregion |
||||
################################################################################### |
||||
################################################################################### |
||||
|
||||
def _add_param(self, param: Arg, initialized: bool = False) -> LocalVariableVyper: |
||||
|
||||
local_var = LocalVariable() |
||||
local_var.set_function(self._function) |
||||
local_var.set_offset(param.src, self._function.compilation_unit) |
||||
local_var_parser = LocalVariableVyper(local_var, param) |
||||
|
||||
if initialized: |
||||
local_var.initialized = True |
||||
|
||||
if local_var.location == "default": |
||||
local_var.set_location("memory") |
||||
|
||||
self._add_local_variable(local_var_parser) |
||||
return local_var_parser |
||||
|
||||
def _parse_params(self, params: Arguments): |
||||
|
||||
self._function.parameters_src().set_offset(params.src, self._function.compilation_unit) |
||||
if params.defaults: |
||||
self._function._default_args_as_expressions = params.defaults |
||||
for param in params.args: |
||||
local_var = self._add_param(param) |
||||
self._function.add_parameters(local_var.underlying_variable) |
||||
|
||||
def _parse_returns(self, returns: Union[Name, TupleVyper, Subscript]): |
||||
|
||||
self._function.returns_src().set_offset(returns.src, self._function.compilation_unit) |
||||
# Only the type of the arg is given, not a name. We create an an `Arg` with an empty name |
||||
# so that the function has the correct return type in its signature but doesn't clash with |
||||
# other identifiers during name resolution (`find_variable`). |
||||
if isinstance(returns, (Name, Subscript)): |
||||
local_var = self._add_param(Arg(returns.src, returns.node_id, "", annotation=returns)) |
||||
self._function.add_return(local_var.underlying_variable) |
||||
else: |
||||
assert isinstance(returns, TupleVyper) |
||||
for ret in returns.elements: |
||||
local_var = self._add_param(Arg(ret.src, ret.node_id, "", annotation=ret)) |
||||
self._function.add_return(local_var.underlying_variable) |
||||
|
||||
################################################################################### |
||||
################################################################################### |
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in new issue