1import os
2from clang.cindex import Config
3if 'CLANG_LIBRARY_PATH' in os.environ:
4    Config.set_library_path(os.environ['CLANG_LIBRARY_PATH'])
5
6from contextlib import contextmanager
7import gc
8import os
9import sys
10import tempfile
11import unittest
12
13from clang.cindex import CursorKind
14from clang.cindex import Cursor
15from clang.cindex import File
16from clang.cindex import Index
17from clang.cindex import SourceLocation
18from clang.cindex import SourceRange
19from clang.cindex import TranslationUnitSaveError
20from clang.cindex import TranslationUnitLoadError
21from clang.cindex import TranslationUnit
22from .util import get_cursor
23from .util import get_tu
24from .util import skip_if_no_fspath
25from .util import str_to_path
26
27
28kInputsDir = os.path.join(os.path.dirname(__file__), 'INPUTS')
29
30
31@contextmanager
32def save_tu(tu):
33    """Convenience API to save a TranslationUnit to a file.
34
35    Returns the filename it was saved to.
36    """
37    with tempfile.NamedTemporaryFile() as t:
38        tu.save(t.name)
39        yield t.name
40
41
42@contextmanager
43def save_tu_pathlike(tu):
44    """Convenience API to save a TranslationUnit to a file.
45
46    Returns the filename it was saved to.
47    """
48    with tempfile.NamedTemporaryFile() as t:
49        tu.save(str_to_path(t.name))
50        yield t.name
51
52
53class TestTranslationUnit(unittest.TestCase):
54    def test_spelling(self):
55        path = os.path.join(kInputsDir, 'hello.cpp')
56        tu = TranslationUnit.from_source(path)
57        self.assertEqual(tu.spelling, path)
58
59    def test_cursor(self):
60        path = os.path.join(kInputsDir, 'hello.cpp')
61        tu = get_tu(path)
62        c = tu.cursor
63        self.assertIsInstance(c, Cursor)
64        self.assertIs(c.kind, CursorKind.TRANSLATION_UNIT)
65
66    def test_parse_arguments(self):
67        path = os.path.join(kInputsDir, 'parse_arguments.c')
68        tu = TranslationUnit.from_source(path, ['-DDECL_ONE=hello', '-DDECL_TWO=hi'])
69        spellings = [c.spelling for c in tu.cursor.get_children()]
70        self.assertEqual(spellings[-2], 'hello')
71        self.assertEqual(spellings[-1], 'hi')
72
73    def test_reparse_arguments(self):
74        path = os.path.join(kInputsDir, 'parse_arguments.c')
75        tu = TranslationUnit.from_source(path, ['-DDECL_ONE=hello', '-DDECL_TWO=hi'])
76        tu.reparse()
77        spellings = [c.spelling for c in tu.cursor.get_children()]
78        self.assertEqual(spellings[-2], 'hello')
79        self.assertEqual(spellings[-1], 'hi')
80
81    def test_unsaved_files(self):
82        tu = TranslationUnit.from_source('fake.c', ['-I./'], unsaved_files = [
83                ('fake.c', """
84#include "fake.h"
85int x;
86int SOME_DEFINE;
87"""),
88                ('./fake.h', """
89#define SOME_DEFINE y
90""")
91                ])
92        spellings = [c.spelling for c in tu.cursor.get_children()]
93        self.assertEqual(spellings[-2], 'x')
94        self.assertEqual(spellings[-1], 'y')
95
96    def test_unsaved_files_2(self):
97        if sys.version_info.major >= 3:
98            from io import StringIO
99        else:
100            from io import BytesIO as StringIO
101        tu = TranslationUnit.from_source('fake.c', unsaved_files = [
102                ('fake.c', StringIO('int x;'))])
103        spellings = [c.spelling for c in tu.cursor.get_children()]
104        self.assertEqual(spellings[-1], 'x')
105
106    @skip_if_no_fspath
107    def test_from_source_accepts_pathlike(self):
108        tu = TranslationUnit.from_source(str_to_path('fake.c'), ['-Iincludes'], unsaved_files = [
109                (str_to_path('fake.c'), """
110#include "fake.h"
111    int x;
112    int SOME_DEFINE;
113    """),
114                    (str_to_path('includes/fake.h'), """
115#define SOME_DEFINE y
116    """)
117                ])
118        spellings = [c.spelling for c in tu.cursor.get_children()]
119        self.assertEqual(spellings[-2], 'x')
120        self.assertEqual(spellings[-1], 'y')
121
122    def assert_normpaths_equal(self, path1, path2):
123        """ Compares two paths for equality after normalizing them with
124            os.path.normpath
125        """
126        self.assertEqual(os.path.normpath(path1),
127                         os.path.normpath(path2))
128
129    def test_includes(self):
130        def eq(expected, actual):
131            if not actual.is_input_file:
132                self.assert_normpaths_equal(expected[0], actual.source.name)
133                self.assert_normpaths_equal(expected[1], actual.include.name)
134            else:
135                self.assert_normpaths_equal(expected[1], actual.include.name)
136
137        src = os.path.join(kInputsDir, 'include.cpp')
138        h1 = os.path.join(kInputsDir, "header1.h")
139        h2 = os.path.join(kInputsDir, "header2.h")
140        h3 = os.path.join(kInputsDir, "header3.h")
141        inc = [(src, h1), (h1, h3), (src, h2), (h2, h3)]
142
143        tu = TranslationUnit.from_source(src)
144        for i in zip(inc, tu.get_includes()):
145            eq(i[0], i[1])
146
147    def test_inclusion_directive(self):
148        src = os.path.join(kInputsDir, 'include.cpp')
149        h1 = os.path.join(kInputsDir, "header1.h")
150        h2 = os.path.join(kInputsDir, "header2.h")
151        h3 = os.path.join(kInputsDir, "header3.h")
152        inc = [h1, h3, h2, h3, h1]
153
154        tu = TranslationUnit.from_source(src, options=TranslationUnit.PARSE_DETAILED_PROCESSING_RECORD)
155        inclusion_directive_files = [c.get_included_file().name for c in tu.cursor.get_children() if c.kind == CursorKind.INCLUSION_DIRECTIVE]
156        for i in zip(inc, inclusion_directive_files):
157            self.assert_normpaths_equal(i[0], i[1])
158
159    def test_save(self):
160        """Ensure TranslationUnit.save() works."""
161
162        tu = get_tu('int foo();')
163
164        with save_tu(tu) as path:
165            self.assertTrue(os.path.exists(path))
166            self.assertGreater(os.path.getsize(path), 0)
167
168    @skip_if_no_fspath
169    def test_save_pathlike(self):
170        """Ensure TranslationUnit.save() works with PathLike filename."""
171
172        tu = get_tu('int foo();')
173
174        with save_tu_pathlike(tu) as path:
175            self.assertTrue(os.path.exists(path))
176            self.assertGreater(os.path.getsize(path), 0)
177
178    def test_save_translation_errors(self):
179        """Ensure that saving to an invalid directory raises."""
180
181        tu = get_tu('int foo();')
182
183        path = '/does/not/exist/llvm-test.ast'
184        self.assertFalse(os.path.exists(os.path.dirname(path)))
185
186        with self.assertRaises(TranslationUnitSaveError) as cm:
187            tu.save(path)
188        ex = cm.exception
189        expected = TranslationUnitSaveError.ERROR_UNKNOWN
190        self.assertEqual(ex.save_error, expected)
191
192    def test_load(self):
193        """Ensure TranslationUnits can be constructed from saved files."""
194
195        tu = get_tu('int foo();')
196        self.assertEqual(len(tu.diagnostics), 0)
197        with save_tu(tu) as path:
198            self.assertTrue(os.path.exists(path))
199            self.assertGreater(os.path.getsize(path), 0)
200
201            tu2 = TranslationUnit.from_ast_file(filename=path)
202            self.assertEqual(len(tu2.diagnostics), 0)
203
204            foo = get_cursor(tu2, 'foo')
205            self.assertIsNotNone(foo)
206
207            # Just in case there is an open file descriptor somewhere.
208            del tu2
209
210    @skip_if_no_fspath
211    def test_load_pathlike(self):
212        """Ensure TranslationUnits can be constructed from saved files -
213        PathLike variant."""
214        tu = get_tu('int foo();')
215        self.assertEqual(len(tu.diagnostics), 0)
216        with save_tu(tu) as path:
217            tu2 = TranslationUnit.from_ast_file(filename=str_to_path(path))
218            self.assertEqual(len(tu2.diagnostics), 0)
219
220            foo = get_cursor(tu2, 'foo')
221            self.assertIsNotNone(foo)
222
223            # Just in case there is an open file descriptor somewhere.
224            del tu2
225
226    def test_index_parse(self):
227        path = os.path.join(kInputsDir, 'hello.cpp')
228        index = Index.create()
229        tu = index.parse(path)
230        self.assertIsInstance(tu, TranslationUnit)
231
232    def test_get_file(self):
233        """Ensure tu.get_file() works appropriately."""
234
235        tu = get_tu('int foo();')
236
237        f = tu.get_file('t.c')
238        self.assertIsInstance(f, File)
239        self.assertEqual(f.name, 't.c')
240
241        with self.assertRaises(Exception):
242            f = tu.get_file('foobar.cpp')
243
244    @skip_if_no_fspath
245    def test_get_file_pathlike(self):
246        """Ensure tu.get_file() works appropriately with PathLike filenames."""
247
248        tu = get_tu('int foo();')
249
250        f = tu.get_file(str_to_path('t.c'))
251        self.assertIsInstance(f, File)
252        self.assertEqual(f.name, 't.c')
253
254        with self.assertRaises(Exception):
255            f = tu.get_file(str_to_path('foobar.cpp'))
256
257    def test_get_source_location(self):
258        """Ensure tu.get_source_location() works."""
259
260        tu = get_tu('int foo();')
261
262        location = tu.get_location('t.c', 2)
263        self.assertIsInstance(location, SourceLocation)
264        self.assertEqual(location.offset, 2)
265        self.assertEqual(location.file.name, 't.c')
266
267        location = tu.get_location('t.c', (1, 3))
268        self.assertIsInstance(location, SourceLocation)
269        self.assertEqual(location.line, 1)
270        self.assertEqual(location.column, 3)
271        self.assertEqual(location.file.name, 't.c')
272
273    def test_get_source_range(self):
274        """Ensure tu.get_source_range() works."""
275
276        tu = get_tu('int foo();')
277
278        r = tu.get_extent('t.c', (1,4))
279        self.assertIsInstance(r, SourceRange)
280        self.assertEqual(r.start.offset, 1)
281        self.assertEqual(r.end.offset, 4)
282        self.assertEqual(r.start.file.name, 't.c')
283        self.assertEqual(r.end.file.name, 't.c')
284
285        r = tu.get_extent('t.c', ((1,2), (1,3)))
286        self.assertIsInstance(r, SourceRange)
287        self.assertEqual(r.start.line, 1)
288        self.assertEqual(r.start.column, 2)
289        self.assertEqual(r.end.line, 1)
290        self.assertEqual(r.end.column, 3)
291        self.assertEqual(r.start.file.name, 't.c')
292        self.assertEqual(r.end.file.name, 't.c')
293
294        start = tu.get_location('t.c', 0)
295        end = tu.get_location('t.c', 5)
296
297        r = tu.get_extent('t.c', (start, end))
298        self.assertIsInstance(r, SourceRange)
299        self.assertEqual(r.start.offset, 0)
300        self.assertEqual(r.end.offset, 5)
301        self.assertEqual(r.start.file.name, 't.c')
302        self.assertEqual(r.end.file.name, 't.c')
303
304    def test_get_tokens_gc(self):
305        """Ensures get_tokens() works properly with garbage collection."""
306
307        tu = get_tu('int foo();')
308        r = tu.get_extent('t.c', (0, 10))
309        tokens = list(tu.get_tokens(extent=r))
310
311        self.assertEqual(tokens[0].spelling, 'int')
312        gc.collect()
313        self.assertEqual(tokens[0].spelling, 'int')
314
315        del tokens[1]
316        gc.collect()
317        self.assertEqual(tokens[0].spelling, 'int')
318
319        # May trigger segfault if we don't do our job properly.
320        del tokens
321        gc.collect()
322        gc.collect() # Just in case.
323
324    def test_fail_from_source(self):
325        path = os.path.join(kInputsDir, 'non-existent.cpp')
326        try:
327            tu = TranslationUnit.from_source(path)
328        except TranslationUnitLoadError:
329            tu = None
330        self.assertEqual(tu, None)
331
332    def test_fail_from_ast_file(self):
333        path = os.path.join(kInputsDir, 'non-existent.ast')
334        try:
335            tu = TranslationUnit.from_ast_file(path)
336        except TranslationUnitLoadError:
337            tu = None
338        self.assertEqual(tu, None)
339