1. Juri Pakaste
  2. nsstringgen


nsstringgen / nsstringgen / NSStringFromEnumGenerator.py

"""Generate NSStringFromEnumName functions for Objective-C given
an enum input.

The main entry point is the funs_from_file function.

from collections import namedtuple, OrderedDict
from functools import reduce
from tempfile import NamedTemporaryFile
import os.path

from nsstringgen.clang import cindex

class NSGException(Exception):
    """Exception class for displaying errors to the user."""

EnumValue = namedtuple("EnumValue", "name, value")

class ParsedEnum(object):
    """A parsed enumeration, with typedef_name telling typedeffed name if
    one exists."""
    def __init__(self, name, typedef_name, values = None):
        self.name = name if name is not None else ""
        self.typedef_name = typedef_name if typedef_name is not None else ""
        self.values = values if values is not None else []

    def preferred_name(self):
        if self.typedef_name:
            return self.typedef_name
        return self.name

    def __repr__(self):
        return "ParsedEnum({0}, {1}, {2})".format(
            repr(self.name), repr(self.typedef_name), self.values)

def print_tree(node, depth = 0):
    """Stringify a cursor tree for debugging."""
    indent = depth * "  "
    defn = node.get_definition()
    r = ["{0}node:          {1}".format(indent, node),
         "{0}spelling:      {1}".format(indent, node.spelling),
         "{0}displayname:   {1}".format(indent, node.displayname),
         "{0}kind:          {1}".format(indent, node.kind),
         "{0}type:          {1}".format(indent, node.type.kind),
         "{0}is_definition: {1}".format(indent, node.is_definition()),
         "{0}extent:        {1}".format(indent, node.extent),
         "{0}definition:    {1} {2} ({3})".format(
            defn.kind if defn else None,
            "Same" if defn is not None and node == defn else "Not same"),
    for c in node.get_children():
        r.extend(print_tree(c, depth + 1))
    return r

class FileReader(object):
    def __init__(self, size):
        self.size = size
        self.files = OrderedDict()
        self.fixed_files = {}

    def insert_content(self, sourcefile, content):
        self.fixed_files[os.path.abspath(sourcefile)] = content

    def read_extent(self, sourcefile, extent):
        sfpath = os.path.abspath(sourcefile)
        content = self.fixed_files.get(sfpath)
        if not content:
            content = self.files.get(sfpath)
            if not content:
                with open(sfpath) as fo:
                    content = fo.read()
                self.files[sfpath] = content
                # refresh LRU order
                del self.files[sfpath]
                self.files[sfpath] = content
            while len(self.files) > self.size:
                self.files.popitem(last = False)
        return content[extent.start.offset:extent.end.offset]

def handle_enum(node, typedef_name, file_reader):
    # sys.stdout.write("\n".join(print_tree(node)) + "\n")

    pe = ParsedEnum(node.spelling, typedef_name)
    for c in node.get_children():
        if c.kind == cindex.CursorKind.ENUM_CONSTANT_DECL:
            name = c.spelling
            value = None

            childlist = list(c.get_children())
            if len(childlist) > 0:
                vbytes = file_reader.read_extent(
                    c.location.file.name, childlist[0].extent)
                value = vbytes

            pe.values.append(EnumValue(name, value))
    return pe

def create_finder(path, file_reader):
    ppath = os.path.abspath(path)

    def find_enums(node, parents = ()):
        enums = []

        # we get nil files at the start for some reason
        if (node.location.file is not None and
            os.path.abspath(node.location.file.name) != ppath):
            return []
            kind = node.kind
        except ValueError:
            # a cursor kind not recognized by cindex.py
            kind = cindex.CursorKind.UNEXPOSED_DECL
        # plain enum. see if the parent was a typedef and if so, use its name.
        if kind == cindex.CursorKind.ENUM_DECL:
            typedef_name = None
            if (len(parents) > 0 and
                parents[-1].kind == cindex.CursorKind.TYPEDEF_DECL):

                typedef_name = parents[-1].spelling
            enums.append(handle_enum(node, typedef_name, file_reader))
        # typeref inside a typedecl pointing to an enum we can find
        # with get_definition()
        elif (kind == cindex.CursorKind.TYPE_REF and
              node.type.kind == cindex.TypeKind.ENUM and
              len(parents) > 0
              and parents[-1].kind == cindex.CursorKind.TYPEDEF_DECL):
            for c in node.get_children():
                enums.extend(find_enums(c, parents + (node,)))
        return enums
    return find_enums

def create_case(enumvalue):
    if enumvalue.value is None:
        return "case {0}: return @\"{0}\";".format(enumvalue.name)
        return ("case {0}: return [NSString stringWithFormat:@\""
                "{0} (%d)\", {1}];".format(enumvalue.name, enumvalue.value))

def create_fun(parsed_enum):
    name = parsed_enum.preferred_name

    if parsed_enum.typedef_name:
        typename = name
        typename = "enum {0}".format(parsed_enum.name)

    decl = "NSString* NSStringFrom{0}({1} v)".format(name, typename)
    ext_decl = "extern {0};".format(decl)

    defn = "{0} {{".format(decl)
    switchstart = "  switch (v) {"
    cases = ["    {0}".format(create_case(ev)) for ev in parsed_enum.values]
    switchend = "  }"
    funend = "}"
    return "\n".join(
        (ext_decl, defn, switchstart, "\n".join(cases), switchend, funend))

def create_mask_fun(parsed_enum):
    name = parsed_enum.preferred_name

    if parsed_enum.typedef_name:
        typename = name
        typename = "enum {0}".format(parsed_enum.name)

    decl = "NSString* NSStringFrom{0}({1} v)".format(name, typename)
    ext_decl = "extern {0};".format(decl)
    defn = "{0} {{".format(decl)

    lines = [ext_decl, defn]
    lines.append("  NSMutableArray *bits = [NSMutableArray array];")
    for ev in parsed_enum.values:
        check = "  if (v & {0}) [bits addObject:@\"{0}\"];".format(ev.name)
    lines.append("  return [bits componentsJoinedByString:@\" | \"];")
    return "\n".join(lines)

def create_includeargs(includes):
    includepairs = [["-include", inc] for inc in includes] if includes else []
    includeargs = sum(includepairs, [])
    return includeargs

def funs_from_file(fname, includes, mask, contents = None):
    index = cindex.Index.create()
    includeargs = create_includeargs(includes)
    unsaved = []
    if contents:
        # create a dummy file if we get contents, because seems
        # libclang is not happy with a name like "-"
        ntemp = NamedTemporaryFile(suffix = ".h")
        fname = ntemp.name
        unsaved = [[fname, contents]]
    tu = index.parse(fname,
                     args = ["-x", "objective-c"] + includeargs,
                     unsaved_files = unsaved)
    if not tu:
        raise NSGException(
            "Unable to parse input {0} for unknown reason".format(fname))
    if tu.diagnostics:
        msg = "\n".join([str(d) for d in tu.diagnostics])
        raise NSGException(msg)

    file_reader = FileReader(10)
    for ufname, content in unsaved:
        file_reader.insert_content(ufname, content)

    finder = create_finder(fname, file_reader)
    enums = finder(tu.cursor)
    creator = create_mask_fun if mask else create_fun
    return [creator(e) for e in enums if e.preferred_name]