Skip to content

Commit e73b729

Browse files
SONARPY-970 Serialize stub class members to Protobuf
1 parent 76d1aa3 commit e73b729

File tree

7 files changed

+57
-13
lines changed

7 files changed

+57
-13
lines changed

python-frontend/src/main/java/org/sonar/python/semantic/ClassSymbolImpl.java

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -137,22 +137,25 @@ public ClassSymbolImpl(SymbolsProtos.ClassSymbol classSymbolProto, String module
137137
hasMetaClass = classSymbolProto.getHasMetaclass();
138138
metaclassFQN = classSymbolProto.getMetaclassName();
139139
supportsGenerics = classSymbolProto.getIsGeneric();
140-
Set<Symbol> methods = new HashSet<>();
140+
Set<Symbol> classMembers = new HashSet<>();
141141
Map<String, Set<Object>> descriptorsByFqn = new HashMap<>();
142142
classSymbolProto.getMethodsList().stream()
143143
.filter(d -> isValidForProjectPythonVersion(d.getValidForList()))
144144
.forEach(proto -> descriptorsByFqn.computeIfAbsent(proto.getFullyQualifiedName(), d -> new HashSet<>()).add(proto));
145145
classSymbolProto.getOverloadedMethodsList().stream()
146146
.filter(d -> isValidForProjectPythonVersion(d.getValidForList()))
147147
.forEach(proto -> descriptorsByFqn.computeIfAbsent(proto.getFullname(), d -> new HashSet<>()).add(proto));
148+
classSymbolProto.getAttributesList().stream()
149+
.filter(d -> isValidForProjectPythonVersion(d.getValidForList()))
150+
.forEach(proto -> descriptorsByFqn.computeIfAbsent(proto.getFullyQualifiedName(), d -> new HashSet<>()).add(proto));
148151

149152
inlineInheritedMethodsFromPrivateClass(classSymbolProto.getSuperClassesList(), descriptorsByFqn);
150153

151154
for (Map.Entry<String, Set<Object>> entry : descriptorsByFqn.entrySet()) {
152155
Set<Symbol> symbols = symbolsFromProtobufDescriptors(entry.getValue(), fullyQualifiedName, moduleName);
153-
methods.add(symbols.size() > 1 ? AmbiguousSymbolImpl.create(symbols) : symbols.iterator().next());
156+
classMembers.add(symbols.size() > 1 ? AmbiguousSymbolImpl.create(symbols) : symbols.iterator().next());
154157
}
155-
addMembers(methods);
158+
addMembers(classMembers);
156159
superClassesFqns.addAll(classSymbolProto.getSuperClassesList().stream().map(TypeShed::normalizedFqn).collect(Collectors.toList()));
157160
superClassesFqns.removeAll(inlinedSuperClassFqn);
158161
validForPythonVersions = new HashSet<>(classSymbolProto.getValidForList());

python-frontend/src/main/protobuf/symbols.proto

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -81,6 +81,7 @@ message ClassSymbol {
8181
bool is_protocol = 11;
8282
optional string metaclass_name = 12;
8383
repeated string valid_for = 13;
84+
repeated VarSymbol attributes = 14;
8485
}
8586

8687
message ModuleSymbol {

python-frontend/typeshed_serializer/serializer/symbols.py

Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -264,6 +264,7 @@ def __init__(self, type_info: mpn.TypeInfo):
264264
self.super_classes = []
265265
self.methods = []
266266
self.overloaded_methods = []
267+
self.vars = []
267268
self.is_enum = type_info.is_enum
268269
self.is_generic = type_info.is_generic()
269270
self.is_protocol = type_info.is_protocol
@@ -276,10 +277,12 @@ def __init__(self, type_info: mpn.TypeInfo):
276277
node = name.node
277278
if isinstance(node, mpn.FuncDef):
278279
self.methods.append(FunctionSymbol(node))
279-
if isinstance(node, mpn.Decorator):
280+
elif isinstance(node, mpn.Decorator):
280281
self.methods.append(FunctionSymbol(node.func, decorators=node.original_decorators))
281-
if isinstance(node, mpn.OverloadedFuncDef):
282+
elif isinstance(node, mpn.OverloadedFuncDef):
282283
self.overloaded_methods.append(OverloadedFunctionSymbol(node))
284+
elif isinstance(node, mpn.Var) and node.name not in DEFAULT_EXPORTED_VARS:
285+
self.vars.append(VarSymbol.from_var(node))
283286
class_def = type_info.defn
284287
self.has_metaclass = class_def.metaclass is not None
285288
if class_def.metaclass is not None:
@@ -316,6 +319,8 @@ def to_proto(self) -> symbols_pb2.ClassSymbol:
316319
pb_class.methods.append(method.to_proto())
317320
for overloaded_method in self.overloaded_methods:
318321
pb_class.overloaded_methods.append(overloaded_method.to_proto())
322+
for var in self.vars:
323+
pb_class.attributes.append(var.to_proto())
319324
return pb_class
320325

321326

@@ -406,11 +411,12 @@ def to_proto(self) -> symbols_pb2.FunctionSymbol:
406411

407412
class MergedClassSymbol:
408413
def __init__(self, reference_class_symbols: ClassSymbol, merged_methods, merged_overloaded_methods,
409-
valid_for: List[str]):
414+
merged_attributes, valid_for: List[str]):
410415
# nested class symbols functions are not relevant anymore
411416
self.class_symbol = reference_class_symbols
412417
self.methods = merged_methods
413418
self.overloaded_methods = merged_overloaded_methods
419+
self.vars = merged_attributes
414420
self.valid_for = valid_for
415421

416422
def to_proto(self) -> symbols_pb2.ClassSymbol:
@@ -431,6 +437,9 @@ def to_proto(self) -> symbols_pb2.ClassSymbol:
431437
for overloaded_func in self.overloaded_methods:
432438
for elem in self.overloaded_methods[overloaded_func]:
433439
pb_class.overloaded_methods.append(elem.to_proto())
440+
for var in self.vars:
441+
for elem in self.vars[var]:
442+
pb_class.attributes.append(elem.to_proto())
434443
for elem in self.valid_for:
435444
pb_class.valid_for.append(elem)
436445
return pb_class

python-frontend/typeshed_serializer/serializer/symbols_merger.py

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -74,8 +74,8 @@ def merge_modules(all_python_modules: Set[str], model_by_version: Dict[str, Dict
7474
return merged_modules
7575

7676

77-
def merge_vars(current_module, handled_vars, version):
78-
for var in current_module.vars:
77+
def merge_vars(module_or_class, handled_vars, version):
78+
for var in module_or_class.vars:
7979
if var.fullname not in handled_vars:
8080
# doesn't exist: we add it
8181
handled_vars[var.fullname] = [MergedVarSymbol(var, [version])]
@@ -95,27 +95,33 @@ def merge_classes(current_module, handled_classes, version):
9595
if mod_class.fullname not in handled_classes:
9696
functions = {}
9797
overloaded_functions = {}
98+
variables = {}
9899
merge_functions(mod_class, functions, version)
99100
merge_overloaded_functions(mod_class, overloaded_functions, version)
101+
merge_vars(mod_class, variables, version)
100102
handled_classes[mod_class.fullname] = [MergedClassSymbol(mod_class, functions,
101-
overloaded_functions, [version])]
103+
overloaded_functions, variables, [version])]
102104
else:
103105
# merge
104106
compared = handled_classes[mod_class.fullname]
105107
for elem in compared:
106108
if elem.class_symbol == mod_class:
107109
functions = elem.methods
108110
overloaded_functions = elem.overloaded_methods
111+
variables = elem.vars
109112
merge_functions(mod_class, functions, version)
110113
merge_overloaded_functions(mod_class, overloaded_functions, version)
114+
merge_vars(mod_class, variables, version)
111115
elem.valid_for.append(version)
112116
break
113117
else:
114118
functions = {}
115119
overloaded_functions = {}
120+
variables = {}
116121
merge_functions(mod_class, functions, version)
117122
merge_overloaded_functions(mod_class, overloaded_functions, version)
118-
compared.append(MergedClassSymbol(mod_class, functions, overloaded_functions, [version]))
123+
merge_vars(mod_class, variables, version)
124+
compared.append(MergedClassSymbol(mod_class, functions, overloaded_functions, variables, [version]))
119125

120126

121127
def merge_overloaded_functions(module_or_class, handled_overloaded_funcs, version):

python-frontend/typeshed_serializer/tests/resources/fakemodule.pyi

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -103,3 +103,12 @@ else:
103103
common_var: bool
104104

105105
alias = common_overloaded_function
106+
107+
class ClassWithFields:
108+
common_field: str
109+
if sys.version_info >= (3, 8):
110+
field_unique_38: int
111+
field_multiple_defs: str
112+
else:
113+
field_unique_36: int
114+
field_multiple_defs: int

python-frontend/typeshed_serializer/tests/test_symbols.py

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -37,12 +37,14 @@ def test_module_symbol(typeshed_stdlib):
3737
assert pb_module.fully_qualified_name == "abc"
3838
assert len(pb_module.classes) == 5
3939
assert len(pb_module.functions) == 4
40-
imported_modules = [imported_module for imported_module in pb_module.vars if imported_module.is_imported_module is True]
40+
imported_modules = [imported_module for imported_module in pb_module.vars if
41+
imported_module.is_imported_module is True]
4142
assert len(imported_modules) == 0
4243

4344
os_module = typeshed_stdlib.files.get("os")
4445
pb_module = symbols.ModuleSymbol(os_module).to_proto()
45-
imported_modules = [imported_module for imported_module in pb_module.vars if imported_module.is_imported_module is True]
46+
imported_modules = [imported_module for imported_module in pb_module.vars if
47+
imported_module.is_imported_module is True]
4648
assert len(imported_modules) == 3
4749
imported_modules = map(lambda m: (m.fully_qualified_name, m.name), imported_modules)
4850
assert ("sys", "sys") in imported_modules
@@ -57,6 +59,8 @@ def test_class_symbol(typeshed_stdlib):
5759
assert cmd_class_symbol.fullname == "cmd.Cmd"
5860
assert cmd_class_symbol.name == "Cmd"
5961
assert cmd_class_symbol.super_classes == ["builtins.object"]
62+
assert len(cmd_class_symbol.methods) == 18
63+
assert len(cmd_class_symbol.vars) == 17
6064

6165
pb_class_symbol = cmd_class_symbol.to_proto()
6266
assert pb_class_symbol.name == "Cmd"

python-frontend/typeshed_serializer/tests/test_symbols_merger.py

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -139,7 +139,7 @@ def test_actual_module_merge(fake_module_36_38):
139139
merged_fakemodule_module = merged_modules['fakemodule']
140140
classes_dict = merged_fakemodule_module.classes
141141

142-
assert len(classes_dict) == 4
142+
assert len(classes_dict) == 5
143143

144144
# Class unique to Python 3.6 is present
145145
fakemodule_someclassunique36_symbols = classes_dict['fakemodule.SomeClassUnique36']
@@ -242,6 +242,18 @@ def test_actual_module_merge(fake_module_36_38):
242242
assert len(imported_sys) == 1
243243
assert imported_sys[0].var_symbol.is_imported_module is True
244244

245+
fakemodule_class_with_fields_symbols = classes_dict['fakemodule.ClassWithFields']
246+
assert len(fakemodule_class_with_fields_symbols) == 1
247+
fakemodule_class_symbol = fakemodule_class_with_fields_symbols[0]
248+
# Some fields are common
249+
assert fakemodule_class_symbol.vars['fakemodule.ClassWithFields.common_field'][0].valid_for == ["36", "38"]
250+
# Some fields exist only in a given Python version
251+
assert fakemodule_class_symbol.vars['fakemodule.ClassWithFields.field_unique_36'][0].valid_for == ["36"]
252+
assert fakemodule_class_symbol.vars['fakemodule.ClassWithFields.field_unique_38'][0].valid_for == ["38"]
253+
# Some fields have different definitions depending on the Python version
254+
assert fakemodule_class_symbol.vars['fakemodule.ClassWithFields.field_multiple_defs'][0].valid_for == ["36"]
255+
assert fakemodule_class_symbol.vars['fakemodule.ClassWithFields.field_multiple_defs'][1].valid_for == ["38"]
256+
245257

246258
def assert_merged_class_symbol_to_proto(merged_classes_proto, merged_classes):
247259
assert len(merged_classes_proto) == len(merged_classes)

0 commit comments

Comments
 (0)