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 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280
|
# --------------------------------------------------------------------------------------
# Copyright (c) 2021-2025, Nucleic Development Team.
#
# Distributed under the terms of the Modified BSD License.
#
# The full license is in the file LICENSE, distributed with this software.
# --------------------------------------------------------------------------------------
import ast
import warnings
from typing import Dict, Iterable, List, Type, Union
from .. import enaml_ast
from .base_python_parser import BasePythonParser
class BaseEnamlParser(BasePythonParser):
"""Base class for an enaml parser.
It provides AST validation methods that are used by the actual parser.
"""
# The nodes which can be inverted to form an assignable expression.
INVERTABLE = (ast.Name, ast.Attribute, ast.Call, ast.Subscript)
# The disallowed ast types on the rhs of a :: operator
NOTIFICATION_DISALLOWED = {
ast.FunctionDef: "function definition",
ast.ClassDef: "class definition",
ast.Yield: "yield statement",
ast.Return: "return statement",
}
# The disallowed ast types on the rhs of a << operator
SUBSCRIPTION_DISALLOWED = {
ast.FunctionDef: "function definition",
ast.ClassDef: "class definition",
ast.Yield: "yield statement",
}
# The disallowed ast types in the body of a declarative function
DECL_FUNCDEF_DISALLOWED = {
ast.FunctionDef: "function definition",
ast.ClassDef: "class definition",
ast.Yield: "yield statement",
ast.GeneratorExp: "generator expressions",
}
def create_python_module(self, stmts: List[ast.AST]) -> enaml_ast.PythonModule:
"""Create a python from a list of Python ast node."""
return enaml_ast.PythonModule(
ast=ast.Module(
body=stmts,
type_ignores=[],
),
lineno=stmts[0].lineno,
col_offset=stmts[0].col_offset,
end_lineno=stmts[-1].end_lineno,
end_col_offset=stmts[-1].end_col_offset,
)
def create_enaml_module(
self,
nodes: Iterable[ast.AST],
lineno: int,
col_offset: int,
end_lineno: int,
end_col_offset: int,
) -> enaml_ast.Module:
"""Create an enaml Module from a list of ast nodes."""
body = []
stmts = []
for node in nodes:
if isinstance(node, (enaml_ast.EnamlDef, enaml_ast.Template)):
if stmts:
body.append(self.create_python_module(stmts))
stmts = []
body.append(node)
else:
stmts.append(node)
if stmts:
body.append(self.create_python_module(stmts))
return enaml_ast.Module(
body=body,
lineno=lineno,
col_offset=col_offset,
end_lineno=end_lineno,
end_col_offset=end_col_offset,
)
# rework to take a list of ast node and return it and filter out None values
def validate_enamldef(self, node: enaml_ast.EnamlDef) -> enaml_ast.EnamlDef:
"""Validate the correctness of names in an enamldef definition.
This function ensures that identifiers do not shadow one another.
"""
ident_names = set()
def check_id(name, node):
if name in ident_names:
msg = (
f"redeclaration of identifier '{name}'"
" (this will be an error in Enaml version 1.0)"
)
warnings.warn_explicit(msg, SyntaxWarning, self.filename, node.lineno)
ident_names.add(name)
# validate the identifiers
ChildDef = enaml_ast.ChildDef
TemplateInst = enaml_ast.TemplateInst
stack = list(reversed(node.body))
while stack:
n = stack.pop()
if isinstance(n, ChildDef):
if n.identifier:
check_id(n.identifier, n)
stack.extend(reversed(n.body))
elif isinstance(n, TemplateInst):
idents = n.identifiers
if idents is not None:
for name in idents.names:
check_id(name, idents)
if idents.starname:
check_id(idents.starname, idents)
return node
def create_python_func_for_operator(
self, body: List[ast.AST], forbidden_nodes: Dict[Type[ast.AST], str], msg: str
) -> enaml_ast.PythonModule:
for node in body:
for item in ast.walk(node):
if type(item) in forbidden_nodes:
msg = msg % forbidden_nodes[type(item)]
self.raise_syntax_error_known_location(msg, item)
func_node = ast.FunctionDef(
name = "f",
args = self.make_arguments(None, [], None, None, None),
decorator_list = [],
returns = None,
body = body,
lineno = body[0].lineno,
col_offset = body[0].col_offset,
end_lineno = body[-1].end_lineno,
end_col_offset = body[-1].end_col_offset,
)
return self.create_python_module([func_node])
def validate_decl_func_body(self, body: List[ast.AST]) -> List[ast.AST]:
"""Validate the body of declarative function.
Any definition that may capture the surrounding scope is forbidden.
"""
for node in body:
for item in ast.walk(node):
if type(item) in self.DECL_FUNCDEF_DISALLOWED:
msg = (
f"{self.DECL_FUNCDEF_DISALLOWED[type(item)]} "
"not allowed in a declarative function block."
)
self.raise_syntax_error_known_location(msg, item)
return body
def validate_template(self, node: enaml_ast.Template) -> enaml_ast.Template:
"""Validate the correctness of names in a template definitions.
This function ensures that parameters, const expressions, and
identifiers do not shadow one another.
"""
param_names = set()
const_names = set()
ident_names = set()
def check_const(name: str, node: enaml_ast.ConstExpr):
msg = None
if name in param_names:
msg = f"declaration of 'const {name}' shadows a parameter"
elif name in const_names:
msg = f"redeclaration of 'const {name}'"
if msg is not None:
self.raise_syntax_error_known_location(msg, node)
const_names.add(name)
def check_id(
name: str, node: Union[enaml_ast.ChildDef, enaml_ast.TemplateIdentifiers]
):
msg = None
if name in param_names:
msg = f"identifier '{name}' shadows a parameter"
elif name in const_names:
msg = f"identifier '{name}' shadows a const expression"
elif name in ident_names:
msg = f"redeclaration of identifier '{name}'"
if msg is not None:
self.raise_syntax_error_known_location(msg, node)
ident_names.add(name)
# collect the parameter names
params = node.parameters
for param in params.positional:
param_names.add(param.name)
for param in params.keywords:
param_names.add(param.name)
if params.starparam:
param_names.add(params.starparam)
# validate the const expressions
ConstExpr = enaml_ast.ConstExpr
for item in node.body:
if isinstance(item, ConstExpr):
check_const(item.name, item)
# validate the identifiers
ChildDef = enaml_ast.ChildDef
TemplateInst = enaml_ast.TemplateInst
stack = list(reversed(node.body))
while stack:
n = stack.pop()
if isinstance(n, ChildDef):
if n.identifier:
check_id(n.identifier, n)
stack.extend(reversed(n.body))
elif isinstance(n, TemplateInst):
idents = n.identifiers
if idents is not None:
for name in idents.names:
check_id(name, idents)
if idents.starname:
check_id(idents.starname, idents)
return node
def validate_template_paramlist(
self,
paramlist: List[
Union[enaml_ast.PositionalParameter, enaml_ast.KeywordParameter]
],
starparam: str,
) -> Dict[str, Union[list, str]]:
keywords = []
positional = []
seen_params = set([starparam])
for param in paramlist:
if param.name in seen_params:
msg = f"duplicate argument '{param.name}' in template definition"
self.raise_syntax_error_known_location(msg, param)
seen_params.add(param.name)
if isinstance(param, enaml_ast.KeywordParameter):
keywords.append(param)
elif keywords:
msg = "non-default argument follows default argument"
self.raise_syntax_error_known_location(msg, param)
else:
positional.append(param)
return {"positional": positional, "keywords": keywords, "starparam": starparam}
def validate_template_inst(
self, node: enaml_ast.TemplateInst
) -> enaml_ast.TemplateInst:
"""Validate a template instantiation.
This function ensures that the bindings on the instantiation refer
to declared identifiers on the instantiation.
"""
names = set()
if node.identifiers:
names.update(node.identifiers.names)
for binding in node.body:
if binding.name not in names:
msg = f"'{binding.name}' is not a valid template id reference"
self.raise_syntax_error_known_location(msg, binding)
return node
|