Skip to content

Commit 1384ff8

Browse files
committed
Merge branch 'hotfix/v1.8.6'
2 parents b632d7e + fab1af3 commit 1384ff8

File tree

7 files changed

+200
-13
lines changed

7 files changed

+200
-13
lines changed

CHANGELOG.md

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,13 @@
11
Changes
22
=======
33

4+
Version 1.8.6
5+
-------------
6+
7+
1. Fix _HAS_TR1=0 definition for msvc9 (#72)
8+
9+
2. Fix possible infinite recursion in ```find_noncopyable_vars()``` (#71)
10+
411
Version 1.8.5
512
-------------
613

pygccxml/__init__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -40,4 +40,4 @@
4040
# TODO:
4141
# 1. Add "explicit" property for constructors
4242

43-
__version__ = '1.8.5'
43+
__version__ = '1.8.6'

pygccxml/declarations/type_traits_classes.py

Lines changed: 59 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -152,12 +152,18 @@ def find_copy_constructor(type_):
152152
return None
153153

154154

155-
def find_noncopyable_vars(type_):
155+
def find_noncopyable_vars(type_, already_visited_cls_vars=None):
156156
"""
157157
Returns list of all `noncopyable` variables.
158158
159+
If an already_visited_cls_vars list is provided as argument, the returned
160+
list will not contain these variables. This list will be extended with
161+
whatever variables pointing to classes have been found.
162+
159163
Args:
160164
type_ (declarations.class_t): the class to be searched.
165+
already_visited_cls_vars (list): optional list of vars that should not
166+
be checked a second time, to prevent infinite recursions.
161167
162168
Returns:
163169
list: list of all `noncopyable` variables.
@@ -172,6 +178,9 @@ def find_noncopyable_vars(type_):
172178
allow_empty=True)
173179
noncopyable_vars = []
174180

181+
if already_visited_cls_vars is None:
182+
already_visited_cls_vars = []
183+
175184
message = (
176185
"__contains_noncopyable_mem_var - %s - TRUE - " +
177186
"contains const member variable")
@@ -196,7 +205,13 @@ def find_noncopyable_vars(type_):
196205
if class_traits.is_my_case(type_):
197206

198207
cls = class_traits.get_declaration(type_)
199-
if is_noncopyable(cls):
208+
209+
# Exclude classes that have already been visited.
210+
if cls in already_visited_cls_vars:
211+
continue
212+
already_visited_cls_vars.append(cls)
213+
214+
if is_noncopyable(cls, already_visited_cls_vars):
200215
logger.debug((message + " - class that is not copyable")
201216
% type_.decl_string)
202217
noncopyable_vars.append(mvar)
@@ -632,8 +647,20 @@ def is_convertible(source, target):
632647
return __is_convertible_t(source, target).is_convertible()
633648

634649

635-
def __is_noncopyable_single(class_):
636-
"""implementation details"""
650+
def __is_noncopyable_single(class_, already_visited_cls_vars=None):
651+
"""
652+
Implementation detail.
653+
654+
Checks if the class is non copyable, without considering the base classes.
655+
656+
Args:
657+
class_ (declarations.class_t): the class to be checked
658+
already_visited_cls_vars (list): optional list of vars that should not
659+
be checked a second time, to prevent infinite recursions.
660+
661+
Returns:
662+
bool: if the class is non copyable
663+
"""
637664
# It is not enough to check base classes, we should also to check
638665
# member variables.
639666
logger = utils.loggers.cxx_parser
@@ -650,7 +677,11 @@ def __is_noncopyable_single(class_):
650677
" public destructor: yes"])
651678
logger.debug(msg)
652679
return False
653-
if find_noncopyable_vars(class_):
680+
681+
if already_visited_cls_vars is None:
682+
already_visited_cls_vars = []
683+
684+
if find_noncopyable_vars(class_, already_visited_cls_vars):
654685
logger.debug(
655686
("__is_noncopyable_single(TRUE) - %s - contains noncopyable " +
656687
"members") % class_.decl_string)
@@ -662,9 +693,22 @@ def __is_noncopyable_single(class_):
662693
return False
663694

664695

665-
def is_noncopyable(class_):
666-
"""returns True, if class is noncopyable, False otherwise"""
696+
def is_noncopyable(class_, already_visited_cls_vars=None):
697+
"""
698+
Checks if class is non copyable
699+
700+
Args:
701+
class_ (declarations.class_t): the class to be checked
702+
already_visited_cls_vars (list): optional list of vars that should not
703+
be checked a second time, to prevent infinite recursions.
704+
In general you can ignore this argument, it is mainly used during
705+
recursive calls of is_noncopyable() done by pygccxml.
706+
707+
Returns:
708+
bool: if the class is non copyable
709+
"""
667710
logger = utils.loggers.cxx_parser
711+
668712
class_decl = class_traits.get_declaration(class_)
669713

670714
true_header = "is_noncopyable(TRUE) - %s - " % class_.decl_string
@@ -683,6 +727,9 @@ def is_noncopyable(class_):
683727
if copy_ and copy_.access_type == 'public' and not copy_.is_artificial:
684728
return False
685729

730+
if already_visited_cls_vars is None:
731+
already_visited_cls_vars = []
732+
686733
for base_desc in class_decl.recursive_bases:
687734
assert isinstance(base_desc, class_declaration.hierarchy_info_t)
688735

@@ -700,13 +747,15 @@ def is_noncopyable(class_):
700747
true_header +
701748
"there is private copy constructor")
702749
return True
703-
elif __is_noncopyable_single(base_desc.related_class):
750+
elif __is_noncopyable_single(
751+
base_desc.related_class, already_visited_cls_vars):
704752
logger.debug(
705753
true_header +
706754
"__is_noncopyable_single returned True")
707755
return True
708756

709-
if __is_noncopyable_single(base_desc.related_class):
757+
if __is_noncopyable_single(
758+
base_desc.related_class, already_visited_cls_vars):
710759
logger.debug(
711760
true_header +
712761
"__is_noncopyable_single returned True")
@@ -722,7 +771,7 @@ def is_noncopyable(class_):
722771
logger.debug(true_header + "has private destructor")
723772
return True
724773
else:
725-
return __is_noncopyable_single(class_decl)
774+
return __is_noncopyable_single(class_decl, already_visited_cls_vars)
726775

727776

728777
def is_unary_operator(oper):

pygccxml/parser/source_reader.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -183,7 +183,7 @@ def __create_command_line_castxml(self, source_file, xmlfile):
183183
cmd.append('--castxml-cc-msvc ' +
184184
'"%s"' % self.__config.compiler_path)
185185
if 'msvc9' == self.__config.compiler:
186-
cmd.append('-D"_HAS_TR1=0"')
186+
cmd.append('"-D_HAS_TR1=0"')
187187
else:
188188

189189
# On mac or linux, use gcc or clang (the flag is the same)
Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
// Copyright 2014-2016 Insight Software Consortium.
2+
// Copyright 2004-2008 Roman Yakovenko.
3+
// Distributed under the Boost Software License, Version 1.0.
4+
// See http://www.boost.org/LICENSE_1_0.txt
5+
6+
#include <sstream>
7+
8+
// Demonstration of the real problem with basic c++
9+
namespace Test1 {
10+
11+
// Forward declaration
12+
class Base2;
13+
14+
// Base 1, with pointer to Base2
15+
class Base1 {
16+
private:
17+
Base1();
18+
19+
protected:
20+
Base2* aBasePtr2;
21+
};
22+
23+
// Base 2, child class of Base1
24+
class Base2: public Base1 {
25+
private:
26+
Base2();
27+
};
28+
29+
// Child class of Base2
30+
// Holds a pointer to Base2
31+
class Child: public Base2 {
32+
private:
33+
Child();
34+
35+
protected:
36+
Base2* aBasePtr2;
37+
};
38+
39+
}
40+
41+
// Real-life test with std::istream where this happened
42+
namespace Test2 {
43+
44+
class FileStreamDataStream {
45+
public:
46+
FileStreamDataStream(const std::istream* s) {}
47+
48+
protected:
49+
std::istream* mInStream;
50+
};
51+
}
52+

unittests/test_all.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -76,6 +76,7 @@
7676
import test_function_pointer
7777
import test_directory_cache
7878
import test_config
79+
import test_non_copyable_recursive
7980

8081
testers = [
8182
# , demangled_tester # failing right now
@@ -144,7 +145,8 @@
144145
test_pattern_parser,
145146
test_function_pointer,
146147
test_directory_cache,
147-
test_config
148+
test_config,
149+
test_non_copyable_recursive
148150
]
149151

150152
if 'posix' in os.name:
Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
1+
# Copyright 2014-2016 Insight Software Consortium.
2+
# Copyright 2004-2009 Roman Yakovenko.
3+
# Distributed under the Boost Software License, Version 1.0.
4+
# See http://www.boost.org/LICENSE_1_0.txt
5+
6+
import unittest
7+
import parser_test_case
8+
9+
from pygccxml import parser
10+
from pygccxml import declarations
11+
12+
13+
class Test(parser_test_case.parser_test_case_t):
14+
15+
def __init__(self, *args):
16+
parser_test_case.parser_test_case_t.__init__(self, *args)
17+
self.header = "test_non_copyable_recursive.hpp"
18+
19+
def test_infinite_recursion_base_classes(self):
20+
"""
21+
Test find_noncopyable_vars
22+
23+
See #71
24+
25+
find_noncopyable_vars was throwing:
26+
RuntimeError: maximum recursion depth exceeded while
27+
calling a Python object
28+
"""
29+
decls = parser.parse([self.header], self.config)
30+
global_ns = declarations.get_global_namespace(decls)
31+
32+
# Description of the problem (before the fix):
33+
# find_noncopyable_vars (on Child class) looks up the variables,
34+
# and finds aBasePtr2 (a pointer to the Base2 class).
35+
# Then it looks recursively at the base classes of Base2, and finds
36+
# Base1. Then, it looks up the variables from Base, to check if Base1
37+
# is non copyable. It finds another aBasePtr2 variable, which leads to
38+
# a new check of Base2; this recurses infinitely.
39+
test_ns = global_ns.namespace('Test1')
40+
cls = test_ns.class_('Child')
41+
declarations.type_traits_classes.find_noncopyable_vars(cls)
42+
self.assertTrue(declarations.type_traits_classes.is_noncopyable(cls))
43+
44+
def test_infinite_recursion_sstream(self):
45+
"""
46+
Test find_noncopyable_vars
47+
48+
See #71
49+
50+
find_noncopyable_vars was throwing:
51+
RuntimeError: maximum recursion depth exceeded while
52+
calling a Python object
53+
"""
54+
decls = parser.parse([self.header], self.config)
55+
global_ns = declarations.get_global_namespace(decls)
56+
57+
# Real life example of the bug. This leads to a similar error,
58+
# but the situation is more complex as there are multiple
59+
# classes that are related the one to the others
60+
# (basic_istream, basic_ios, ios_base, ...)
61+
test_ns = global_ns.namespace('Test2')
62+
cls = test_ns.class_('FileStreamDataStream')
63+
declarations.type_traits_classes.find_noncopyable_vars(cls)
64+
self.assertFalse(declarations.type_traits_classes.is_noncopyable(cls))
65+
66+
67+
def create_suite():
68+
suite = unittest.TestSuite()
69+
suite.addTest(unittest.makeSuite(Test))
70+
return suite
71+
72+
73+
def run_suite():
74+
unittest.TextTestRunner(verbosity=2).run(create_suite())
75+
76+
if __name__ == "__main__":
77+
run_suite()

0 commit comments

Comments
 (0)