Skip to content

Commit

Permalink
Introduce type renderer
Browse files Browse the repository at this point in the history
  • Loading branch information
theHolgi committed Mar 20, 2024
1 parent 7b0d938 commit d79bb49
Show file tree
Hide file tree
Showing 2 changed files with 120 additions and 47 deletions.
135 changes: 97 additions & 38 deletions hammocking/hammocking.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,11 +11,52 @@
from argparse import ArgumentParser
from pathlib import Path
from typing import List, Set, Union, Tuple, Iterator, Iterable, Optional
from clang.cindex import Index, TranslationUnit, Cursor, CursorKind, Config, TypeKind
from clang.cindex import Index, TranslationUnit, Cursor, CursorKind, Config, Type, TypeKind
from jinja2 import Environment, FileSystemLoader
import logging
import configparser


class RenderableType:
def __init__(self, t):
self.t = t

@staticmethod
def _collect_arguments(params) -> str:
# TODO: Merge with Function _collect_arguments
unnamed_index = 1
arguments = []
for param in params:
if not param.name:
param.name = 'unnamed' + str(unnamed_index)
unnamed_index = unnamed_index + 1
arguments.append(param.get_definition(True))

return ", ".join(arguments)

def render(self, name) -> str:
if self.t.kind == TypeKind.CONSTANTARRAY:
res = f"{name}[{self.t.get_array_size()}]"
element_type = RenderableType(self.t.get_array_element_type())
return element_type.render(res)
elif self.t.kind == TypeKind.POINTER and self.t.get_pointee().kind == TypeKind.FUNCTIONPROTO:
# param is of type function pointer
pt = self.t.get_pointee()
args = [arg for arg in pt.argument_types()]
return f"{pt.get_result().spelling} (*{name})({','.join(arg.spelling for arg in pt.argument_types())})"
else:
return self.t.spelling + " " + name

@property
def is_basic(self) -> bool:
if self.t.kind == TypeKind.CONSTANTARRAY:
return False
return True

@property
def spelling(self) -> str:
return self.t.spelling

class ConfigReader:
section = "hammocking"
configfile = Path(__file__).parent / (section + ".ini")
Expand Down Expand Up @@ -49,45 +90,66 @@ def _scan(self, items: Iterator[Tuple[str, str]]) -> None:


class Variable:
def __init__(self, type: str, name: str, size: int = 0) -> None:
self.type = type
self.name = name
self.size = size
def __init__(self, c: Cursor) -> None:
self.type = RenderableType(c.type)
self.name = c.spelling

def get_definition(self) -> str:
return f"{self.type} {self.name}" + (f"[{self.size}]" if self.size > 0 else "")
def get_definition(self, with_type: bool = True) -> str:
if with_type:
return self.type.render(self.name)
else:
return self.name

def __repr__(self) -> str:
return f"<{self.get_definition()}>"

class Function:
def __init__(self, type: str, name: str, params: List[Variable], is_variadic: bool = False) -> None:
self.type = type
self.name = name
self.params = params
self.is_variadic = is_variadic
def __init__(self, c: Cursor) -> None:
self.type = RenderableType(c.result_type)
self.name = c.spelling
self.params = [Variable(arg) for arg in c.get_arguments()]
self.is_variadic = c.type.is_function_variadic() if c.type.kind == TypeKind.FUNCTIONPROTO else False

def get_signature(self) -> str:
return f"{self.type} {self.name}({self._collect_arguments(True)}{', ...' if self.is_variadic else ''})"
"""
Return the function declaration form
"""
return f"{self.type.render(self.name)}({self._collect_arguments(True)}{', ...' if self.is_variadic else ''})"

def _collect_arguments(self, with_types: bool) -> str:
unnamed_index = 1
arguments = []
for param in self.params:
arg_name = param.name if param.name else 'unnamed' + str(unnamed_index)
unnamed_index = unnamed_index + 1
arg_type = param.type + ' ' if with_types else ''
arguments.append(f"{arg_type}{arg_name}")
if not param.name:
param.name = 'unnamed' + str(unnamed_index)
unnamed_index = unnamed_index + 1
arguments.append(param.get_definition(with_types))

return ", ".join(arguments)

def has_return_value(self) -> bool:
return self.type != "void"
return self.type.t.kind != TypeKind.VOID

@property
def return_type(self) -> str:
return self.type.spelling # rendering includes the name, which is not what the user wants here.

def get_call(self) -> str:
return f"{self.name}({self._collect_arguments(False)})"
"""
Return a piece of C code to call the function
"""
if self.is_variadic and False: # TODO
return "TODO"
else:
return f"{self.name}({self._collect_arguments(False)})"

def get_param_types(self) -> str:
param_types = ", ".join(f"{param.type}" for param in self.params)
param_types = ", ".join(f"{param.type.spelling}" for param in self.params)
return f"{param_types}"

def __repr__(self) -> str:
return f"<{self.type} {self.name} ()>"


class MockupWriter:

Expand All @@ -98,6 +160,7 @@ def __init__(self, mockup_style="gmock", suffix=None) -> None:
self.template_dir = f"{dirname(__file__)}/templates"
self.mockup_style = mockup_style
self.suffix = suffix or ""
self.logger = logging.getLogger("HammocKing")
self.environment = Environment(
loader=FileSystemLoader(f"{self.template_dir}/{self.mockup_style}"),
keep_trailing_newline=True,
Expand All @@ -112,15 +175,15 @@ def add_header(self, name: str) -> None:
if name not in self.headers:
self.headers.append(name)

def add_variable(self, type: str, name: str, size: int = 0) -> None:
def add_variable(self, c: Cursor) -> None:
"""Add a variable definition"""
logging.info(f"HammocKing: Create mockup for variable {name}")
self.variables.append(Variable(type, name, size))
self.logger.info(f"Create mockup for variable {c.spelling}")
self.variables.append(Variable(c))

def add_function(self, type: str, name: str, params: List[Tuple[str, str]] = [], is_variadic: bool = False) -> None:
def add_function(self, c: Cursor) -> None:
"""Add a variable definition"""
logging.info(f"Create mockup for function {name}")
self.functions.append(Function(type, name, [Variable(param[0], param[1]) for param in params], is_variadic))
self.logger.info(f"Create mockup for function {c.spelling}")
self.functions.append(Function(c))

def get_mockup(self, file: str) -> str:
return self.render(Path(file + '.j2'))
Expand Down Expand Up @@ -188,10 +251,12 @@ def parse(self, input: Union[Path, str]) -> None:
if issubclass(type(input), Path):
# Read a path
parseOpts["path"] = input
basepath = input.parent.absolute()
else:
# Interpret a string as content of the file
parseOpts["path"] = "~.c"
parseOpts["unsaved_files"] = [("~.c", input)]
basepath = Path.cwd()

self.logger.debug(f"Symbols to be mocked: {self.symbols}")
translation_unit = Index.create(excludeDecls=True).parse(**parseOpts)
Expand All @@ -205,20 +270,14 @@ def parse(self, input: Union[Path, str]) -> None:
self.logger.debug(f"Found {child.spelling} in {child.location.file}")
in_header = child.location.file.name != translation_unit.spelling
if in_header: # We found it in the Source itself. Better not include the whole source!
self.writer.add_header(str(child.location.file))
headerpath = child.location.file.name
if headerpath.startswith("./"): # Replace reference to current directory with CWD's path
headerpath = (basepath / headerpath[2:]).as_posix()
self.writer.add_header(headerpath)
if child.kind == CursorKind.VAR_DECL:
child_type_array_size = child.type.get_array_size()
if child_type_array_size > 0:
self.writer.add_variable(child.type.element_type.spelling, child.spelling, child_type_array_size)
else:
self.writer.add_variable(child.type.spelling, child.spelling)
self.writer.add_variable(child)
elif child.kind == CursorKind.FUNCTION_DECL:
self.writer.add_function(
child.type.get_result().spelling,
child.spelling,
[(arg.type.spelling, arg.spelling) for arg in child.get_arguments()],
is_variadic = child.type.is_function_variadic() if child.type.kind == TypeKind.FUNCTIONPROTO else False
)
self.writer.add_function(child)
else:
self.logger.warning(f"Unknown kind of symbol: {child.kind}")
self.symbols.remove(child.spelling)
Expand Down
32 changes: 23 additions & 9 deletions tests/hammocking_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,59 +9,73 @@
# Apply default config
ConfigReader()

def clang_parse(snippet: str):
parseOpts = {
"path": "~.c",
"unsaved_files": [("~.c", snippet)],
"options": TranslationUnit.PARSE_SKIP_FUNCTION_BODIES | TranslationUnit.PARSE_INCOMPLETE,
}
translation_unit = Index.create(excludeDecls=True).parse(**parseOpts)
return next(Hammock.iter_children(translation_unit.cursor))

class TestVariable:
def test_creation(self):
v = Variable("char", "x")
v = Variable(clang_parse("char x"))
assert v.name == "x"
assert v.type == "char"
assert v.get_definition() == "char x"

w = Variable("int", "my_array", 2)
w = Variable(clang_parse("int my_array[2]"))
assert w.name == "my_array"
assert w.type == "int"
assert w.get_definition() == "int my_array[2]"


class TestFunction:
def test_void_void(self):
f = Function(type="void", name="func", params=[])
f = Function(clang_parse("void func(void);"))
assert f.name == "func"
assert f.get_signature() == "void func()"
assert f.get_call() == "func()"
assert f.get_param_types() == ""
assert f.has_return_value() == False

def test_void_int(self):
f = Function(type="void", name="set", params=[Variable('int', 'a')])
f = Function(clang_parse("void set(int a);"))
assert f.name == "set"
assert f.get_signature() == "void set(int a)"
assert f.get_call() == "set(a)"
assert f.get_param_types() == "int"
assert f.has_return_value() == False

def test_int_void(self):
f = Function(type="int", name="get", params=[])
f = Function(clang_parse("int get(void);"))
assert f.name == "get"
assert f.get_signature() == "int get()"
assert f.get_call() == "get()"
assert f.get_param_types() == ""
assert f.has_return_value() == True

def test_void_int_double(self):
f = Function(type="void", name="set", params=[Variable('int', 'a'), Variable('double', 'b')])
f = Function(clang_parse("void set(int a, double b);"))
assert f.name == "set"
assert f.get_signature() == "void set(int a, double b)"
assert f.get_call() == "set(a, b)"
assert f.get_param_types() == "int, double"
# assert f.has_return_value() == False

def test_function_with_unnamed_arguments(self):
f = Function(type="float", name="my_func", params=[Variable('float', ''), Variable('float', '')])
f = Function(clang_parse("float my_func(float, float);"))
assert f.name == "my_func"
assert f.get_signature() == "float my_func(float unnamed1, float unnamed2)"
assert f.get_call() == "my_func(unnamed1, unnamed2)"
assert f.get_param_types() == "float, float"

def test_variadic_function(self):
f = Function(clang_parse("int printf_func(const char* fmt, ...);"))
assert f.name == "printf_func"
assert f.get_signature() == "int printf_func(const char * fmt, ...)"
assert f.get_call() == "printf_func(fmt)" # TODO
assert f.get_param_types() == "const char *" # ?


class TestMockupWriter:
def test_empty_templates(self):
Expand Down

0 comments on commit d79bb49

Please sign in to comment.