예제 #1
0
    def __init__(self, atok, lino, node, origin_file):
        """
        Initialize a new tree node instance.

        Arguments:
            atok -- asttokens object containing the tokens for the AST
            lino - asttokens LineNumber object containing the conversion for offsets to line numbers
            node -- Single raw node produced by the Python AST parser.
            origin_file {string} -- Relative path to the source file.

        """
        self.node = node

        start, end = atok.get_text_range(node)
        start_line, start_char = LineNumbers.offset_to_line(lino, start)
        end_line, end_char = LineNumbers.offset_to_line(lino, end)
        self.origin = NodeOrigin(origin_file, start_line, end_line)

        # Check if this type of node can have docstring.
        can_have_docstring = node.__class__ in [ast.ClassDef, ast.FunctionDef]

        # HACK: Ignore useless context-related children.
        # This should greatly reduce the total number of nodes.
        self.children = [
            TreeNode(atok, lino, n, origin_file)
            for n in ast.iter_child_nodes(node)
            if n.__class__ not in _IGNORE_CLASSES and
            # Ignore docstrings in class and function definitions.
            (not can_have_docstring or not isinstance(n, ast.Expr)
             or not isinstance(n.value, ast.Str))
        ]

        self.weight = 1 + sum([c.weight for c in self.children])

        # Name nodes are handled in a special way.
        if isinstance(node, ast.Name):
            self.value = f"Name('{node.id}')"
            self.names = [node.id]

        else:
            # Class name if the node has children, AST dump if it does not.
            self.value = node.__class__.__name__ if self.children else self.dump(
            )

            self.names = []
            for c in self.children:
                self.names.extend(c.names)

        # These values are set externally after all nodes are parsed
        # during the node tree flattening process.
        self.index = None
        self.parent_index = None
        self.child_indices = []
def _get_tree_node_from_file(file_path, repo_path):
    """
    Parse a TreeNode representing the module in the specified file.

    Arguments:
        file_path {string} -- Path of file to parse the TreeNode from.

    Returns:
        TreeNode -- TreeNode parsed from the specified file.

    """
    atok = asttokens.ASTTokens(_read_whole_file(file_path), True)
    root = atok.tree
    lino = LineNumbers(atok.get_text(root))
    return TreeNode(atok, lino, root, relpath(file_path, repo_path))
예제 #3
0
def _parse_docstring(
    source: str,
    docstring: str,
    invalid_fields: Tuple,
    params: Optional[Tuple] = None,
    return_length: int = 0,
) -> dict:

    natspec: dict = {}
    if params is None:
        params = tuple()

    line_no = LineNumbers(source)
    start = source.index(docstring)

    translate_map = {
        "return": "returns",
        "dev": "details",
        "param": "params",
    }

    pattern = r"(?:^|\n)\s*@(\S+)\s*([\s\S]*?)(?=\n\s*@\S|\s*$)"

    for match in re.finditer(pattern, docstring):
        tag, value = match.groups()
        err_args = (source, *line_no.offset_to_line(start + match.start(1)))

        if tag not in SINGLE_FIELDS + PARAM_FIELDS:
            raise NatSpecSyntaxException(f"Unknown NatSpec field '@{tag}'",
                                         *err_args)
        if tag in invalid_fields:
            raise NatSpecSyntaxException(
                f"'@{tag}' is not a valid field for this docstring", *err_args)

        if not value or value.startswith("@"):
            raise NatSpecSyntaxException(
                f"No description given for tag '@{tag}'", *err_args)

        if tag not in PARAM_FIELDS:
            if tag in natspec:
                raise NatSpecSyntaxException(
                    f"Duplicate NatSpec field '@{tag}'", *err_args)
            natspec[translate_map.get(tag, tag)] = " ".join(value.split())
            continue

        tag = translate_map.get(tag, tag)
        natspec.setdefault(tag, {})

        if tag == "params":
            try:
                key, value = value.split(maxsplit=1)
            except ValueError as exc:
                raise NatSpecSyntaxException(
                    f"No description given for parameter '{value}'",
                    *err_args) from exc
            if key not in params:
                raise NatSpecSyntaxException(
                    f"Method has no parameter '{key}'", *err_args)

        elif tag == "returns":
            if not return_length:
                raise NatSpecSyntaxException(
                    "Method does not return any values", *err_args)
            if len(natspec["returns"]) >= return_length:
                raise NatSpecSyntaxException(
                    "Number of documented return values exceeds actual number",
                    *err_args,
                )
            key = f"_{len(natspec['returns'])}"

        if key in natspec[tag]:
            raise NatSpecSyntaxException(
                f"Parameter '{key}' documented more than once", *err_args)
        natspec[tag][key] = " ".join(value.split())

    if not natspec:
        natspec["notice"] = " ".join(docstring.split())
    elif not docstring.strip().startswith("@"):
        raise NatSpecSyntaxException(
            "NatSpec docstring opens with untagged comment",
            source,
            *line_no.offset_to_line(start),
        )

    return natspec