mirror of
https://gitflic.ru/project/openide/openide.git
synced 2026-04-30 10:20:15 +07:00
PY-18096 Fixed: False positive "Type doesn't have expected attributes" for namedtuple
Introduce PyClassLikeType.getMemberNames(boolean, TypeEvalContext). This method returns all members including dynamically ones (e.g. fields of namedtuple)
This commit is contained in:
@@ -31,9 +31,7 @@ import com.jetbrains.python.psi.types.TypeEvalContext;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.*;
|
||||
|
||||
/**
|
||||
* @author yole
|
||||
@@ -49,7 +47,7 @@ public class PyJavaClassType implements PyClassLikeType {
|
||||
|
||||
@Nullable
|
||||
public List<? extends RatedResolveResult> resolveMember(@NotNull final String name,
|
||||
PyExpression location,
|
||||
@Nullable PyExpression location,
|
||||
@NotNull AccessDirection direction,
|
||||
@NotNull PyResolveContext resolveContext) {
|
||||
return resolveMember(name, location, direction, resolveContext, true);
|
||||
@@ -156,6 +154,30 @@ public class PyJavaClassType implements PyClassLikeType {
|
||||
// TODO: Implement
|
||||
}
|
||||
|
||||
@NotNull
|
||||
@Override
|
||||
public Set<String> getMemberNames(boolean inherited, @NotNull TypeEvalContext context) {
|
||||
final Set<String> result = new LinkedHashSet<>();
|
||||
|
||||
for (PsiMethod method : myClass.getAllMethods()) {
|
||||
result.add(method.getName());
|
||||
}
|
||||
|
||||
for (PsiField field : myClass.getAllFields()) {
|
||||
result.add(field.getName());
|
||||
}
|
||||
|
||||
if (inherited) {
|
||||
for (PyClassLikeType type : getAncestorTypes(context)) {
|
||||
if (type != null) {
|
||||
result.addAll(type.getMemberNames(false, context));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
@NotNull
|
||||
@Override
|
||||
public List<PyClassLikeType> getAncestorTypes(@NotNull final TypeEvalContext context) {
|
||||
|
||||
@@ -26,6 +26,7 @@ import org.jetbrains.annotations.NotNull;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
|
||||
/**
|
||||
* @author vlan
|
||||
@@ -42,8 +43,10 @@ public interface PyClassLikeType extends PyCallableType, PyWithAncestors {
|
||||
List<PyClassLikeType> getSuperClassTypes(@NotNull TypeEvalContext context);
|
||||
|
||||
@Nullable
|
||||
List<? extends RatedResolveResult> resolveMember(@NotNull final String name, @Nullable PyExpression location,
|
||||
@NotNull AccessDirection direction, @NotNull PyResolveContext resolveContext,
|
||||
List<? extends RatedResolveResult> resolveMember(@NotNull final String name,
|
||||
@Nullable PyExpression location,
|
||||
@NotNull AccessDirection direction,
|
||||
@NotNull PyResolveContext resolveContext,
|
||||
boolean inherited);
|
||||
|
||||
// TODO: Pull to PyType at next iteration
|
||||
@@ -58,6 +61,9 @@ public interface PyClassLikeType extends PyCallableType, PyWithAncestors {
|
||||
*/
|
||||
void visitMembers(@NotNull Processor<PsiElement> processor, boolean inherited, @NotNull TypeEvalContext context);
|
||||
|
||||
@NotNull
|
||||
Set<String> getMemberNames(boolean inherited, @NotNull TypeEvalContext context);
|
||||
|
||||
boolean isValid();
|
||||
|
||||
@Nullable
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2000-2014 JetBrains s.r.o.
|
||||
* Copyright 2000-2016 JetBrains s.r.o.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
@@ -112,7 +112,10 @@ public class PyCustomType implements PyClassLikeType {
|
||||
|
||||
// Delegate calls to classes, we mimic but filter if filter is set.
|
||||
for (final PyClassLikeType typeToMimic : myTypesToMimic) {
|
||||
final List<? extends RatedResolveResult> results = typeToMimic.toInstance().resolveMember(name, location, direction, resolveContext, inherited);
|
||||
final List<? extends RatedResolveResult> results = typeToMimic.toInstance().resolveMember(
|
||||
name, location, direction, resolveContext, inherited
|
||||
);
|
||||
|
||||
if (results != null) {
|
||||
globalResult.addAll(Collections2.filter(results, new ResolveFilter()));
|
||||
}
|
||||
@@ -253,7 +256,9 @@ public class PyCustomType implements PyClassLikeType {
|
||||
}
|
||||
|
||||
@Override
|
||||
public final void visitMembers(@NotNull final Processor<PsiElement> processor, final boolean inherited, @NotNull final TypeEvalContext context) {
|
||||
public final void visitMembers(@NotNull final Processor<PsiElement> processor,
|
||||
final boolean inherited,
|
||||
@NotNull final TypeEvalContext context) {
|
||||
for (final PyClassLikeType type : myTypesToMimic) {
|
||||
// Only visit methods that are allowed by filter (if any)
|
||||
type.visitMembers(new Processor<PsiElement>() {
|
||||
@@ -271,6 +276,18 @@ public class PyCustomType implements PyClassLikeType {
|
||||
}
|
||||
}
|
||||
|
||||
@NotNull
|
||||
@Override
|
||||
public Set<String> getMemberNames(boolean inherited, @NotNull TypeEvalContext context) {
|
||||
final Set<String> result = new LinkedHashSet<>();
|
||||
|
||||
for (PyClassLikeType type : myTypesToMimic) {
|
||||
result.addAll(type.getMemberNames(inherited, context));
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Predicate that filters completion using {@link #myFilter}
|
||||
*/
|
||||
|
||||
@@ -35,6 +35,7 @@ import org.jetbrains.annotations.Nullable;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
|
||||
/**
|
||||
* @author yole
|
||||
@@ -69,7 +70,7 @@ public class PyNamedTupleType extends PyClassTypeImpl implements PyCallableType
|
||||
return classMembers;
|
||||
}
|
||||
if (myFields.contains(name)) {
|
||||
return Collections.singletonList(new RatedResolveResult(1000, new PyElementImpl(myDeclaration.getNode())));
|
||||
return Collections.singletonList(new RatedResolveResult(RatedResolveResult.RATE_HIGH, new PyElementImpl(myDeclaration.getNode())));
|
||||
}
|
||||
return null;
|
||||
}
|
||||
@@ -98,7 +99,7 @@ public class PyNamedTupleType extends PyClassTypeImpl implements PyCallableType
|
||||
@Override
|
||||
public PyType getCallType(@NotNull TypeEvalContext context, @NotNull PyCallSiteExpression callSite) {
|
||||
if (myDefinitionLevel > 0) {
|
||||
return new PyNamedTupleType(myClass, myDeclaration, myName, myFields, myDefinitionLevel-1);
|
||||
return new PyNamedTupleType(myClass, myDeclaration, myName, myFields, myDefinitionLevel - 1);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
@@ -113,6 +114,15 @@ public class PyNamedTupleType extends PyClassTypeImpl implements PyCallableType
|
||||
return "PyNamedTupleType: " + myName;
|
||||
}
|
||||
|
||||
@NotNull
|
||||
@Override
|
||||
public Set<String> getMemberNames(boolean inherited, @NotNull TypeEvalContext context) {
|
||||
final Set<String> result = super.getMemberNames(inherited, context);
|
||||
result.addAll(myFields);
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
@Nullable
|
||||
public static PyType fromCall(@NotNull PyCallExpression call, @NotNull TypeEvalContext context, int level) {
|
||||
final String name = PyPsiUtils.strValue(call.getArgument(0, PyExpression.class));
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2000-2014 JetBrains s.r.o.
|
||||
* Copyright 2000-2016 JetBrains s.r.o.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
@@ -46,8 +46,9 @@ public class PyTypeCheckerInspection extends PyInspection {
|
||||
@NotNull
|
||||
@Override
|
||||
public PsiElementVisitor buildVisitor(@NotNull ProblemsHolder holder, boolean isOnTheFly, @NotNull LocalInspectionToolSession session) {
|
||||
if (LOG.isDebugEnabled())
|
||||
if (LOG.isDebugEnabled()) {
|
||||
session.putUserData(TIME_KEY, System.nanoTime());
|
||||
}
|
||||
return new Visitor(holder, session);
|
||||
}
|
||||
|
||||
@@ -88,18 +89,22 @@ public class PyTypeCheckerInspection extends PyInspection {
|
||||
|
||||
private void checkCallSite(@Nullable PyCallSiteExpression callSite) {
|
||||
final List<PyTypeChecker.AnalyzeCallResults> resultsSet = PyTypeChecker.analyzeCallSite(callSite, myTypeEvalContext);
|
||||
final List<Map<PyExpression, Pair<String, ProblemHighlightType>>> problemsSet = new ArrayList<Map<PyExpression, Pair<String, ProblemHighlightType>>>();
|
||||
final List<Map<PyExpression, Pair<String, ProblemHighlightType>>> problemsSet =
|
||||
new ArrayList<Map<PyExpression, Pair<String, ProblemHighlightType>>>();
|
||||
for (PyTypeChecker.AnalyzeCallResults results : resultsSet) {
|
||||
problemsSet.add(checkMapping(results.getReceiver(), results.getArguments()));
|
||||
}
|
||||
if (!problemsSet.isEmpty()) {
|
||||
Map<PyExpression, Pair<String, ProblemHighlightType>> minProblems = Collections.min(problemsSet, new Comparator<Map<PyExpression, Pair<String, ProblemHighlightType>>>() {
|
||||
@Override
|
||||
public int compare(Map<PyExpression, Pair<String, ProblemHighlightType>> o1,
|
||||
Map<PyExpression, Pair<String, ProblemHighlightType>> o2) {
|
||||
return o1.size() - o2.size();
|
||||
Map<PyExpression, Pair<String, ProblemHighlightType>> minProblems = Collections.min(
|
||||
problemsSet,
|
||||
new Comparator<Map<PyExpression, Pair<String, ProblemHighlightType>>>() {
|
||||
@Override
|
||||
public int compare(Map<PyExpression, Pair<String, ProblemHighlightType>> o1,
|
||||
Map<PyExpression, Pair<String, ProblemHighlightType>> o2) {
|
||||
return o1.size() - o2.size();
|
||||
}
|
||||
}
|
||||
});
|
||||
);
|
||||
for (Map.Entry<PyExpression, Pair<String, ProblemHighlightType>> entry : minProblems.entrySet()) {
|
||||
registerProblem(entry.getKey(), entry.getValue().getFirst(), entry.getValue().getSecond());
|
||||
}
|
||||
@@ -109,7 +114,8 @@ public class PyTypeCheckerInspection extends PyInspection {
|
||||
@NotNull
|
||||
private Map<PyExpression, Pair<String, ProblemHighlightType>> checkMapping(@Nullable PyExpression receiver,
|
||||
@NotNull Map<PyExpression, PyNamedParameter> mapping) {
|
||||
final Map<PyExpression, Pair<String, ProblemHighlightType>> problems = new HashMap<PyExpression, Pair<String, ProblemHighlightType>>();
|
||||
final Map<PyExpression, Pair<String, ProblemHighlightType>> problems =
|
||||
new HashMap<PyExpression, Pair<String, ProblemHighlightType>>();
|
||||
final Map<PyGenericType, PyType> substitutions = new LinkedHashMap<PyGenericType, PyType>();
|
||||
boolean genericsCollected = false;
|
||||
for (Map.Entry<PyExpression, PyNamedParameter> entry : mapping.entrySet()) {
|
||||
@@ -130,7 +136,6 @@ public class PyTypeCheckerInspection extends PyInspection {
|
||||
final Pair<String, ProblemHighlightType> problem = checkTypes(paramType, argType, myTypeEvalContext, substitutions);
|
||||
if (problem != null) {
|
||||
problems.put(arg, problem);
|
||||
|
||||
}
|
||||
}
|
||||
return problems;
|
||||
@@ -151,13 +156,13 @@ public class PyTypeCheckerInspection extends PyInspection {
|
||||
final PyType substitute = PyTypeChecker.substitute(expected, substitutions, context);
|
||||
if (substitute != null) {
|
||||
quotedExpectedName = String.format("'%s' (matched generic type '%s')",
|
||||
PythonDocumentationProvider.getTypeName(substitute, context),
|
||||
expectedName);
|
||||
PythonDocumentationProvider.getTypeName(substitute, context),
|
||||
expectedName);
|
||||
highlightType = ProblemHighlightType.WEAK_WARNING;
|
||||
}
|
||||
}
|
||||
final String actualName = PythonDocumentationProvider.getTypeName(actual, context);
|
||||
String msg= String.format("Expected type %s, got '%s' instead", quotedExpectedName, actualName);
|
||||
String msg = String.format("Expected type %s, got '%s' instead", quotedExpectedName, actualName);
|
||||
if (expected instanceof PyStructuralType) {
|
||||
final Set<String> expectedAttributes = ((PyStructuralType)expected).getAttributeNames();
|
||||
final Set<String> actualAttributes = getAttributes(actual, context);
|
||||
@@ -190,8 +195,8 @@ public class PyTypeCheckerInspection extends PyInspection {
|
||||
if (type instanceof PyStructuralType) {
|
||||
return ((PyStructuralType)type).getAttributeNames();
|
||||
}
|
||||
else if (type instanceof PyClassType) {
|
||||
return PyTypeChecker.getClassTypeAttributes((PyClassType)type, true, context);
|
||||
else if (type instanceof PyClassLikeType) {
|
||||
return ((PyClassLikeType)type).getMemberNames(true, context);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2000-2014 JetBrains s.r.o.
|
||||
* Copyright 2000-2016 JetBrains s.r.o.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
@@ -368,17 +368,17 @@ public class PyClassTypeImpl extends UserDataHolderBase implements PyClassType {
|
||||
@Nullable
|
||||
@Override
|
||||
public PyType getReturnType(@NotNull TypeEvalContext context) {
|
||||
return getReturnType(context, null);
|
||||
return getPossibleCallType(context, null);
|
||||
}
|
||||
|
||||
@Nullable
|
||||
@Override
|
||||
public PyType getCallType(@NotNull TypeEvalContext context, @NotNull PyCallSiteExpression callSite) {
|
||||
return getReturnType(context, callSite);
|
||||
return getPossibleCallType(context, callSite);
|
||||
}
|
||||
|
||||
@Nullable
|
||||
private PyType getReturnType(@NotNull TypeEvalContext context, @Nullable PyCallSiteExpression callSite) {
|
||||
private PyType getPossibleCallType(@NotNull TypeEvalContext context, @Nullable PyCallSiteExpression callSite) {
|
||||
if (!isDefinition()) {
|
||||
return PyUtil.getReturnTypeOfMember(this, PyNames.CALL, callSite, context);
|
||||
}
|
||||
@@ -523,7 +523,6 @@ public class PyClassTypeImpl extends UserDataHolderBase implements PyClassType {
|
||||
public void visitMembers(@NotNull final Processor<PsiElement> processor,
|
||||
final boolean inherited,
|
||||
@NotNull final TypeEvalContext context) {
|
||||
|
||||
myClass.visitMethods(new MyProcessorWrapper<PyFunction>(processor), false, context);
|
||||
myClass.visitClassAttributes(new MyProcessorWrapper<PyTargetExpression>(processor), false, context);
|
||||
|
||||
@@ -541,6 +540,42 @@ public class PyClassTypeImpl extends UserDataHolderBase implements PyClassType {
|
||||
}
|
||||
}
|
||||
|
||||
@NotNull
|
||||
@Override
|
||||
public Set<String> getMemberNames(boolean inherited, @NotNull TypeEvalContext context) {
|
||||
final Set<String> result = new LinkedHashSet<>();
|
||||
|
||||
for (PyFunction function : myClass.getMethods()) {
|
||||
result.add(function.getName());
|
||||
}
|
||||
|
||||
for (PyTargetExpression expression : myClass.getClassAttributes()) {
|
||||
result.add(expression.getName());
|
||||
}
|
||||
|
||||
for (PyTargetExpression expression : myClass.getInstanceAttributes()) {
|
||||
result.add(expression.getName());
|
||||
}
|
||||
|
||||
for (PyClassMembersProvider provider : Extensions.getExtensions(PyClassMembersProvider.EP_NAME)) {
|
||||
for (PyCustomMember member : provider.getMembers(this, null, context)) {
|
||||
result.add(member.getName());
|
||||
}
|
||||
}
|
||||
|
||||
if (inherited) {
|
||||
for (PyClassLikeType type : getAncestorTypes(context)) {
|
||||
if (type != null) {
|
||||
final PyClassLikeType ancestorType = isDefinition() ? type : type.toInstance();
|
||||
|
||||
result.addAll(ancestorType.getMemberNames(false, context));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
private void addOwnClassMembers(PsiElement expressionHook,
|
||||
Set<String> namesAlready,
|
||||
boolean suppressParentheses,
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2000-2014 JetBrains s.r.o.
|
||||
* Copyright 2000-2016 JetBrains s.r.o.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
@@ -15,11 +15,10 @@
|
||||
*/
|
||||
package com.jetbrains.python.psi.types;
|
||||
|
||||
import com.intellij.openapi.extensions.Extensions;
|
||||
import com.intellij.psi.*;
|
||||
import com.intellij.psi.PsiElement;
|
||||
import com.intellij.psi.PsiNamedElement;
|
||||
import com.intellij.util.ArrayUtil;
|
||||
import com.jetbrains.python.PyNames;
|
||||
import com.jetbrains.python.codeInsight.PyCustomMember;
|
||||
import com.jetbrains.python.psi.*;
|
||||
import com.jetbrains.python.psi.impl.PyBuiltinCache;
|
||||
import com.jetbrains.python.psi.impl.PyCallExpressionHelper;
|
||||
@@ -46,8 +45,8 @@ public class PyTypeChecker {
|
||||
* For example int matches object, while str doesn't match int.
|
||||
* Work for builtin types, classes, tuples etc.
|
||||
*
|
||||
* @param expected expected type
|
||||
* @param actual type to be matched against expected
|
||||
* @param expected expected type
|
||||
* @param actual type to be matched against expected
|
||||
* @param context
|
||||
* @param substitutions
|
||||
* @return
|
||||
@@ -179,11 +178,11 @@ public class PyTypeChecker {
|
||||
if (overridesGetAttr(actualClassType.getPyClass(), context)) {
|
||||
return true;
|
||||
}
|
||||
final Set<String> actualAttributes = getClassTypeAttributes(actualClassType, true, context);
|
||||
final Set<String> actualAttributes = actualClassType.getMemberNames(true, context);
|
||||
return actualAttributes.containsAll(((PyStructuralType)expected).getAttributeNames());
|
||||
}
|
||||
if (actual instanceof PyStructuralType && expected instanceof PyClassType) {
|
||||
final Set<String> expectedAttributes = getClassTypeAttributes((PyClassType)expected, true, context);
|
||||
final Set<String> expectedAttributes = ((PyClassType)expected).getMemberNames(true, context);
|
||||
return expectedAttributes.containsAll(((PyStructuralType)actual).getAttributeNames());
|
||||
}
|
||||
if (actual instanceof PyCallableType && expected instanceof PyCallableType) {
|
||||
@@ -212,47 +211,6 @@ public class PyTypeChecker {
|
||||
return matchNumericTypes(expected, actual);
|
||||
}
|
||||
|
||||
@NotNull
|
||||
public static Set<String> getClassTypeAttributes(@NotNull PyClassType type, boolean inherited, @NotNull TypeEvalContext context) {
|
||||
final Set<String> attributes = getClassAttributes(type.getPyClass(), inherited, type.isDefinition(), context);
|
||||
for (PyClassMembersProvider provider : Extensions.getExtensions(PyClassMembersProvider.EP_NAME)) {
|
||||
final Collection<PyCustomMember> members = provider.getMembers(type, null, context);
|
||||
for (PyCustomMember member : members) {
|
||||
attributes.add(member.getName());
|
||||
}
|
||||
}
|
||||
return attributes;
|
||||
}
|
||||
|
||||
@NotNull
|
||||
private static Set<String> getClassAttributes(@NotNull PyClass cls,
|
||||
boolean inherited,
|
||||
boolean isDefinition,
|
||||
@NotNull TypeEvalContext context) {
|
||||
final Set<String> attributes = new HashSet<String>();
|
||||
for (PyFunction function : cls.getMethods()) {
|
||||
attributes.add(function.getName());
|
||||
}
|
||||
for (PyTargetExpression instanceAttribute : cls.getInstanceAttributes()) {
|
||||
attributes.add(instanceAttribute.getName());
|
||||
}
|
||||
for (PyTargetExpression classAttribute : cls.getClassAttributes()) {
|
||||
attributes.add(classAttribute.getName());
|
||||
}
|
||||
if (inherited) {
|
||||
for (PyClass ancestor : cls.getAncestorClasses(null)) {
|
||||
final PyType ancestorType = context.getType(ancestor);
|
||||
if (ancestorType instanceof PyClassLikeType) {
|
||||
final PyClassLikeType classType = isDefinition ? (PyClassLikeType)ancestorType : ((PyClassLikeType)ancestorType).toInstance();
|
||||
if (classType instanceof PyClassType) {
|
||||
attributes.addAll(getClassTypeAttributes((PyClassType)classType, false, context));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return attributes;
|
||||
}
|
||||
|
||||
private static boolean matchNumericTypes(PyType expected, PyType actual) {
|
||||
final String superName = expected.getName();
|
||||
final String subName = actual.getName();
|
||||
@@ -527,7 +485,7 @@ public class PyTypeChecker {
|
||||
return isUnionCallable((PyUnionType)type);
|
||||
}
|
||||
if (type instanceof PyCallableType) {
|
||||
return ((PyCallableType) type).isCallable();
|
||||
return ((PyCallableType)type).isCallable();
|
||||
}
|
||||
if (type instanceof PyStructuralType && ((PyStructuralType)type).isInferredFromUsages()) {
|
||||
return true;
|
||||
|
||||
@@ -0,0 +1,16 @@
|
||||
from collections import namedtuple
|
||||
|
||||
|
||||
class C(namedtuple('C', ['foo', 'bar'])):
|
||||
pass
|
||||
|
||||
|
||||
def f(x):
|
||||
return x.foo, x.bar
|
||||
|
||||
def g():
|
||||
x = C(foo=0, bar=1)
|
||||
return f(x)
|
||||
|
||||
|
||||
print(g())
|
||||
@@ -151,6 +151,11 @@ public class PyTypeCheckerInspectionTest extends PyTestCase {
|
||||
doTest();
|
||||
}
|
||||
|
||||
// PY-18096
|
||||
public void testNamedTupleBaseClass() {
|
||||
doTest();
|
||||
}
|
||||
|
||||
// PY-6803
|
||||
public void testPropertyAndFactoryFunction() {
|
||||
doTest();
|
||||
|
||||
Reference in New Issue
Block a user