nsstringgen / nsstringgen / NSStringFromEnumGenerator.py

#!/usr/bin/python

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

import clang.cindex

class NSGException(Exception):
    pass

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

class ParsedEnum(object):
    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 []

    @property
    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):
    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(indent, defn, 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
            else:
                # 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 == clang.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 []
        try:
            kind = node.kind
        except ValueError:
            # a cursor kind not recognized by cindex.py
            kind = clang.cindex.CursorKind.UNEXPOSED_DECL
        # plain enum. see if the parent was a typedef and if so, use its name.
        if kind == clang.cindex.CursorKind.ENUM_DECL:
            typedef_name = None
            if len(parents) and parents[-1].kind == clang.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 == clang.cindex.CursorKind.TYPE_REF and node.type.kind == clang.cindex.TypeKind.ENUM and len(parents) > 0 and parents[-1].kind == clang.cindex.CursorKind.TYPEDEF_DECL:
            enums.append(handle_enum(node.get_definition(), parents[-1].spelling, file_reader))
        else:
            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)
    else:
        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
    else:
        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
    else:
        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(check)
    lines.append("  return [bits componentsJoinedByString:@\" | \"];")
    lines.append("}")
    return "\n".join(lines)



def funs_from_file(f, includes, mask, contents = None):
    index = clang.cindex.Index.create()
    includeargs = reduce(lambda acc, e: acc + e, [["-include", inc] for inc in includes], []) if includes else []
    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")
        f = ntemp.name
        unsaved = [[f, contents]]
    tu = index.parse(f, args = ["-x", "objective-c"] + includeargs, unsaved_files = unsaved)
    if not tu:
        raise NSGException("Unable to parse input {0} for unknown reason".format(f))
    if tu.diagnostics:
        msg = "\n".join([str(d) for d in tu.diagnostics])
        raise NSGException(msg)

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

    finder = create_finder(f, 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]

if __name__ == '__main__':
    for fun in funs_from_file(sys.argv[1]):
        sys.stdout.write(fun)
        sys.stdout.write("\n")
Tip: Filter by directory path e.g. /media app.js to search for public/media/app.js.
Tip: Use camelCasing e.g. ProjME to search for ProjectModifiedEvent.java.
Tip: Filter by extension type e.g. /repo .js to search for all .js files in the /repo directory.
Tip: Separate your search with spaces e.g. /ssh pom.xml to search for src/ssh/pom.xml.
Tip: Use ↑ and ↓ arrow keys to navigate and return to view the file.
Tip: You can also navigate files with Ctrl+j (next) and Ctrl+k (previous) and view the file with Ctrl+o.
Tip: You can also navigate files with Alt+j (next) and Alt+k (previous) and view the file with Alt+o.