--- /dev/null
+import ast
+import textwrap
+import os
+
+from lib.utils import ConfigAST, ConfigASTVisitor
+from .common import NodeProperty, ConfigNodeError, ValueTypeConstrain
+from .nodes import GroupNode, TermNode
+from .sanitiser import TreeSanitiser
+
+class NodeBuilder(ConfigASTVisitor):
+ def __init__(self, env):
+ super().__init__()
+
+ self.__env = env
+ self.__level = []
+ self.__noncfg_astns = []
+
+ def __pop_and_merge(self):
+ if len(self.__level) == 0:
+ return
+
+ if len(self.__level) == 1:
+ self.__level.pop()
+ return
+
+ node = self.__level.pop()
+ prev = self.__level[-1]
+
+ assert isinstance(prev, GroupNode)
+
+ prev.add_child(node)
+
+ def __push(self, cfg_node):
+ self.__level.append(cfg_node)
+
+ def __check_literal_expr(self, node):
+ return (
+ isinstance(node, ast.Expr)
+ and isinstance(node.value, ast.Constant)
+ and isinstance(node.value.value, str)
+ )
+
+ def _visit_fndef(self, node):
+ if hasattr(node, "__builtin"):
+ self.__noncfg_astns.append(node)
+ return
+
+ cfgn_type = TermNode
+ if not node.returns:
+ cfgn_type = GroupNode
+
+ cfgn = cfgn_type(self.__env, self.current_file(), node.name)
+
+ self.__push(cfgn)
+
+ try:
+ for decor in node.decorator_list:
+ cfgn.apply_decorator(decor)
+
+ astns = []
+ help_text = ""
+ for sub in node.body:
+ if isinstance(sub, ast.FunctionDef):
+ self._visit_fndef(sub)
+ continue
+
+ if self.__check_literal_expr(sub):
+ help_text += sub.value.value
+ continue
+
+ astns.append(sub)
+
+ NodeProperty.Token[cfgn] = node
+ NodeProperty.HelpText[cfgn] = textwrap.dedent(help_text)
+
+ if cfgn_type is TermNode:
+ NodeProperty.Type[cfgn] = ValueTypeConstrain(cfgn, node.returns)
+
+ astns.append(ast.Pass())
+ cfgn.set_node_body(astns)
+
+ self.__env.register_node(cfgn)
+
+ except ConfigNodeError as e:
+ raise e
+ except Exception as e:
+ msg = e.args[0] if len(e.args) > 0 else type(e).__name__
+ raise cfgn.config_error(msg)
+
+ self.__pop_and_merge()
+
+ def visit(self, node):
+ super().visit(node)
+
+ if isinstance(node, ast.Module):
+ return
+
+ if not isinstance(node, ast.FunctionDef):
+ self.__noncfg_astns.append(node)
+
+ @staticmethod
+ def build(env, rootfile):
+ if not os.path.exists(rootfile):
+ print(f"warning: config file '{rootfile}' not detected, skipped")
+ return
+
+ build = NodeBuilder(env)
+ ast = ConfigAST(rootfile)
+
+ env.reset()
+ ast.visit(TreeSanitiser())
+ ast.visit(build)
+
+ for node in env.nodes():
+ node.apply_node_body()
+
+ env.set_exec_context(build.__noncfg_astns)
+ env.relocate_children()
\ No newline at end of file