• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

trolldbois / ctypeslib / 4773970805

pending completion
4773970805

push

github

Loic Jaquemet
more linting

1274 of 1490 relevant lines covered (85.5%)

51.05 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

64.18
/ctypeslib/codegen/clangparser.py
1
"""clangparser - use clang to get preprocess a source code."""
2

3
import logging
4
import os
5
import collections
6

7
from clang.cindex import Index, TranslationUnit
8
from clang.cindex import TypeKind
9

10
from ctypeslib.codegen import cursorhandler
11
from ctypeslib.codegen import typedesc
12
from ctypeslib.codegen import typehandler
13
from ctypeslib.codegen import util
14
from ctypeslib.codegen.handler import DuplicateDefinitionException
15
from ctypeslib.codegen.handler import InvalidDefinitionError
16
from ctypeslib.codegen.handler import InvalidTranslationUnitException
17

18
log = logging.getLogger("clangparser")
19

20

21
class Clang_Parser:
22
    """
23
    Will parse libclang AST tree to create a representation of Types and
×
24
    different others source code objects objets as described in Typedesc.
×
25

26
    For each Declaration a declaration will be saved, and the type of that
×
27
    declaration will be cached and saved.
×
28
    """
29

30
    has_values = {
48✔
31
        "Enumeration",
12✔
32
        "Function",
12✔
33
        "FunctionType",
12✔
34
        "OperatorFunction",
12✔
35
        "Method",
12✔
36
        "Constructor",
12✔
37
        "Destructor",
12✔
38
        "OperatorMethod",
12✔
39
        "Converter",
12✔
40
    }
41

42
    # FIXME, macro definition __SIZEOF_DOUBLE__
43
    ctypes_typename = {
48✔
44
        TypeKind.VOID: "None",  # because ctypes.POINTER(None) == c_void_p
48✔
45
        TypeKind.BOOL: "c_bool",
48✔
46
        TypeKind.CHAR_U: "c_ubyte",  # ?? used for PADDING
48✔
47
        TypeKind.UCHAR: "c_ubyte",  # unsigned char
48✔
48
        TypeKind.CHAR16: "c_wchar",  # char16_t
48✔
49
        TypeKind.CHAR32: "c_wchar",  # char32_t
48✔
50
        TypeKind.USHORT: "c_ushort",
48✔
51
        TypeKind.UINT: "c_uint",
48✔
52
        TypeKind.ULONG: "TBD",
48✔
53
        TypeKind.ULONGLONG: "c_ulonglong",
48✔
54
        TypeKind.UINT128: "c_uint128",  # FIXME
48✔
55
        TypeKind.CHAR_S: "c_char",  # char
48✔
56
        TypeKind.SCHAR: "c_byte",  # signed char
48✔
57
        TypeKind.WCHAR: "c_wchar",
48✔
58
        TypeKind.SHORT: "c_short",
48✔
59
        TypeKind.INT: "c_int",
48✔
60
        TypeKind.LONG: "TBD",
48✔
61
        TypeKind.LONGLONG: "c_longlong",
48✔
62
        TypeKind.INT128: "c_int128",  # FIXME
48✔
63
        TypeKind.FLOAT: "c_float",
48✔
64
        TypeKind.DOUBLE: "c_double",
48✔
65
        TypeKind.LONGDOUBLE: "c_longdouble",
48✔
66
        TypeKind.POINTER: "POINTER_T",
48✔
67
        TypeKind.NULLPTR: "c_void_p",
48✔
68
    }
69

70
    def __init__(self, flags):
48✔
71
        self.all = collections.OrderedDict()
48✔
72
        # a shortcut to identify registered decl in cases of records
73
        self.all_set = set()
48✔
74
        self.cpp_data = {}
48✔
75
        self._unhandled = []
48✔
76
        self.fields = {}
48✔
77
        self.tu = None
48✔
78
        self.tu_options = None
48✔
79
        self.flags = flags
48✔
80
        self.ctypes_sizes = {}
48✔
81
        self.init_parsing_options()
48✔
82
        self.make_ctypes_convertor(flags)
48✔
83
        self.cursorkind_handler = cursorhandler.CursorHandler(self)
48✔
84
        self.typekind_handler = typehandler.TypeHandler(self)
48✔
85
        self.__filter_location = None
48✔
86
        self.__processed_location = set()
48✔
87

88
    def init_parsing_options(self):
48✔
89
        """Set the Translation Unit to skip functions bodies per default."""
90
        self.tu_options = TranslationUnit.PARSE_SKIP_FUNCTION_BODIES
12✔
91

92
    def activate_macros_parsing(self):
12✔
93
        """Activates the detailled code parsing options in the Translation
94
        Unit."""
95
        self.tu_options |= TranslationUnit.PARSE_DETAILED_PROCESSING_RECORD
60✔
96

97
    def activate_comment_parsing(self):
60✔
98
        """Activates the comment parsing options in the Translation Unit."""
99
        self.tu_options |= TranslationUnit.PARSE_INCLUDE_BRIEF_COMMENTS_IN_CODE_COMPLETION
100

101
    def deactivate_function_body_parsing(self):
102
        self.tu_options |= TranslationUnit.PARSE_SKIP_FUNCTION_BODIES
103

104
    def filter_location(self, src_files):
105
        self.__filter_location = [os.path.abspath(f) for f in src_files]
106

107
    def parse(self, filename):
108
        """
109
        . reads 1 file
×
110
        . if there is a compilation error, print a warning
×
111
        . get root cursor and recurse
×
112
        . for each STRUCT_DECL, register a new struct type
×
113
        . for each UNION_DECL, register a new union type
×
114
        . for each TYPEDEF_DECL, register a new alias/typdef to the underlying type
×
115
            - underlying type is cursor.type.get_declaration() for Record
×
116
        . for each VAR_DECL, register a Variable
×
117
        . for each TYPEREF ??
×
118
        """
×
119
        if os.path.abspath(filename) in self.__processed_location:
60✔
120
            return
×
121
        index = Index.create()
60✔
122
        translation_unit = index.parse(filename, self.flags, options=self.tu_options)
60✔
123
        if not translation_unit:
60✔
124
            log.warning("unable to load input")
×
125
            return
×
126
        self._parse_tu_diagnostics(translation_unit, filename)
60✔
127
        self.tu = translation_unit
60✔
128
        root = self.tu.cursor
60✔
129
        for node in root.get_children():
60✔
130
            self.start_element(node)
60✔
131
        return
60✔
132

133
    def parse_string(self, input_data, lang="c", all_warnings=False, flags=None):
60✔
134
        """Use this parser on a memory string/file, instead of a file on disk"""
135
        translation_unit = util.get_tu(input_data, lang, all_warnings, flags)
136
        self._parse_tu_diagnostics(translation_unit, "memory_input.c")
137
        self.tu = translation_unit
138
        root = self.tu.cursor
139
        for node in root.get_children():
140
            self.start_element(node)
141

142
    @staticmethod
143
    def _parse_tu_diagnostics(translation_unit, input_filename):
144
        if len(translation_unit.diagnostics) == 0:
145
            return
146
        errors = []
147
        for diagnostic in translation_unit.diagnostics:
148
            msg = (
149
                f"{diagnostic.spelling} ({diagnostic.location.file}:{diagnostic.location.line}:"
150
                f"{diagnostic.location.column}) during processing {input_filename}"
151
            )
152
            log.warning(msg)
153
            if diagnostic.severity > 2:
154
                errors.append(msg)
155
        if len(errors) > 0:
156
            log.warning("Source code has %d error. Please fix.", len(errors))
157
            # code.interact(local=locals())
158
            raise InvalidTranslationUnitException(errors[0])
159

160
    def start_element(self, node):
161
        """Recurses in children of this node"""
162
        if node is None:
60✔
163
            return None
×
164

165
        if self.__filter_location is not None:
60✔
166
            # dont even parse includes.
167
            # FIXME: go back on dependencies ?
168
            if node.location.file is None:
60✔
169
                return None
×
170
            filepath = os.path.abspath(node.location.file.name)
60✔
171
            if filepath not in self.__filter_location:
60✔
172
                if not filepath.startswith("/usr"):
×
173
                    log.debug("skipping include '%s'", filepath)
×
174
                return None
×
175
        # find and call the handler for this element
176
        log.debug(
60✔
177
            "%s:%d: Found a %s|%s|%s",
60✔
178
            node.location.file,
60✔
179
            node.location.line,
60✔
180
            node.kind.name,
60✔
181
            node.displayname,
60✔
182
            node.spelling,
60✔
183
        )
184
        # build stuff.
185
        try:
60✔
186
            stop_recurse = self.parse_cursor(node)
60✔
187
            if node.location.file is not None:
60✔
188
                filepath = os.path.abspath(node.location.file.name)
60✔
189
                self.__processed_location.add(filepath)
60✔
190
            # Signature of parse_cursor is:
191
            # if the fn returns True, do not recurse into children.
192
            # anything else will be ignored.
193
            if stop_recurse is not False:  # True:
60✔
194
                return None
60✔
195
            # if fn returns something, if this element has children, treat
196
            # them.
197
            for child in node.get_children():
60✔
198
                self.start_element(child)
×
199
        except InvalidDefinitionError:
60✔
200
            log.exception("Invalid definition")
×
201
            # if the definition is invalid
202
        # startElement returns None.
203
        return None
60✔
204

205
    def register(self, name, obj):
60✔
206
        """Registers an unique type description"""
207
        if (name, obj) in self.all_set:
208
            log.debug("register: %s already defined: %s", name, obj.name)
209
            return self.all[name]
210
        if name in self.all:
211
            if not isinstance(self.all[name], typedesc.Structure) or (self.all[name].members is not None):
212
                # code.interact(local=locals())
213
                raise DuplicateDefinitionException(
214
                    f"register: {name} which has a previous incompatible definition: {obj.name}"
215
                    f"\ndefined here: {obj.location}"
216
                    f"\npreviously defined here: {self.all[name].location}"
217
                )
218
            if isinstance(self.all[name], typedesc.Structure) and (self.all[name].members is None):
219
                return obj
220
        log.debug("register: %s ", name)
221
        self.all[name] = obj
222
        self.all_set.add((name, obj))
223
        return obj
224

225
    def get_registered(self, name):
226
        """Returns a registered type description"""
227
        return self.all[name]
60✔
228

229
    def is_registered(self, name):
60✔
230
        """Checks if a named type description is registered"""
231
        return name in self.all
232

233
    def remove_registered(self, name):
234
        """Removes a named type"""
235
        log.debug("Unregister %s", name)
60✔
236
        self.all_set.remove((name, self.all[name]))
60✔
237
        del self.all[name]
60✔
238

239
    def make_ctypes_convertor(self, _flags):
60✔
240
        """
241
        Fix clang types to ctypes conversion for this parsing instance.
242
        Some architecture dependent size types have to be changed if the target
243
        architecture is not the same as local
244
        """
245
        # NOTE: one could also use the __SIZEOF_x__ MACROs to obtain sizes.
246
        translation_unit = util.get_tu(
60✔
247
            """
248
typedef short short_t;
249
typedef int int_t;
250
typedef long long_t;
251
typedef long long longlong_t;
252
typedef float float_t;
253
typedef double double_t;
254
typedef long double longdouble_t;
255
typedef void* pointer_t;""",
12✔
256
            flags=_flags,
12✔
257
        )
258
        size = util.get_cursor(translation_unit, "short_t").type.get_size() * 8
