/
pre_parser.py
133 lines (102 loc) · 4.58 KB
/
pre_parser.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
import io
import re
from tokenize import COMMENT, NAME, OP, TokenError, TokenInfo, tokenize, untokenize
from typing import Tuple
from semantic_version import NpmSpec, Version
from vyper.exceptions import SyntaxException, VersionException
from vyper.typing import ModificationOffsets, ParserPosition
VERSION_ALPHA_RE = re.compile(r"(?<=\d)a(?=\d)") # 0.1.0a17
VERSION_BETA_RE = re.compile(r"(?<=\d)b(?=\d)") # 0.1.0b17
VERSION_RC_RE = re.compile(r"(?<=\d)rc(?=\d)") # 0.1.0rc17
def _convert_version_str(version_str: str) -> str:
"""
Convert loose version (0.1.0b17) to strict version (0.1.0-beta.17)
"""
version_str = re.sub(VERSION_ALPHA_RE, "-alpha.", version_str) # 0.1.0-alpha.17
version_str = re.sub(VERSION_BETA_RE, "-beta.", version_str) # 0.1.0-beta.17
version_str = re.sub(VERSION_RC_RE, "-rc.", version_str) # 0.1.0-rc.17
return version_str
def validate_version_pragma(version_str: str, start: ParserPosition) -> None:
"""
Validates a version pragma directive against the current compiler version.
"""
from vyper import __version__
# NOTE: should be `x.y.z.*`
installed_version = ".".join(__version__.split(".")[:3])
version_arr = version_str.split("@version")
raw_file_version = version_arr[1].strip()
strict_file_version = _convert_version_str(raw_file_version)
strict_compiler_version = Version(_convert_version_str(installed_version))
if len(strict_file_version) == 0:
raise VersionException("Version specification cannot be empty", start)
try:
npm_spec = NpmSpec(strict_file_version)
except ValueError:
raise VersionException(
f'Version specification "{raw_file_version}" is not a valid NPM semantic '
f"version specification",
start,
)
if not npm_spec.match(strict_compiler_version):
raise VersionException(
f'Version specification "{raw_file_version}" is not compatible '
f'with compiler version "{installed_version}"',
start,
)
# compound statements that are replaced with `class`
VYPER_CLASS_TYPES = {"enum", "event", "interface", "struct"}
# simple statements or expressions that are replaced with `yield`
VYPER_EXPRESSION_TYPES = {"log"}
def pre_parse(code: str) -> Tuple[ModificationOffsets, str]:
"""
Re-formats a vyper source string into a python source string and performs
some validation. More specifically,
* Translates "interface", "struct" and "event" keywords into python "class" keyword
* Validates "@version" pragma against current compiler version
* Prevents direct use of python "class" keyword
* Prevents use of python semi-colon statement separator
Also returns a mapping of detected interface and struct names to their
respective vyper class types ("interface" or "struct").
Parameters
----------
code : str
The vyper source code to be re-formatted.
Returns
-------
dict
Mapping of offsets where source was modified.
str
Reformatted python source string.
"""
result = []
modification_offsets: ModificationOffsets = {}
try:
code_bytes = code.encode("utf-8")
token_list = list(tokenize(io.BytesIO(code_bytes).readline))
for i in range(len(token_list)):
token = token_list[i]
toks = [token]
typ = token.type
string = token.string
start = token.start
end = token.end
line = token.line
if typ == COMMENT and "@version" in string:
validate_version_pragma(string[1:], start)
if typ == NAME and string in ("class", "yield"):
raise SyntaxException(
f"The `{string}` keyword is not allowed. ", code, start[0], start[1]
)
if typ == NAME:
if string in VYPER_CLASS_TYPES and start[1] == 0:
toks = [TokenInfo(NAME, "class", start, end, line)]
modification_offsets[start] = f"{string.capitalize()}Def"
elif string in VYPER_EXPRESSION_TYPES:
toks = [TokenInfo(NAME, "yield", start, end, line)]
modification_offsets[start] = string.capitalize()
if (typ, string) == (OP, ";"):
raise SyntaxException("Semi-colon statements not allowed", code, start[0], start[1])
result.extend(toks)
except TokenError as e:
raise SyntaxException(e.args[0], code, e.args[1][0], e.args[1][1]) from e
return modification_offsets, untokenize(result).decode("utf-8")