def test_frame_scopes(): # regular block (scope is True) and frame has no parent -> scope == Scope() block = Block(Ident("hello"), Ident("there")) frame = Frame(block) assert frame.scope() == Scope() assert frame.block == block assert frame.parent is None # irregular block (scope is False) frame has no parent... block = Block(Ident("hello"), Ident("there"), scope=False) frame = Frame(block) # ...raises TypeError since no parent with raises(TypeError): frame.scope() assert frame.block == block assert frame.parent is None # regular block (scope is True) and frame has a parent -> scope == Scope() block = Block(Ident("hello"), Ident("there")) parent = Block(Ident("fizz"), Ident("fuzz")) frame = Frame(block, parent) assert frame.scope() == Scope() assert frame.block == block assert frame.parent is parent
class Evaluator(Visitor): def __init__(self, root, parser, options: dict): super().__init__(root) self.bifs = bifs self.parser = parser self.options = options self.functions = options.get("functions", {}) # making sure that newly defined functions are builtins # for name, function in self.functions.items(): # if name not in self.bifs.keys(): # log.debug(f'Adding extra built-in function: {name}.') # print(f'Adding extra built-in function: {name}: {function}.') # raw_bifs.append(name) # self.bifs[name] = function self.stack = Stack() self.imports = options.get("imports", []) self.commons = options.get("globals", {}) self.paths = options.get("paths", []) self.prefix = options.get("prefix", "") self.filename = options.get("filename", None) self.include_css = options.get("include css", False) self.resolve_url = False # todo: why is this done separately from the bifs? if "url" in self.functions: url = self.functions["url"] if url.__name__ == "resolver" and hasattr(url, "options"): log.debug(f"Using this resolver: {url}.") self.resolve_url = True filename = Path(options.get("filename", ".")) self.paths.append(str(filename.parent)) self.common = Frame(root) self.stack.append(self.common) self.warnings = options.get("warn", False) self.calling = [] # todo: remove, use stack self.import_stack = [] self.require_history = {} self.result = 0 self.current_scope = None self.ignore_colors = None self.property = None def vendors(self): return [node.string for node in self.lookup("vendors").nodes] def import_file(self, node: Import, file, literal, lineno=1, column=1): log.debug(f"importing {file}; {self.import_stack}") # handling 'require' if node.once: if self.require_history.get(file, False): return null self.require_history[file] = True if literal and not self.include_css: return node # avoid overflows from reimporting the same file if file in self.import_stack: raise ImportError("import loop has been found") with open(file, "r") as f: source = f.read() # shortcut for empty files if not source.strip(): return null # expose imports node.path = file node.dirname = Path(file).parent # store modified time node.mtime = os.stat(file).st_mtime self.paths.append(str(node.dirname)) if "_imports" in self.options: self.options["_imports"].append(node.clone()) # parse the file self.import_stack.append(file) filename_node.current_filename = file if literal: re.sub("\n\n?", "\n", source) literal = Literal(source, lineno=self.parser.lineno, column=self.parser.column) literal.lineno = 1 literal.column = 1 if not self.resolve_url: return literal # create block block = Block(None, None, lineno=lineno, column=column) block.filename = file # parse merged = {} merged.update(self.options) merged.update({"root": block}) parser = Parser(source, merged) try: block = parser.parse() except Exception: line = parser.lexer.lineno column = parser.lexer.column if literal and self.include_css and self.resolve_url: self.warn(f"ParseError: {file}:{line}:{column}. " f"This file is included as-is") return literal else: raise ParseError( "Issue when parsing an imported file", filename=file, lineno=line, column=column, input=source, ) # evaluate imported 'root' block = block.clone(self.get_current_block()) block.parent = self.get_current_block() block.scope = False ret = self.visit(block) self.import_stack.pop() if not self.resolve_url: # or not self.resolve_url no_check: self.paths.pop() return ret def visit(self, node): try: return super().visit(node) except StilusError as se: if not se.filename: raise se try: with open(str(se.filename)) as f: input = f.read() except (AttributeError, FileNotFoundError): pass raise StilusError( self.stack, filename=node.filename, lineno=node.lineno, column=node.column, input=input, ) def setup(self): self.populate_global_scope() for file in reversed(self.imports): expr = Expression() expr.append( String(file, lineno=self.parser.lineno, column=self.parser.column)) self.root.nodes.insert( 0, Import(expr, lineno=self.parser.lineno, column=self.parser.column), ) def populate_global_scope(self): """ Populate the global scope with: - css colors - user-defined globals :return: """ self.common._scope = Scope() # colors for color, value in colors.items(): rgba = RGBA(value[0], value[1], value[2], value[3]) ident = Ident(color, rgba) rgba.name = color self.common.scope().add(ident) # todo: should this be here? # self.common.scope().add(Ident('embedurl', # Function('embedurl', url.fn, # lineno=self.parser.lineno, # column=self.parser.column))) # user defined globals commons = self.commons for common, val in commons.items(): if val.name: self.common.scope().add(Ident(common, val)) def evaluate(self): self.setup() return self.visit(self.root) def visit_group(self, group: Group): new_nodes = [] for selector in group.nodes: selector.value = self.interpolate(selector) new_nodes.append(selector) group.nodes = new_nodes group.block = self.visit(group.block) return group def visit_returnnode(self, ret): ret.expression = self.visit(ret.expression) raise ret def visit_media(self, media): media.block = self.visit(media.block) media.value = self.visit(media.value) return media def visit_querylist(self, queries): for node in queries.nodes: self.visit(node) if len(queries.nodes) == 1: query = queries.nodes[0] val = self.lookup(query.type) if val: if hasattr(val, "first") and hasattr(val.first(), "string"): val = val.first().string else: return queries parser = Parser(val, self.options) queries = self.visit(parser.queries()) return queries def visit_query(self, node): node.predicate = self.visit(node.predicate) node.type = self.visit(node.type) for n in node.nodes: self.visit(n) return node def visit_feature(self, node): node.name = self.interpolate(node) if node.expr: self.result += 1 node.expr = self.visit(node.expr) self.result -= 1 return node def visit_objectnode(self, obj: ObjectNode): for key in obj.values.keys(): obj.values[key] = self.visit(obj.values[key]) return obj def visit_member(self, node): left = node.left right = node.right obj = self.visit(left).first() if "objectnode" != obj.node_name: raise ParseError(f"{left} has no property .{right}") if node.value: self.result += 1 obj.set(right.name, self.visit(node.value)) self.result -= 1 return obj.get(right.name) def visit_keyframes(self, keyframes): if keyframes.fabricated: return keyframes keyframes.value = self.interpolate(keyframes).strip() val = self.lookup(keyframes.value) if val: if type(val.first()) == Function: keyframes.value = val.first().function_name else: keyframes.value = val.first().string keyframes.block = self.visit(keyframes.block) if "official" != keyframes.prefix: return keyframes for prefix in self.vendors(): # IE never had prefixes for keyframes if "ms" == prefix: continue node = keyframes.clone() node.value = keyframes.value node.prefix = prefix node.block = keyframes.block node.fabricated = True self.get_current_block().append(node) return null def visit_function(self, fn, args=None, content=None): # check local local = self.stack.current_frame().scope().lookup(fn.function_name) if local: self.warn(f'local {local.node_name} "fn.function_name" ' f"previously defined in this scope") # user-defined user = self.functions.get(fn.function_name, None) if user: self.warn(f'user-defined function "{fn.function_name}" ' f"is already defined") if fn.function_name in self.bifs: self.warn(f'built-in function "{fn.function_name}" ' f"is already defined") return fn def visit_each(self, each): self.result += 1 expr = utils.unwrap(self.visit(each.expr)) length = len(expr.nodes) val = Ident(each.value) key = Ident("__index__") if each.key: key = Ident(each.key) scope = self.get_current_scope() block = self.get_current_block() vals = [] self.result -= 1 each.block.scope = False def visit_body(key, value): scope.add(value) scope.add(key) body = self.visit(each.block.clone()) vals.extend(body.nodes) # for prop in obj if length == 1 and "objectnode" == expr.nodes[0].node_name: obj = expr.nodes[0] for prop in obj.values: val.value = String(prop, lineno=self.parser.lineno, column=self.parser.column) key.value = obj.get(prop) # checkme: works? visit_body(key, val) else: for i, n in enumerate(expr.nodes): val.value = n key.value = Unit(i) visit_body(key, val) self.mixin(vals, block) if vals and len(vals) > 0: return vals[len(vals) - 1] else: return null def visit_call(self, call): fn = self.lookup(call.function_name) log.debug(f"Visiting {fn}; {call}...") if hasattr(fn, "param") and fn.param: log.debug(fn.params) # url() self.ignore_colors = "url" == call.function_name # variable function if fn and "expression" == fn.node_name: fn = fn.nodes[0] # not a function? try user-defined or built-ins if fn and "function" != fn.node_name: fn = self.lookup_function(call.function_name) # undefined function? render literal css if fn is None or fn.node_name != "function": ret = None if "calc" == self.unvendorize(call.function_name): if call.args.nodes: if call.args.nodes[0]: ret = Literal( call.function_name + str(call.args.nodes[0]), lineno=self.parser.lineno, column=self.parser.column, ) else: ret = self.literal_call(call) self.ignore_colors = False return ret self.calling.append(call.function_name) # massive stack if len(self.calling) > 200: raise ParseError("Maximum stylus call stack size exceeded") # first node in expression if fn and "expression" == fn.function_name: fn = fn.first() # evaluate arguments self.result += 1 args = self.visit(call.args) if hasattr(args, "map"): for key in args.map: args.map[key] = self.visit(args.map[key].clone()) self.result -= 1 log.debug(f"Going to invoke function: {fn} " f"(builtin: {fn.builtin}).") if fn.builtin or (fn.function_name == "url" and hasattr(fn, "params") and fn.params.__name__ == "resolver"): log.debug(f"{fn} is a built-in method ({fn.params}).") ret = self.invoke_builtin(fn.params, args) elif "function" == fn.node_name: # user-defined # evaluate mixin block log.debug(f"{fn} is a regular method.") if call.block: log.debug(f"{fn} is a mixin.") call.block = self.visit(call.block) ret = self.invoke_function(fn, args, call.block) self.calling.pop() self.ignore_colors = False return ret def visit_ident(self, ident): if ident.property: # property lookup prop = self.lookup_property(ident.name) if prop: return self.visit(prop.expr.clone()) return null elif ident.value.node_name == "null": # lookup val = self.lookup(ident.name) # object or block mixin if val is not None and ident.mixin: self.mixin_node(val) if val is not None: return self.visit(val) else: return ident # return self.visit(val) if val is not None else ident else: # assign self.result += 1 ident.value = self.visit(ident.value) self.result -= 1 self.get_current_scope().add(ident) return ident.value def visit_binop(self, binop): # special case 'is defined' pseudo binop if "is defined" == binop.op: return self.is_defined(binop.left) self.result += 1 # visit operands op = binop.op left = self.visit(binop.left) if "||" == op or "&&" == op: right = binop.right else: right = self.visit(binop.right) # hack (sic): ternary if binop.value: value = self.visit(binop.value) else: value = null self.result -= 1 # operate try: return self.visit(left.operate(op, right, value)) except CoercionError as e: # disregard coercion issues in equality # checks, and simply return false if op == "==": return Boolean(False) elif op == "!=": return Boolean(True) raise e def visit_unaryop(self, unary): op = unary.op node = self.visit(unary.expr) if "!" != op: node = node.first().clone() utils.assert_type(node, "unit") if op == "-": node.value = -node.value elif op == "+": node.value = +node.value elif op == "~": if isinstance(node.value, float): node.value = ~int(node.value) else: node.value = ~node.value elif op == "!": return node.to_boolean().negate() return node def visit_ternary(self, ternary): ok = self.visit(ternary.cond).to_boolean() if ok.is_true(): return self.visit(ternary.true_expr) else: return self.visit(ternary.false_expr) def visit_expression(self, expr): expr.nodes = [self.visit(node) for node in expr.nodes] # support (n * 5)px etc if self.castable(expr): expr = self.cast(expr) return expr def visit_arguments(self, args): return self.visit_expression(args) def visit_property(self, prop): name = self.interpolate(prop) fn = self.lookup(name) call = fn and "function" == fn.first().node_name literal = name in self.calling _prop = self.property prop_lit = True if hasattr(prop, "literal") and prop.literal else False if call and not literal and not prop_lit: # function of the same node_name clone = prop.expr.clone(None, None) args = Arguments.from_expression(utils.unwrap(clone)) prop.name = name self.property = prop self.result += 1 self.property.expr = self.visit(prop.expr) self.result -= 1 ret = self.visit( Call( name, args, lineno=self.parser.lineno, column=self.parser.column, )) self.property = _prop return ret else: # regular property self.result += 1 prop.name = name prop.literal = True self.property = prop prop.expr = self.visit(prop.expr) self.property = _prop self.result -= 1 return prop def visit_root(self, block): if block != self.root: # normalize cached imports return self.visit(block.to_block()) # for i, node in enumerate(list(block.nodes)): i = 0 while i < len(block.nodes): block.index = i v = self.visit(block.nodes[i]) if hasattr(block, "mixin") and block.mixin: # log.debug(f'Not adding mixin [{v}]!') block.mixin = False else: block.nodes[i] = v i += 1 return block def visit_block(self, block): self.stack.append(Frame(block)) index = 0 while index < len(block.nodes): block.index = index try: v = self.visit(block.nodes[block.index]) if block.mixin: log.debug(f"Not adding mixin [{v}]!") block.mixin = False else: try: block.nodes[index] = v except TypeError: pass except ReturnNode as rn: if self.result: self.stack.pop() raise rn else: block.nodes[block.index] = rn break index = block.index + 1 block.index += 1 self.stack.pop() return block def visit_atblock(self, atblock): atblock.block = self.visit(atblock.block) return atblock def visit_atrule(self, atrule): atrule.value = self.interpolate(atrule) if atrule.block: atrule.block = self.visit(atrule.block) return atrule def visit_supports(self, node): condition = node.condition self.result += 1 node.condition = self.visit(condition) self.result -= 1 value = condition.first() if len(condition.nodes) == 1 and value.node_name == "string": node.condition = value.string node.block = self.visit(node.block) return node def visit_if(self, node): block = self.get_current_block() negate = node.negate self.result += 1 ok = self.visit(node.cond).first().to_boolean() self.result -= 1 node.block.scope = (hasattr(node.block, "has_media") and node.block.has_media()) # evaluate body ret = None if negate: # unless if ok.is_false(): ret = self.visit(node.block) else: # if if ok.is_true(): ret = self.visit(node.block) # else elif node.elses: for b in node.elses: # else if if hasattr(b, "cond") and b.cond: b.block.scope = b.block.has_media() self.result += 1 cond = self.visit(b.cond).first().to_boolean() self.result -= 1 if cond.is_true(): ret = self.visit(b.block) break # else: else: b.scope = b.has_media() ret = self.visit(b) # mixin conditional statements within # a selector group or at-rule # fixme: make this pythonistic! if (ret and node.postfix is None and hasattr(block, "node") and block.node and block.node.node_name in ["group", "atrule", "media", "supports", "keyframes"]): self.mixin(ret.nodes, block) return null if ret: return ret return null def visit_extend(self, extend, id=None): block = self.get_current_block() if block.node.node_name != "group": block = self.closest_group() for selector in extend.selectors: c = selector.clone() # todo: this is really bad; refactor this some_object = { "selector": self.interpolate(c).strip(), "optional": selector.optional, "lineno": c.lineno, "column": c.column, } block.node.extends.append(some_object) return null def invoke(self, body, stack=None, filename=None): if filename: self.paths.append(str(Path(filename).parent)) if self.result: ret = self.eval(body.nodes) if stack: self.stack.pop() else: body = self.visit(body) if stack: self.stack.pop() self.mixin(body.nodes, self.get_current_block()) ret = null if filename: self.paths.pop() return ret def mixin(self, nodes, block): if len(nodes) == 0: return None head = block.nodes[:block.index] tail = block.nodes[block.index + 1:] self._mixin(nodes, head, block) block.index = 0 block.mixin = True head.extend(tail) block.nodes = head # todo: rewrite this; this is not Python >:-( def _mixin(self, items, dest, block): for item in items: checked = False media_passed = False if item.node_name == "returnnode": return elif item.node_name == "block": checked = True self._mixin(item.nodes, dest, block) elif item.node_name == "media": # fix link to the parent block parent_node = item.block.parent.node if parent_node and parent_node.node_name != "call": item.block.parent = block media_passed = True if media_passed or item.node_name == "property": value = None if hasattr(item, "expr"): value = item.expr # prevent `block` mixin recursion if (hasattr(item, "literal") and item.literal and hasattr(value, "first") and value.first().node_name == "block"): value = unwrap(value) value.nodes[0] = Literal( "block", lineno=self.parser.lineno, column=self.parser.column, ) if not checked: dest.append(item) def mixin_node(self, node): node = self.visit(node.first()) if node.node_name == "objectnode": self.mixin_object(node) return null elif node.node_name in ["block", "atblock"]: self.mixin(node.nodes, self.get_current_block()) return null def mixin_object(self, object: ObjectNode): s = f"$block {object.to_block()}" p = Parser(s, utils.merge({"root": Root()}, self.options)) try: block = p.parse() except StilusError as e: e.filename = self.filename e.lineno = p.lexer.lineno e.column = p.lexer.column e.input = s raise e block.parent = self.root block.scope = False ret = self.visit(block) values = ret.first().nodes for value in values: if value.block: self.mixin(value.block.nodes, self.get_current_block()) break def eval(self, vals=None): if vals is None: return null def update(node): do_visit = True skip_next = False if node.node_name == "if": if node.block.node_name != "block": node = self.visit(node) do_visit = False skip_next = True if not skip_next and node.node_name in ["if", "each", "block"]: node = self.visit(node) if hasattr(node, "nodes") and node.nodes: node = self.eval(node.nodes) do_visit = False if do_visit: node = self.visit(node) return node try: nodes = [update(node) for node in vals] except ReturnNode as rn: return rn.expression return nodes[-1] if nodes else null # todo: fix this; this is really bad def invoke_builtin(self, fn, args): """ :param fn: :param args: :return: """ # map arguments to first node # providing a nicer js api for # built-in functions. Functions may specify that # they wish to accept full expressions # via raw_bifs if hasattr(fn, "__name__") and fn.__name__ in raw_bifs: ret = args.nodes else: ret = [] # fit in the args to the functions sig = inspect.signature(fn) i = 0 # remove the last parameter (the evaluator) first keys = [key for key in sig.parameters.keys()][:-1] for key in keys: param = sig.parameters.get(key) # handle the *args parameters if param.name == "args": while i < len(args.nodes): ret.append(utils.unwrap(args.nodes[i].first())) i += 1 # regular parameters elif param.name in args.map.keys(): # in the map ret.append(args.map[param.name].first()) else: # then in the nodes all_arguments = [] for nodes in args.nodes: all_arguments.extend(unwrap(nodes)) if i < len(unwrap(all_arguments)): ret.append(unwrap(all_arguments)[i].first()) i += 1 # else: assume remaining parameters are not required # invoke builtin function body = utils.coerce( fn(*ret, evaluator=self), False, lineno=self.parser.lineno, column=self.parser.column, ) # Always wrapping allows js functions # to return several values with a single # Expression node expr = Expression() expr.append(body) body = expr return self.invoke(body) def visit_import(self, imported): literal = False self.result += 1 path = self.visit(imported.path).first() node_name = "require" if imported.once else "import" self.result -= 1 log.debug(f"import {path}") # url() passed if hasattr(path, "function_name") and path.function_name == "url": if hasattr(imported, "once") and imported.once: raise StilusError("You cannot @require a url") return imported # ensure string if not hasattr(path, "string"): raise StilusError(f"@{node_name} string expected") name = path = path.string # absolute URL or hash m = re.match(r"(?:url\s*\(\s*)?[\'\"]?(?:#|(?:https?:)?\/\/)", path, re.I) if m: if imported.once: raise StilusError("You cannot @require a url") return imported # literal if path.endswith(".css") or '.css"' in path: literal = True if not imported.once and not self.include_css: return imported # support optional .styl if not literal and not path.endswith(".styl"): path += ".styl" # lookup found = utils.find(path, self.paths, self.filename) if not found: found = utils.lookup_index(name, self.paths, self.filename, parser=self.parser) # throw if import failed if not found: raise TypeError(f"failed to locate @{node_name} file in {path}" f" {self.paths}") block = Block(None, None, lineno=self.parser.lineno, column=self.parser.column) for f in found: block.append( self.import_file( imported, f, literal, lineno=self.parser.lineno, column=self.parser.column, )) return block def lookup_property(self, name): i = len(self.stack) index = self.get_current_block().index top = i while i > 0: i -= 1 block = self.stack[i].block if (block and hasattr(block, "node") and block.node.node_name in [ "group", "function", "if", "each", "atrule", "media", "atblock", "call", ]): nodes = block.nodes if i + 1 == top: while index > 0: index -= 1 if self.property == nodes[index]: continue other = self.interpolate(nodes[index]) if name == other: return nodes[index].clone() else: length = len(nodes) while length > 0: length -= 1 if ("property" != nodes[length].node_name or self.property == nodes[length]): continue other = self.interpolate(nodes[length]) if name == other: return nodes[length].clone() return null def closest_block(self): for stack in reversed(self.stack): block = stack.block if (hasattr(block, "node") and block.node and block.node.node_name in [ "group", "keyframes", "atrule", "atblock", "media", "call" ]): return block # todo: create warning when entering here return None def closest_group(self): for s in self.stack: b = s.block if hasattr(b, "node") and b.node and b.node.node_name == "group": return b def selector_stack(self): stack = [] for s in self.stack: b = s.block if hasattr(b, "node") and b.node and b.node.node_name == "group": for s in b.node.nodes: if not s.value: s.value = self.interpolate(s) stack.append(b.node.nodes) return stack def property_expression(self, prop, name): expr = Expression(lineno=self.parser.lineno, column=self.parser.column) value = prop.expr.clone(None) # name expr.append( String(prop.name, lineno=self.parser.lineno, column=self.parser.column)) # replace cyclic call with __CALL__ def replace(node): if (node.node_name == "call" and hasattr(node, "function_name") and name == node.function_name): return Literal( "__CALL__", lineno=self.parser.lineno, column=self.parser.column, ) if hasattr(node, "nodes") and node.nodes: node.nodes = [replace(n) for n in node.nodes] return node replace(value) expr.append(value) return expr def lookup(self, name): """ Lookup `name`, with support for JavaScript functions, and BIFs. :param name: :return: """ if self.ignore_colors and name in colors: return val = self.stack.lookup(name) if val is not None: # fixme: implement __len__() return utils.unwrap(val) else: return self.lookup_function(name) def interpolate(self, node): is_selector = "selector" == node.node_name def to_string(node): if node.node_name == "ident": return node.name elif node.node_name == "function": return node.function_name elif node.node_name in ["literal", "string"]: if (self.prefix and not node.prefixed and not hasattr(node.value, "node_name")): node.value = re.sub(r"\.", f".{self.prefix}", node.value) node.prefixed = True return node.value elif node.node_name == "unit": # interpolation inside keyframes if "%" == node.type: return f"{int(node.value)}%" else: return f"{int(node.value)}" elif node.node_name == "member": return to_string(self.visit(node)) elif node.node_name == "expression": # prevent cyclic 'selector()' calls if (self.calling and "selector" in self.calling and self._selector): return self._selector self.result += 1 ret = to_string(self.visit(node).first()) self.result -= 1 if is_selector: self._selector = ret return ret if node and hasattr(node, "segments"): s = "" for segment in node.segments: s += str(to_string(segment)) return s # return ''.join(map(to_string, node.segments)) else: return to_string(node) def lookup_function(self, name): function = self.functions.get(name, self.bifs.get(name, None)) if function: log.debug(f"Lookup function for {name} " f"returned: {function}.") return Function( name, function, lineno=self.parser.lineno, column=self.parser.column, ) else: log.debug(f"No function found for {name}.") return None def is_defined(self, node): if "ident" == node.node_name: return Boolean(self.lookup(node.name)) else: raise ParseError(f'invalid "is defined" ' f"check on non-variable {node}") def cast(self, expr: Expression): return Unit(expr.first().value, expr.nodes[1].name) def castable(self, expr: Expression): return (len(expr.nodes) == 2 and expr.first().node_name == "unit" and expr.nodes[1] and hasattr(expr.nodes[1], "name") and expr.nodes[1].name in units) def warn(self, message): if not self.warnings: return msg = f"Warning: {message}" print(msg) log.info(msg) def get_current_scope(self): return self.stack.current_frame().scope() def get_current_frame(self): return self.stack.current_frame() def get_current_block(self): current_frame = self.get_current_frame() if current_frame: b = current_frame.block return b return None def set_current_block(self, block): current_frame = self.get_current_frame() if current_frame: current_frame.block = block else: raise TypeError def unvendorize(self, prop: str): for vendor in self.vendors(): if vendor != "official": vendor = f"-{vendor}-" if vendor in prop: return prop.replace(vendor, "") return prop def literal_call(self, call): call.args = self.visit(call.args) return call def invoke_function(self, fn, args, content): block = Block( fn.block.parent, None, lineno=self.parser.lineno, column=self.parser.column, ) # clone the function body to prevent mutation of subsequent calls body = fn.block.clone(block) mixin_block = self.stack.current_frame().block # new block scope self.stack.append(Frame(block)) scope = self.get_current_scope() # normalize arguments if args.node_name != "arguments": expr = Expression() expr.append(args) args = Arguments.from_expression(expr) # arguments scope.add(Ident("arguments", args)) # mixin scope introspection bn = mixin_block.node_name if self.result: scope.add(Ident("mixin", false)) else: scope.add( Ident( "mixin", String( bn, lineno=self.parser.lineno, column=self.parser.column, ), )) # current property if self.property: prop = self.property_expression(self.property, fn.function_name) scope.add( Ident( "current-property", prop, lineno=self.parser.lineno, column=self.parser.column, )) else: scope.add( Ident( "current-property", null, lineno=self.parser.lineno, column=self.parser.column, )) # current call stack expr = Expression(lineno=self.parser.lineno, column=self.parser.column) for call in reversed(self.calling[:-1]): expr.append( Literal(call, lineno=self.parser.lineno, column=self.parser.column)) scope.add( Ident( "called-from", expr, lineno=self.parser.lineno, column=self.parser.column, )) # inject arguments as locals # todo: rewrite this! i = 0 for index, node in enumerate(fn.params.nodes): # rest param support if node.rest: node.value = Expression(lineno=self.parser.lineno, column=self.parser.column) for n in args.nodes[i:]: node.value.append(n) node.value.preserve = True node.value.is_list = args.is_list else: # argument default support # in dict? arg = args.map.get(node.name) # next node? if not arg: if hasattr(args, "nodes") and i < len(args.nodes): arg = args.nodes[i] i += 1 node = node.clone() if arg is not None: if hasattr(arg, "is_empty") and arg.is_empty(): args.nodes[i - 1] = self.visit(node) else: node.value = arg else: args.append(node.value) # required argument not satisfied if not node.value: raise TypeError(f'argument "{node}" required for {fn}') scope.add(node) # mixin block if content: scope.add(Ident("block", content, True)) # invoke return self.invoke(body, True, fn.filename)
def test_frame_creation(): frame = Frame(Block(Ident("hello"), Ident("there"))) assert frame.scope() == Scope() # assert frame.block == Block(Ident('hello'), Ident('there')) assert frame.parent is None