12✔
259
        self.ctypes_typename[TypeKind.SHORT] = f"c_int{size:d}"
12✔
260
        self.ctypes_typename[TypeKind.USHORT] = f"c_uint{size:d}"
12✔
261
        self.ctypes_sizes[TypeKind.SHORT] = size
12✔
262
        self.ctypes_sizes[TypeKind.USHORT] = size
12✔
263

264
        size = util.get_cursor(translation_unit, "int_t").type.get_size() * 8
12✔
265
        self.ctypes_typename[TypeKind.INT] = f"c_int{size:d}"
12✔
266
        self.ctypes_typename[TypeKind.UINT] = f"c_uint{size:d}"
12✔
267
        self.ctypes_sizes[TypeKind.INT] = size
12✔
268
        self.ctypes_sizes[TypeKind.UINT] = size
12✔
269

270
        size = util.get_cursor(translation_unit, "long_t").type.get_size() * 8
12✔
271
        self.ctypes_typename[TypeKind.LONG] = f"c_int{size:d}"
12✔
272
        self.ctypes_typename[TypeKind.ULONG] = f"c_uint{size:d}"
12✔
273
        self.ctypes_sizes[TypeKind.LONG] = size
12✔
274
        self.ctypes_sizes[TypeKind.ULONG] = size
12✔
275

276
        size = util.get_cursor(translation_unit, "longlong_t").type.get_size() * 8
12✔
277
        self.ctypes_typename[TypeKind.LONGLONG] = f"c_int{size:d}"
12✔
278
        self.ctypes_typename[TypeKind.ULONGLONG] = f"c_uint{size:d}"
12✔
279
        self.ctypes_sizes[TypeKind.LONGLONG] = size
12✔
280
        self.ctypes_sizes[TypeKind.ULONGLONG] = size
12✔
281

282
        # FIXME : Float && http://en.wikipedia.org/wiki/Long_double
283
        size0 = util.get_cursor(translation_unit, "float_t").type.get_size() * 8
12✔
284
        size1 = util.get_cursor(translation_unit, "double_t").type.get_size() * 8
12✔
285
        size2 = util.get_cursor(translation_unit, "longdouble_t").type.get_size() * 8
12✔
286
        # 2014-01 stop generating crap.
287
        # 2015-01 reverse until better solution is found
288
        # the idea is that you cannot assume a c_double will be same format as a c_long_double.
289
        # at least this pass size TU
290
        if size1 != size2:
12✔
291
            self.ctypes_typename[TypeKind.LONGDOUBLE] = "c_long_double_t"
12✔
292
        else:
293
            self.ctypes_typename[TypeKind.LONGDOUBLE] = "c_double"
12✔
294

295
        self.ctypes_sizes[TypeKind.FLOAT] = size0
12✔
296
        self.ctypes_sizes[TypeKind.DOUBLE] = size1
12✔
297
        self.ctypes_sizes[TypeKind.LONGDOUBLE] = size2
12✔
298

299
        # save the target pointer size.
300
        size = util.get_cursor(translation_unit, "pointer_t").type.get_size() * 8
12✔
301
        self.ctypes_sizes[TypeKind.POINTER] = size
12✔
302
        self.ctypes_sizes[TypeKind.NULLPTR] = size
12✔
303

304
        log.debug(
12✔
305
            "ARCH sizes: long:%s longdouble:%s",
12✔
306
            self.ctypes_typename[TypeKind.LONG],
12✔
307
            self.ctypes_typename[TypeKind.LONGDOUBLE],
12✔
308
        )
309
        return
12✔
310

311
    def get_ctypes_name(self, typekind):
12✔
312
        return self.ctypes_typename[typekind]
12✔
313

314
    def get_ctypes_size(self, typekind):
12✔
315
        return self.ctypes_sizes[typekind]
12✔
316

317
    def parse_cursor(self, cursor):
12✔
318
        """Forward parsing calls to dedicated CursorKind Handlder"""
