diff --git a/tests/parser-cases/name_collision.thrift b/tests/parser-cases/name_collision.thrift new file mode 100644 index 00000000..69df901d --- /dev/null +++ b/tests/parser-cases/name_collision.thrift @@ -0,0 +1,16 @@ +// Test case for when a local struct has the same name as an imported module +include "name_collision_imported.thrift" + +// Local struct with the same name as the imported module +struct name_collision_imported { + 1: required string local_field1 + 2: optional i32 local_field2 +} + +struct TestStruct { + // Should resolve to local struct 'name_collision_imported' + 1: required name_collision_imported localStruct + + // Should resolve to UserProfile from the imported module + 2: required name_collision_imported.UserProfile importedUserProfile +} diff --git a/tests/parser-cases/name_collision_imported.thrift b/tests/parser-cases/name_collision_imported.thrift new file mode 100644 index 00000000..482cda5b --- /dev/null +++ b/tests/parser-cases/name_collision_imported.thrift @@ -0,0 +1,6 @@ +// This file is imported and has a struct named UserProfile +struct UserProfile { + 1: required string user_id + 2: required string username + 3: optional string email +} diff --git a/tests/test_parser.py b/tests/test_parser.py index 809ebfc9..39976802 100644 --- a/tests/test_parser.py +++ b/tests/test_parser.py @@ -396,3 +396,40 @@ def test_nest_incomplete_type(): def test_issue_121(): load('parser-cases/issue_121.thrift') + + +def test_name_collision_with_imported_module(): + """Test that qualified names resolve correctly when a local struct + has the same name as an imported module.""" + thrift = load('parser-cases/name_collision.thrift') + + # The local struct 'name_collision_imported' should exist + assert hasattr(thrift, 'name_collision_imported') + local_struct = thrift.name_collision_imported + + # Verify it's the local struct with the expected fields + assert local_struct.thrift_spec[1][1] == 'local_field1' + assert local_struct.thrift_spec[2][1] == 'local_field2' + + # TestStruct should exist with two fields + test_struct = thrift.TestStruct + assert len(test_struct.thrift_spec) == 2 + + # Field 1: should reference the local struct 'name_collision_imported' + field1_spec = test_struct.thrift_spec[1] + assert field1_spec[1] == 'localStruct' + assert field1_spec[2] == local_struct + + # Field 2: should reference UserProfile from the imported module + field2_spec = test_struct.thrift_spec[2] + assert field2_spec[1] == 'importedUserProfile' + + # Critical assertions: field2 must NOT be the local struct + assert field2_spec[2] != local_struct + + # It must be UserProfile with the correct structure + user_profile_type = field2_spec[2] + assert user_profile_type.__name__ == 'UserProfile' + assert user_profile_type.thrift_spec[1][1] == 'user_id' + assert user_profile_type.thrift_spec[2][1] == 'username' + assert user_profile_type.thrift_spec[3][1] == 'email' diff --git a/thriftpy2/parser/parser.py b/thriftpy2/parser/parser.py index 92706ee0..5bae7d49 100644 --- a/thriftpy2/parser/parser.py +++ b/thriftpy2/parser/parser.py @@ -416,6 +416,31 @@ def p_ref_type(p): '''ref_type : IDENTIFIER''' ref_type = threadlocal.thrift_stack[-1] + # For qualified names (e.g., 'module.Type'), check if the prefix matches an included module + # This handles the case where a local struct shadows an imported module name + if '.' in p[1]: + prefix = p[1].split('.', 1)[0] + + # First, check if this prefix matches any included module + if hasattr(ref_type, '__thrift_meta__') and 'includes' in ref_type.__thrift_meta__: + for included_module in ref_type.__thrift_meta__['includes']: + if hasattr(included_module, '__name__') and included_module.__name__ == prefix: + # Found the included module, now resolve the rest of the path + path_parts = p[1].split('.')[1:] + current_ref = included_module + for part in path_parts: + current_ref = getattr(current_ref, part, None) + if current_ref is None: + break + + if current_ref is not None: + if hasattr(current_ref, '_ttype'): + p[0] = getattr(current_ref, '_ttype'), current_ref + else: + p[0] = current_ref + return + + # Original resolution logic for backward compatibility for attr in dir(ref_type): if attr in {'__doc__', '__loader__', '__name__', '__package__', '__spec__', '__thrift_file__', '__thrift_meta__'}: @@ -428,6 +453,7 @@ def p_ref_type(p): ref_type = resolved_ref_type break else: + # Standard resolution for unqualified names for index, name in enumerate(p[1].split('.')): ref_type = getattr(ref_type, name, None) if ref_type is None: