Skip to content

Commit

Permalink
feat: add ContractType.pcmap (#47)
Browse files Browse the repository at this point in the history
  • Loading branch information
helloibis authored Oct 31, 2022
1 parent 43e3ce5 commit 38faac0
Show file tree
Hide file tree
Showing 2 changed files with 117 additions and 1 deletion.
63 changes: 62 additions & 1 deletion ethpm_types/contract_type.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
from typing import Callable, Iterator, List, Optional, Union
from typing import Callable, Dict, Iterator, List, Optional, Union

from eth_utils import add_0x_prefix
from hexbytes import HexBytes
Expand Down Expand Up @@ -139,6 +139,7 @@ class SourceMap(BaseModel):
"""
As part of the AST output, the compiler provides the range of the source code
that is represented by the respective node in the AST.
This can be used for various purposes ranging from static analysis tools that
report errors based on the AST and debugging tools that highlight local variables
and their uses.
Expand Down Expand Up @@ -204,6 +205,58 @@ def extract_sourcemap_item(expanded_row, item_idx, previous_val=None):
yield item


class PCMapItem(BaseModel):
"""
Line information for a given EVM instruction.
These are utilized in the pc-map by which the compiler generates source code spans for given
program counter positions.
"""

line_start: Optional[int] = None
column_start: Optional[int] = None
line_end: Optional[int] = None
column_end: Optional[int] = None


class PCMap(BaseModel):
"""
As part of the source output, the compiler provides a map of program counter values to
statements in the source code that the instructions were compiled from.
This can be used for various purposes ranging from static analysis tools that
report errors based on the program counter value and debugging tools that highlight local
variables and their uses.
"""

__root__: Dict[str, Optional[List[Optional[int]]]]

def parse(self) -> Dict[int, PCMapItem]:
"""
Parses the pc map string into a map of ``PCMapItem`` items, using integer pc values as keys.
The format from the compiler will have numeric string keys with lists of ints for values.
These integers represent (in order) the starting line, starting column, ending line, and
ending column numbers.
"""
results = {}

for key, value in self.__root__.items():
if value is not None:
result = PCMapItem(
line_start=value[0],
column_start=value[1],
line_end=value[2],
column_end=value[3],
)
else:
result = PCMapItem()

results[int(key)] = result

return results


class ABIList(list):
"""
Adds selection by name, selector and keccak(selector).
Expand Down Expand Up @@ -290,6 +343,14 @@ class ContractType(BaseModel):
**NOTE**: This is not part of the canonical EIP-2678 spec.
"""

pcmap: Optional[Dict[str, PCMapItem]] = None
"""
The program counter map representing which lines in the source code account for which
instructions in the bytecode.
**NOTE**: This is not part of the canonical EIP-2678 spec.
"""

userdoc: Optional[dict] = None
devdoc: Optional[dict] = None

Expand Down
55 changes: 55 additions & 0 deletions tests/test_pc_map.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
from ethpm_types.contract_type import PCMap, PCMapItem


def test_pc_map_valid():
"""
Test the parsing of a valid pc-map from a compiler's output.
"""
pcmap = PCMap.parse_obj({"186": [10, 20, 10, 40]}).parse()

keys = list(pcmap.keys())

assert len(keys) == 1
assert keys[0] == 186
assert pcmap[186] == PCMapItem(line_start=10, column_start=20, line_end=10, column_end=40)


def test_pc_map_empty_line_info():
"""
Test the parsing of a pc-map from a compiler's output that has empty line
information.
"""
pcmap = PCMap.parse_obj({"186": [None, None, None, None]}).parse()

keys = list(pcmap.keys())

assert len(keys) == 1
assert keys[0] == 186
assert pcmap[186] == PCMapItem(
line_start=None, column_start=None, line_end=None, column_end=None
)


def test_pc_map_missing_line_info():
"""
Test the parsing of a pc-map from a compiler's output that is entirely missing line
information.
"""
pcmap = PCMap.parse_obj({"186": None}).parse()

keys = list(pcmap.keys())

assert len(keys) == 1
assert keys[0] == 186
assert pcmap[186] == PCMapItem(
line_start=None, column_start=None, line_end=None, column_end=None
)


def test_pc_map_empty():
"""
Test the parsing of an empty pc-map from a compiler's output.
"""
pcmap = PCMap.parse_obj({}).parse()

assert pcmap == {}

0 comments on commit 38faac0

Please sign in to comment.