319
        return self.cursorkind_handler.parse_cursor(cursor)
48✔
320

321
    def parse_cursor_type(self, _cursor_type):
48✔
322
        """Forward parsing calls to dedicated TypeKind Handlder"""
323
        return self.typekind_handler.parse_cursor_type(_cursor_type)
12✔
324

325
    ###########################################################################
326

327
    ################
328

329
    def get_macros(self, text):
12✔
330
        if text is None:
12✔
331
            return
12✔
332
        text = "".join(text)
333
        # preprocessor definitions that look like macros with one or more
334
        # arguments
335
        for macro in text.splitlines():
336
            name, body = macro.split(None, 1)
337
            name, args = name.split("(", 1)
338
            args = f"({args}"
339
            self.all[name] = typedesc.Macro(name, args, body)
340

341
    def get_aliases(self, text, namespace):
12✔
342
        if text is None:
12✔
343
            return
12✔
344
        # preprocessor definitions that look like aliases:
345
        #  #define A B
346
        text = "".join(text)
347
        aliases = {}
348
        for alias in text.splitlines():
349
            name, value = alias.split(None, 1)
350
            alias = typedesc.Alias(name, value)
351
            aliases[name] = alias
352
            self.all[name] = alias
353

354
        for name, alias in aliases.items():
355
            value = alias.alias
356
            # the value should be either in namespace...
357
            if value in namespace:
358
                # set the type
359
                alias.typ = namespace[value]
360
            # or in aliases...
361
            elif value in aliases:
362
                alias.typ = aliases[value]
363
            # or unknown.
364
            else:
365
                # not known
366
                # print "skip %s = %s" % (name, value)
367
                pass
368

369
    def get_result(self):
12✔
370
        # all of these should register()
371
        interesting = (
372
            typedesc.Typedef,
12✔
373
            typedesc.Enumeration,
12✔
374
            typedesc.EnumValue,
12✔
375
            typedesc.Function,
12✔
376
            typedesc.Structure,
12✔
377
            typedesc.Union,
12✔
378
            typedesc.Variable,
12✔
379
            typedesc.Macro,
12✔
380
            typedesc.Alias,
12✔
381
            typedesc.FunctionType,
12✔
382
        )
383
        # typedesc.Field) #???
384

385
        self.get_macros(self.cpp_data.get("functions"))
12✔
386
        # fix all objects after that all are resolved
387
        remove = []
12✔
388
        for _id, _item in self.all.items():
12✔
389
            if _item is None:
12✔
390
                log.warning("ignoring %s", _id)
391
                continue
392
            location = getattr(_item, "location", None)
12✔
393
            # FIXME , why do we get different location types
394
            if location and hasattr(location, "file"):
12✔
395
                _item.location = location.file.name, location.line
396
                log.error("%s %s came in with a SourceLocation", _id, _item)
397
            elif location is None:
12✔
398
                # FIXME make this optional to be able to see internals
399
                # FIXME macro/alias are here
400
                log.warning("No source location in %s - ignoring", _id)
12✔
401
                remove.append(_id)
12✔
402

403
        for _x in remove:
12✔
404
            self.remove_registered(_x)
12✔
405

406
        # Now we can build the namespace.
407
        namespace = {}
12✔
408
        for i in self.all.values():
12✔
409
            if not isinstance(i, interesting):
12✔
410
                log.debug("ignoring %s", i)
411
                continue  # we don't want these
412
            name = getattr(i, "name", None)
12✔
413
            if name is not None:
12✔
414
                namespace[name] = i
12✔
415
        self.get_aliases(self.cpp_data.get("aliases"), namespace)
12✔
416

417
        result = []
12✔
418
        for i in self.all.values():
12✔
419
            if isinstance(i, interesting):
12✔
420
                result.append(i)
12✔
421

422
        log.debug("parsed items order: %s", result)
12✔
423
        return result
12✔
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2025 Coveralls, Inc