Skip to content

Commit 5131e8e

Browse files
committed
Cleanup
1 parent b699bd7 commit 5131e8e

4 files changed

Lines changed: 399 additions & 46 deletions

File tree

src/xyz/cppmodel.py

Lines changed: 256 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,256 @@
1+
from typing import Any
2+
from typing import List
3+
from typing import Optional
4+
5+
from clang.cindex import AccessSpecifier as _AccessSpecifier
6+
from clang.cindex import Cursor
7+
from clang.cindex import CursorKind as _CursorKind
8+
from clang.cindex import Diagnostic
9+
from clang.cindex import ExceptionSpecificationKind as _ExceptionSpecificationKind
10+
from clang.cindex import SourceLocation
11+
from clang.cindex import TranslationUnit
12+
from clang.cindex import TypeKind as _TypeKind
13+
14+
# Suppress type checking warnings for clang.cindex kinds.
15+
AccessSpecifier: Any = _AccessSpecifier
16+
CursorKind: Any = _CursorKind
17+
ExceptionSpecificationKind: Any = _ExceptionSpecificationKind
18+
TypeKind: Any = _TypeKind
19+
20+
21+
def _get_annotations(cursor: Cursor) -> List[str]:
22+
return [c.displayname for c in cursor.get_children() if c.kind == CursorKind.ANNOTATE_ATTR]
23+
24+
25+
class Unmodelled:
26+
def __init__(self, cursor: Cursor):
27+
self.location: SourceLocation = cursor.location
28+
self.name: str = cursor.displayname
29+
30+
def __repr__(self) -> str:
31+
return "<xyz.cppmodel.Unmodelled {} {}>".format(self.name, self.location)
32+
33+
34+
class Type:
35+
def __init__(self, cindex_type):
36+
self.kind = cindex_type.kind
37+
self.name = cindex_type.spelling
38+
self.is_pointer: bool = self.kind == TypeKind.POINTER
39+
self.is_reference: bool = self.kind == TypeKind.LVALUEREFERENCE
40+
self.is_const: bool = cindex_type.is_const_qualified()
41+
if self.is_pointer or self.is_reference:
42+
self.pointee: Optional[Type] = Type(cindex_type.get_pointee())
43+
else:
44+
self.pointee = None
45+
46+
def __repr__(self) -> str:
47+
return "<xyz.cppmodel.Type {}>".format(self.name)
48+
49+
50+
class Member:
51+
def __init__(self, cursor: Cursor):
52+
self.type: Type = Type(cursor.type)
53+
self.name: str = cursor.spelling
54+
55+
def __repr__(self) -> str:
56+
return "<xyz.cppmodel.Member {} {}>".format(self.type, self.name)
57+
58+
59+
class FunctionArgument:
60+
def __init__(self, type: Type, name: Optional[str] = None):
61+
self.type: Type = type
62+
self.name: Optional[str] = name or None
63+
64+
def __repr__(self) -> str:
65+
if self.name is None:
66+
return "<xyz.cppmodel.FunctionArgument self.type.name>"
67+
return "<xyz.cppmodel.FunctionArgument {} {}>".format(self.type, self.name)
68+
69+
70+
class _Function:
71+
def __init__(self, cursor):
72+
self.name: str = cursor.spelling
73+
arguments: List[Optional[str]] = [str(x.spelling) or None for x in cursor.get_arguments()]
74+
argument_types: List[Type] = [Type(x) for x in cursor.type.argument_types()]
75+
self.is_noexcept: bool = cursor.exception_specification_kind == ExceptionSpecificationKind.BASIC_NOEXCEPT
76+
self.return_type: Type = Type(cursor.type.get_result())
77+
self.arguments: List[FunctionArgument] = []
78+
self.annotations: List[str] = _get_annotations(cursor)
79+
80+
for t, n in zip(argument_types, arguments, strict=False):
81+
self.arguments.append(FunctionArgument(t, n))
82+
83+
def __repr__(self) -> str:
84+
r = "{} {}({})".format(
85+
self.return_type.name,
86+
str(self.name),
87+
", ".join([a.type.name for a in self.arguments]),
88+
)
89+
if self.is_noexcept:
90+
r = r + " noexcept"
91+
return r
92+
93+
94+
class Function(_Function):
95+
def __init__(self, cursor, namespaces=[]):
96+
_Function.__init__(self, cursor)
97+
self.namespace: str = "::".join(namespaces)
98+
self.qualified_name: str = self.name
99+
if self.namespace:
100+
self.qualified_name = "::".join([self.namespace, self.name])
101+
102+
def __repr__(self) -> str:
103+
s = _Function.__repr__(self)
104+
return "<xyz.cppmodel.Function {}>".format(s)
105+
106+
def __eq__(self, f) -> bool:
107+
if self.name != f.name:
108+
return False
109+
if self.namespace != f.namespace:
110+
return False
111+
if len(self.arguments) != len(f.arguments):
112+
return False
113+
for x, fx in zip([arg.type for arg in self.arguments], [arg.type for arg in f.arguments], strict=False):
114+
if x.name != fx.name:
115+
return False
116+
return True
117+
118+
119+
class Method(_Function):
120+
def __init__(self, cursor):
121+
_Function.__init__(self, cursor)
122+
self.is_const: bool = cursor.is_const_method()
123+
self.is_virtual: bool = cursor.is_virtual_method()
124+
self.is_pure_virtual: bool = cursor.is_pure_virtual_method()
125+
self.is_public: bool = cursor.access_specifier == AccessSpecifier.PUBLIC
126+
127+
def __repr__(self) -> str:
128+
s = _Function.__repr__(self)
129+
if self.is_const:
130+
s = "{} const".format(s)
131+
if self.is_pure_virtual:
132+
s = "virtual {} = 0".format(s)
133+
elif self.is_virtual:
134+
s = "virtual {}".format(s)
135+
return "<xyz.cppmodel.Method {}>".format(s)
136+
137+
138+
class Class:
139+
def __init__(self, cursor: Cursor, namespaces: List[str]):
140+
self.name: str = cursor.spelling
141+
self.namespace: str = "::".join(namespaces)
142+
self.qualified_name: str = self.name
143+
if self.namespace:
144+
self.qualified_name = "::".join([self.namespace, self.name])
145+
self.constructors: List[Method] = []
146+
self.methods: List[Method] = []
147+
self.members: List[Member] = []
148+
self.annotations = _get_annotations(cursor)
149+
self.base_classes = []
150+
# FIXME: populate these fields with AST info
151+
self.source_file = str(cursor.location.file)
152+
self.source_line = int(cursor.location.line)
153+
self.source_column = int(cursor.location.column)
154+
155+
for c in cursor.get_children():
156+
if c.kind == CursorKind.CXX_METHOD and c.type.kind == TypeKind.FUNCTIONPROTO:
157+
self.methods.append(Method(c))
158+
elif c.kind == CursorKind.CONSTRUCTOR and c.type.kind == TypeKind.FUNCTIONPROTO:
159+
self.constructors.append(Method(c))
160+
elif c.kind == CursorKind.FIELD_DECL:
161+
self.members.append(Member(c))
162+
elif c.kind == CursorKind.CXX_BASE_SPECIFIER:
163+
self.base_classes.append(c.type.spelling)
164+
165+
def __repr__(self) -> str:
166+
return "<xyz.cppmodel.Class {}>".format(self.name)
167+
168+
169+
class Model:
170+
def __init__(self, translation_unit: TranslationUnit):
171+
"""Create a model from a translation unit."""
172+
self.filename: str = translation_unit.spelling
173+
self.functions: List[Function] = []
174+
self.classes: List[Class] = []
175+
self.unmodelled_nodes: List[Unmodelled] = []
176+
# Keep a reference to the translation unit to prevent it from being garbage collected.
177+
self.translation_unit: TranslationUnit = translation_unit
178+
179+
def is_error_in_current_file(diagnostic: Diagnostic) -> bool:
180+
if str(diagnostic.location.file) != str(translation_unit.spelling):
181+
return False
182+
if diagnostic.severity == Diagnostic.Error:
183+
return True
184+
if diagnostic.severity == Diagnostic.Fatal:
185+
return True
186+
return False
187+
188+
errors: List[Diagnostic] = [d for d in translation_unit.diagnostics if is_error_in_current_file(d)]
189+
if errors:
190+
joined_errors = "\n".join(str(e) for e in errors)
191+
raise ValueError(f"Errors in source file:{joined_errors}")
192+
193+
self._add_child_nodes(translation_unit.cursor, [])
194+
195+
def __repr__(self) -> str:
196+
return "<xyz.cppmodel.Model filename={}, classes={}, functions={}>".format(
197+
self.filename,
198+
[c.name for c in self.classes],
199+
[f.name for f in self.functions],
200+
)
201+
202+
def extend(self, translation_unit: TranslationUnit):
203+
# Extend an existing model with contents of a new translation unit.
204+
m = Model(translation_unit)
205+
# Check for duplicates and inconsistencies.
206+
for new_class in m.classes:
207+
is_new = True
208+
for old_class in self.classes:
209+
if new_class.qualified_name == old_class.qualified_name:
210+
if new_class.source_file != old_class.source_file:
211+
raise Exception(
212+
"Class {} is defined in multiple locations: {} {}".format(
213+
old_class.qualified_name,
214+
old_class.source_file,
215+
new_class.source_file,
216+
)
217+
)
218+
# Move on as there can only be one match
219+
is_new = False
220+
break
221+
222+
if is_new:
223+
self.classes.append(new_class)
224+
225+
# We only look at declarations for functions so won't raise exceptions
226+
for new_function in m.functions:
227+
is_new = True
228+
for old_function in self.functions:
229+
if new_function == old_function:
230+
is_new = False
231+
break
232+
if is_new:
233+
self.functions.append(new_function)
234+
235+
def _add_child_nodes(self, cursor: Any, namespaces: List[str] = []):
236+
namespaces = namespaces or []
237+
for c in cursor.get_children():
238+
try:
239+
c.kind
240+
except ValueError: # Handle unknown cursor kind
241+
# TODO(jbcoe): Fix cindex.py upstream to avoid needing to do this.
242+
continue
243+
if c.kind == CursorKind.CLASS_DECL or c.kind == CursorKind.STRUCT_DECL:
244+
if c.location.file.name == self.filename:
245+
self.classes.append(Class(c, namespaces))
246+
elif c.kind == CursorKind.FUNCTION_DECL and c.type.kind == TypeKind.FUNCTIONPROTO:
247+
if c.location.file.name == self.filename:
248+
self.functions.append(Function(c, namespaces))
249+
elif c.kind == CursorKind.NAMESPACE:
250+
child_namespaces = list(namespaces)
251+
child_namespaces.append(c.spelling)
252+
253+
self._add_child_nodes(c, child_namespaces)
254+
else:
255+
if c.location.file.name == self.filename:
256+
self.unmodelled_nodes.append(Unmodelled(c))

tests/test_cppmodel.py

Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,86 @@
1+
import pytest
2+
from clang.cindex import TranslationUnit
3+
4+
import xyz.cppmodel
5+
6+
COMPILER_ARGS = [
7+
"-x",
8+
"c++",
9+
"-std=c++20",
10+
"-I/Library/Developer/CommandLineTools/SDKs/MacOSX.sdk/usr/include/c++/v1",
11+
"-I/Library/Developer/CommandLineTools/usr/include",
12+
]
13+
14+
SOURCE = """\
15+
int z = 0;
16+
17+
struct __attribute__((annotate("A"))) A {
18+
int a;
19+
double b;
20+
char c[8];
21+
22+
__attribute__((annotate("foo"))) int foo(int);
23+
};
24+
25+
template <class T>
26+
class B {
27+
T t;
28+
T wibble(T);
29+
};
30+
31+
double bar(double);
32+
33+
int main() {}
34+
"""
35+
36+
37+
@pytest.fixture
38+
def model():
39+
tu = TranslationUnit.from_source(
40+
"sample.cc",
41+
COMPILER_ARGS,
42+
unsaved_files=[("sample.cc", SOURCE)],
43+
)
44+
return xyz.cppmodel.Model(tu)
45+
46+
47+
def test_filename(model):
48+
assert model.filename == "sample.cc"
49+
50+
51+
def test_functions(model):
52+
assert len(model.functions) == 2
53+
assert str(model.functions[0]) == "<xyz.cppmodel.Function double bar(double)>"
54+
assert str(model.functions[1]) == "<xyz.cppmodel.Function int main()>"
55+
56+
57+
def test_classes(model):
58+
assert len(model.classes) == 1
59+
assert str(model.classes[0]) == "<xyz.cppmodel.Class A>"
60+
61+
assert len(model.classes[0].annotations) == 1
62+
assert model.classes[0].annotations[0] == "A"
63+
64+
65+
def test_class_members(model):
66+
assert len(model.classes[0].members) == 3
67+
assert str(model.classes[0].members[0]) == "<xyz.cppmodel.Member <xyz.cppmodel.Type int> a>"
68+
assert str(model.classes[0].members[1]) == "<xyz.cppmodel.Member <xyz.cppmodel.Type double> b>"
69+
assert str(model.classes[0].members[2]) == "<xyz.cppmodel.Member <xyz.cppmodel.Type char[8]> c>"
70+
71+
assert len(model.classes[0].methods) == 1
72+
assert str(model.classes[0].methods[0]) == "<xyz.cppmodel.Method int foo(int)>"
73+
assert len(model.classes[0].methods[0].annotations) == 1
74+
assert model.classes[0].methods[0].annotations[0] == "foo"
75+
76+
77+
def test_unmodelled_nodes(model):
78+
assert len(model.unmodelled_nodes) == 2
79+
assert (
80+
str(model.unmodelled_nodes[0])
81+
== "<xyz.cppmodel.Unmodelled z <SourceLocation file 'sample.cc', line 1, column 5>>"
82+
)
83+
assert (
84+
str(model.unmodelled_nodes[1])
85+
== "<xyz.cppmodel.Unmodelled B<T> <SourceLocation file 'sample.cc', line 12, column 7>>"
86+
)
Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
import pytest
2+
from clang.cindex import TranslationUnit
3+
4+
import xyz.cppmodel
5+
6+
COMPILER_ARGS = [
7+
"-x",
8+
"c++",
9+
"-std=c++20",
10+
"-I/Library/Developer/CommandLineTools/SDKs/MacOSX.sdk/usr/include/c++/v1",
11+
"-I/Library/Developer/CommandLineTools/usr/include",
12+
]
13+
14+
15+
@pytest.mark.parametrize(
16+
"include",
17+
[
18+
"algorithm",
19+
"any",
20+
"array",
21+
"deque",
22+
"forward_list",
23+
"functional",
24+
"iterator",
25+
"list",
26+
"map",
27+
"memory",
28+
"numeric",
29+
"optional",
30+
"queue",
31+
"set",
32+
"stack",
33+
"string",
34+
"tuple",
35+
"type_traits",
36+
"unordered_map",
37+
"unordered_set",
38+
"utility",
39+
"variant",
40+
"vector",
41+
],
42+
)
43+
def test_include(include):
44+
source = f"#include <{include}>"
45+
tu = TranslationUnit.from_source(
46+
"t.cc",
47+
COMPILER_ARGS,
48+
unsaved_files=[("t.cc", source)],
49+
)
50+
51+
# This should not raise an exception.
52+
xyz.cppmodel.Model(tu)

0 commit comments

Comments
 (0)