mirror of
https://github.com/eclipse-cdt/cdt
synced 2025-07-23 17:05:26 +02:00
Mapping types of variables back to ast-types, if possible; bug 292749.
This commit is contained in:
parent
24ce8e3659
commit
353d1d0875
10 changed files with 256 additions and 14 deletions
|
@ -233,6 +233,72 @@ public abstract class IndexBindingResolutionTestBase extends BaseTestCase {
|
|||
assertQNEquals(expContainedTypeQN, (IBinding) containedType);
|
||||
}
|
||||
}
|
||||
|
||||
class SinglePDOMTestFirstASTStrategy implements ITestStrategy {
|
||||
private IIndex index;
|
||||
private ICProject cproject;
|
||||
private StringBuffer[] testData;
|
||||
private IASTTranslationUnit ast;
|
||||
private boolean cpp;
|
||||
|
||||
public SinglePDOMTestFirstASTStrategy(boolean cpp) {
|
||||
this.cpp = cpp;
|
||||
}
|
||||
|
||||
public ICProject getCProject() {
|
||||
return cproject;
|
||||
}
|
||||
|
||||
public StringBuffer[] getTestData() {
|
||||
return testData;
|
||||
}
|
||||
|
||||
public IASTTranslationUnit getAst() {
|
||||
return ast;
|
||||
}
|
||||
|
||||
public void setUp() throws Exception {
|
||||
cproject = cpp ? CProjectHelper.createCCProject(getName()+System.currentTimeMillis(), "bin", IPDOMManager.ID_NO_INDEXER)
|
||||
: CProjectHelper.createCProject(getName()+System.currentTimeMillis(), "bin", IPDOMManager.ID_NO_INDEXER);
|
||||
Bundle b = CTestPlugin.getDefault().getBundle();
|
||||
testData = TestSourceReader.getContentsForTest(b, "parser", IndexBindingResolutionTestBase.this.getClass(), getName(), 2);
|
||||
|
||||
if (testData.length < 2)
|
||||
return;
|
||||
IFile file = TestSourceReader.createFile(cproject.getProject(), new Path("header.h"), testData[0].toString());
|
||||
CCorePlugin.getIndexManager().setIndexerId(cproject, IPDOMManager.ID_FAST_INDEXER);
|
||||
assertTrue(CCorePlugin.getIndexManager().joinIndexer(360000, new NullProgressMonitor()));
|
||||
|
||||
if (DEBUG) {
|
||||
System.out.println("Project PDOM: "+getName());
|
||||
((PDOM)CCoreInternals.getPDOMManager().getPDOM(cproject)).accept(new PDOMPrettyPrinter());
|
||||
}
|
||||
|
||||
index= CCorePlugin.getIndexManager().getIndex(cproject);
|
||||
|
||||
index.acquireReadLock();
|
||||
IFile cppfile= TestSourceReader.createFile(cproject.getProject(), new Path("references.c" + (cpp ? "pp" : "")), testData[1].toString());
|
||||
ast = TestSourceReader.createIndexBasedAST(index, cproject, cppfile);
|
||||
}
|
||||
|
||||
public void tearDown() throws Exception {
|
||||
if (index != null) {
|
||||
index.releaseReadLock();
|
||||
}
|
||||
if (cproject != null) {
|
||||
cproject.getProject().delete(IResource.FORCE | IResource.ALWAYS_DELETE_PROJECT_CONTENT, new NullProgressMonitor());
|
||||
}
|
||||
}
|
||||
|
||||
public IIndex getIndex() {
|
||||
return index;
|
||||
}
|
||||
|
||||
public boolean isCompositeIndex() {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
class SinglePDOMTestStrategy implements ITestStrategy {
|
||||
private IIndex index;
|
||||
|
|
|
@ -64,13 +64,14 @@ public class IndexCPPBindingResolutionBugs extends IndexBindingResolutionTestBas
|
|||
public SingleProject() {setStrategy(new SinglePDOMTestStrategy(true));}
|
||||
public static TestSuite suite() {return suite(SingleProject.class);}
|
||||
}
|
||||
|
||||
|
||||
public static class ProjectWithDepProj extends IndexCPPBindingResolutionBugs {
|
||||
public ProjectWithDepProj() {setStrategy(new ReferencedProject(true));}
|
||||
public static TestSuite suite() {return suite(ProjectWithDepProj.class);}
|
||||
}
|
||||
|
||||
public static void addTests(TestSuite suite) {
|
||||
suite.addTest(IndexCPPBindingResolutionBugsSingleProjectFirstAST.suite());
|
||||
suite.addTest(SingleProject.suite());
|
||||
suite.addTest(ProjectWithDepProj.suite());
|
||||
}
|
||||
|
@ -87,6 +88,7 @@ public class IndexCPPBindingResolutionBugs extends IndexBindingResolutionTestBas
|
|||
// #define FUNC() void bar()
|
||||
// #define FUNC2(A) void baz()
|
||||
|
||||
|
||||
// #include "header.h"
|
||||
//
|
||||
// OBJ {}
|
||||
|
@ -1136,4 +1138,20 @@ public class IndexCPPBindingResolutionBugs extends IndexBindingResolutionTestBas
|
|||
buf.append('}');
|
||||
return buf.toString();
|
||||
}
|
||||
|
||||
// class Derived;
|
||||
// class X {
|
||||
// Derived* d;
|
||||
// };
|
||||
// class Base {};
|
||||
// void useBase(Base* b);
|
||||
|
||||
// class Derived: Base {};
|
||||
// void test() {
|
||||
// X x;
|
||||
// useBase(x.d);
|
||||
// }
|
||||
public void testLateDefinitionOfInheritance_Bug292749() throws Exception {
|
||||
getBindingFromASTName("useBase(x.d", 7, ICPPFunction.class);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,25 @@
|
|||
/*******************************************************************************
|
||||
* Copyright (c) 2009 Wind River Systems, Inc. and others.
|
||||
* All rights reserved. This program and the accompanying materials
|
||||
* are made available under the terms of the Eclipse Public License v1.0
|
||||
* which accompanies this distribution, and is available at
|
||||
* http://www.eclipse.org/legal/epl-v10.html
|
||||
*
|
||||
* Contributors:
|
||||
* Markus Schorn - initial API and implementation
|
||||
*******************************************************************************/
|
||||
package org.eclipse.cdt.internal.index.tests;
|
||||
|
||||
import junit.framework.TestSuite;
|
||||
|
||||
public class IndexCPPBindingResolutionBugsSingleProjectFirstAST extends IndexCPPBindingResolutionBugs {
|
||||
public IndexCPPBindingResolutionBugsSingleProjectFirstAST() {
|
||||
setStrategy(new SinglePDOMTestFirstASTStrategy(true));
|
||||
}
|
||||
public static TestSuite suite() {return suite(IndexCPPBindingResolutionBugsSingleProjectFirstAST.class);}
|
||||
// invalid tests for this strategy, they assume that the second file is already indexed.
|
||||
@Override public void testBug208558() {}
|
||||
@Override public void testBug176708_CCE() {}
|
||||
@Override public void testIsSameAnonymousType_Bug193962() {}
|
||||
@Override public void testIsSameNestedAnonymousType_Bug193962() {}
|
||||
}
|
|
@ -35,6 +35,7 @@ import org.eclipse.cdt.core.dom.ast.cpp.ICPPMethod;
|
|||
import org.eclipse.cdt.internal.core.dom.parser.ASTNode;
|
||||
import org.eclipse.cdt.internal.core.dom.parser.IASTAmbiguityParent;
|
||||
import org.eclipse.cdt.internal.core.dom.parser.cpp.semantics.CPPSemantics;
|
||||
import org.eclipse.cdt.internal.core.dom.parser.cpp.semantics.SemanticUtil;
|
||||
|
||||
/**
|
||||
* @author jcamelon
|
||||
|
@ -188,11 +189,11 @@ public class CPPASTFieldReference extends ASTNode implements
|
|||
IBinding binding = name.resolvePreBinding();
|
||||
try {
|
||||
if (binding instanceof IVariable) {
|
||||
return ((IVariable) binding).getType();
|
||||
return SemanticUtil.mapToAST(((IVariable) binding).getType(), this);
|
||||
} else if (binding instanceof IEnumerator) {
|
||||
return ((IEnumerator) binding).getType();
|
||||
} else if (binding instanceof IFunction) {
|
||||
return ((IFunction) binding).getType();
|
||||
return SemanticUtil.mapToAST(((IFunction) binding).getType(), this);
|
||||
} else if (binding instanceof ICPPUnknownBinding) {
|
||||
return CPPUnknownClass.createUnnamedInstance();
|
||||
} else if (binding instanceof IProblemBinding) {
|
||||
|
|
|
@ -213,9 +213,9 @@ public class CPPASTFunctionCallExpression extends ASTNode implements
|
|||
return new ProblemBinding(this, IProblemBinding.SEMANTIC_BAD_SCOPE,
|
||||
binding.getName().toCharArray());
|
||||
} else if (binding instanceof IFunction) {
|
||||
t = ((IFunction) binding).getType();
|
||||
t = SemanticUtil.mapToAST(((IFunction) binding).getType(), this);
|
||||
} else if (binding instanceof IVariable) {
|
||||
t = ((IVariable) binding).getType();
|
||||
t = SemanticUtil.mapToAST(((IVariable) binding).getType(), this);
|
||||
} else if (binding instanceof IType) {
|
||||
return (IType) binding; // constructor or simple type initializer
|
||||
} else if (binding instanceof IProblemBinding) {
|
||||
|
|
|
@ -26,6 +26,7 @@ import org.eclipse.cdt.core.dom.ast.cpp.ICPPClassType;
|
|||
import org.eclipse.cdt.core.dom.ast.cpp.ICPPTemplateNonTypeParameter;
|
||||
import org.eclipse.cdt.internal.core.dom.parser.ASTNode;
|
||||
import org.eclipse.cdt.internal.core.dom.parser.cpp.semantics.CPPSemantics;
|
||||
import org.eclipse.cdt.internal.core.dom.parser.cpp.semantics.SemanticUtil;
|
||||
|
||||
/**
|
||||
* @author jcamelon
|
||||
|
@ -91,13 +92,13 @@ public class CPPASTIdExpression extends ASTNode implements IASTIdExpression, IAS
|
|||
IBinding binding = name.resolvePreBinding();
|
||||
try {
|
||||
if (binding instanceof IVariable) {
|
||||
return ((IVariable) binding).getType();
|
||||
return SemanticUtil.mapToAST(((IVariable) binding).getType(), this);
|
||||
} else if (binding instanceof IEnumerator) {
|
||||
return ((IEnumerator) binding).getType();
|
||||
} else if (binding instanceof IProblemBinding) {
|
||||
return (IType) binding;
|
||||
} else if (binding instanceof IFunction) {
|
||||
return ((IFunction) binding).getType();
|
||||
return SemanticUtil.mapToAST(((IFunction) binding).getType(), this);
|
||||
} else if (binding instanceof ICPPTemplateNonTypeParameter) {
|
||||
return ((ICPPTemplateNonTypeParameter) binding).getType();
|
||||
} else if (binding instanceof ICPPClassType) {
|
||||
|
|
|
@ -17,6 +17,7 @@ import java.util.HashMap;
|
|||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
|
||||
import org.eclipse.cdt.core.dom.IName;
|
||||
import org.eclipse.cdt.core.dom.ast.DOMException;
|
||||
import org.eclipse.cdt.core.dom.ast.IBinding;
|
||||
import org.eclipse.cdt.core.dom.ast.IProblemBinding;
|
||||
|
@ -194,10 +195,20 @@ class BaseClassLookup {
|
|||
// assume that there are no bases
|
||||
}
|
||||
if (grandBases != null && grandBases.length > 0) {
|
||||
HashSet<IBinding> grandBaseBindings= grandBases.length > 1 ? new HashSet<IBinding>() : null;
|
||||
for (ICPPBase grandBase : grandBases) {
|
||||
HashSet<IBinding> grandBaseBindings= null;
|
||||
BitSet selectedBases= null;
|
||||
if (grandBases.length > 1) {
|
||||
grandBaseBindings= new HashSet<IBinding>();
|
||||
|
||||
// if we have reachable bases, then ignore the others
|
||||
selectedBases = selectPreferredBases(data, grandBases);
|
||||
}
|
||||
for (int i = 0; i < grandBases.length; i++) {
|
||||
ICPPBase grandBase = grandBases[i];
|
||||
if (grandBase instanceof IProblemBinding)
|
||||
continue;
|
||||
if (selectedBases != null && !selectedBases.get(i))
|
||||
continue;
|
||||
|
||||
try {
|
||||
IBinding grandBaseBinding = grandBase.getBaseClass();
|
||||
|
@ -238,6 +249,31 @@ class BaseClassLookup {
|
|||
return result;
|
||||
}
|
||||
|
||||
private static BitSet selectPreferredBases(LookupData data, ICPPBase[] grandBases) {
|
||||
if (data.contentAssist)
|
||||
return null;
|
||||
|
||||
BitSet selectedBases;
|
||||
selectedBases= new BitSet(grandBases.length);
|
||||
IName baseName= null;
|
||||
for (int i = 0; i < grandBases.length; i++) {
|
||||
ICPPBase nbase = grandBases[i];
|
||||
if (nbase instanceof IProblemBinding)
|
||||
continue;
|
||||
|
||||
final IName nbaseName = nbase.getBaseClassSpecifierName();
|
||||
int cmp= baseName == null ? -1 : CPPSemantics.compareByRelevance(data, baseName, nbaseName);
|
||||
if (cmp <= 0) {
|
||||
if (cmp < 0) {
|
||||
selectedBases.clear();
|
||||
baseName= nbaseName;
|
||||
}
|
||||
selectedBases.set(i);
|
||||
}
|
||||
}
|
||||
return selectedBases;
|
||||
}
|
||||
|
||||
static void hideVirtualBases(BaseClassLookup rootInfo, HashMap<IScope, BaseClassLookup> infoMap) {
|
||||
boolean containsVirtualBase= false;
|
||||
final BaseClassLookup[] allInfos = infoMap.values().toArray(new BaseClassLookup[infoMap.size()]);
|
||||
|
|
|
@ -24,6 +24,7 @@ import java.util.HashSet;
|
|||
import java.util.List;
|
||||
import java.util.Set;
|
||||
|
||||
import org.eclipse.cdt.core.dom.IName;
|
||||
import org.eclipse.cdt.core.dom.ast.ASTNodeProperty;
|
||||
import org.eclipse.cdt.core.dom.ast.ASTVisitor;
|
||||
import org.eclipse.cdt.core.dom.ast.DOMException;
|
||||
|
@ -136,7 +137,9 @@ import org.eclipse.cdt.core.dom.ast.cpp.ICPPUsingDirective;
|
|||
import org.eclipse.cdt.core.dom.ast.cpp.ICPPVariable;
|
||||
import org.eclipse.cdt.core.dom.ast.cpp.ICPPASTCompositeTypeSpecifier.ICPPASTBaseSpecifier;
|
||||
import org.eclipse.cdt.core.index.IIndexBinding;
|
||||
import org.eclipse.cdt.core.index.IIndexFile;
|
||||
import org.eclipse.cdt.core.index.IIndexFileSet;
|
||||
import org.eclipse.cdt.core.index.IIndexName;
|
||||
import org.eclipse.cdt.core.parser.IProblem;
|
||||
import org.eclipse.cdt.core.parser.util.ArrayUtil;
|
||||
import org.eclipse.cdt.core.parser.util.CharArrayObjectMap;
|
||||
|
@ -183,6 +186,7 @@ import org.eclipse.cdt.internal.core.dom.parser.cpp.OverloadableOperator;
|
|||
import org.eclipse.cdt.internal.core.dom.parser.cpp.semantics.Conversions.UDCMode;
|
||||
import org.eclipse.cdt.internal.core.dom.parser.cpp.semantics.Cost.Rank;
|
||||
import org.eclipse.cdt.internal.core.index.IIndexScope;
|
||||
import org.eclipse.core.runtime.CoreException;
|
||||
|
||||
/**
|
||||
* Name resolution
|
||||
|
@ -1788,6 +1792,36 @@ public class CPPSemantics {
|
|||
return 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Compares two bindings for relevance in the context of an AST. AST bindings are
|
||||
* considered more relevant than index ones since the index may be out of date,
|
||||
* built for a different configuration, etc. Index bindings reachable through includes
|
||||
* are more relevant than unreachable ones.
|
||||
* @param ast
|
||||
* @param b1
|
||||
* @param b2
|
||||
* @return 1 if binding <code>b1</code> is more relevant than <code>b2</code>; 0 if
|
||||
* the two bindings have the same relevance; -1 if <code>b1</code> is less relevant than
|
||||
* <code>b2</code>.
|
||||
*/
|
||||
static int compareByRelevance(LookupData data, IName b1, IName b2) {
|
||||
boolean b1FromIndex= (b1 instanceof IIndexName);
|
||||
boolean b2FromIndex= (b2 instanceof IIndexName);
|
||||
if (b1FromIndex != b2FromIndex) {
|
||||
return !b1FromIndex ? 1 : -1;
|
||||
} else if (b1FromIndex) {
|
||||
// Both are from index.
|
||||
if (data.tu != null) {
|
||||
boolean b1Reachable= isReachableFromAst(data.tu, b1);
|
||||
boolean b2Reachable= isReachableFromAst(data.tu, b2);
|
||||
if (b1Reachable != b2Reachable) {
|
||||
return b1Reachable ? 1 : -1;
|
||||
}
|
||||
}
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Compares a binding with a list of function candidates for relevance in the context of an AST. AST bindings are
|
||||
* considered more relevant than index ones since the index may be out of date,
|
||||
|
@ -1864,6 +1898,28 @@ public class CPPSemantics {
|
|||
return indexFileSet != null && indexFileSet.containsDeclaration(indexBinding);
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if a binding is an AST binding, or is reachable from the AST through includes.
|
||||
* The binding is assumed to belong to the AST, if it is not an IIndexBinding and not
|
||||
* a specialization of an IIndexBinding.
|
||||
* @param ast
|
||||
* @param binding
|
||||
* @return <code>true</code> if the <code>binding</code> is reachable from <code>ast</code>.
|
||||
*/
|
||||
private static boolean isReachableFromAst(IASTTranslationUnit ast, IName name) {
|
||||
if (!(name instanceof IIndexName)) {
|
||||
return true;
|
||||
}
|
||||
IIndexName indexName = (IIndexName) name;
|
||||
try {
|
||||
IIndexFile file= indexName.getFile();
|
||||
IIndexFileSet indexFileSet = ast.getIndexFileSet();
|
||||
return indexFileSet != null && indexFileSet.contains(file);
|
||||
} catch (CoreException e) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
static private void reduceToViable(LookupData data, IBinding[] functions) throws DOMException {
|
||||
if (functions == null || functions.length == 0)
|
||||
return;
|
||||
|
@ -2452,7 +2508,7 @@ public class CPPSemantics {
|
|||
* Also collections the function bindings if requested.
|
||||
*/
|
||||
public static IType getChainedMemberAccessOperatorReturnType(ICPPASTFieldReference fieldReference, Collection<ICPPFunction> functionBindings) throws DOMException {
|
||||
IASTExpression owner = fieldReference.getFieldOwner();
|
||||
final IASTExpression owner = fieldReference.getFieldOwner();
|
||||
if (owner == null)
|
||||
return null;
|
||||
|
||||
|
@ -2505,7 +2561,7 @@ public class CPPSemantics {
|
|||
if (functionBindings != null)
|
||||
functionBindings.add(op);
|
||||
|
||||
type= op.getType().getReturnType();
|
||||
type= SemanticUtil.mapToAST(op.getType().getReturnType(), owner);
|
||||
foundOperator= true;
|
||||
}
|
||||
|
||||
|
|
|
@ -594,9 +594,8 @@ public class Conversions {
|
|||
}
|
||||
}
|
||||
|
||||
// This should actually be done in 'checkImplicitConversionSequence', see 13.3.3.1-6 and 8.5.14
|
||||
// 8.5.14 cv-qualifiers can be ignored for non-class types
|
||||
// mstodo
|
||||
// This should actually be done before the conversion is attempted, see for instance 13.3.3.1-6 and 8.5.14.
|
||||
// However, it does not hurt to do it here either.
|
||||
IType unqualifiedTarget= getNestedType(target, CVQ | PTR_CVQ | TDEF | REF);
|
||||
if (!(unqualifiedTarget instanceof ICPPClassType)) {
|
||||
IType unqualifiedSource= getNestedType(source, CVQ | PTR_CVQ | TDEF | REF);
|
||||
|
|
|
@ -15,6 +15,8 @@ package org.eclipse.cdt.internal.core.dom.parser.cpp.semantics;
|
|||
|
||||
import org.eclipse.cdt.core.CCorePlugin;
|
||||
import org.eclipse.cdt.core.dom.ast.DOMException;
|
||||
import org.eclipse.cdt.core.dom.ast.IASTNode;
|
||||
import org.eclipse.cdt.core.dom.ast.IASTTranslationUnit;
|
||||
import org.eclipse.cdt.core.dom.ast.IArrayType;
|
||||
import org.eclipse.cdt.core.dom.ast.IBinding;
|
||||
import org.eclipse.cdt.core.dom.ast.IFunctionType;
|
||||
|
@ -37,6 +39,7 @@ import org.eclipse.cdt.core.parser.util.CharArraySet;
|
|||
import org.eclipse.cdt.core.parser.util.CharArrayUtils;
|
||||
import org.eclipse.cdt.core.parser.util.ObjectSet;
|
||||
import org.eclipse.cdt.internal.core.dom.parser.ITypeContainer;
|
||||
import org.eclipse.cdt.internal.core.dom.parser.cpp.CPPASTTranslationUnit;
|
||||
import org.eclipse.cdt.internal.core.dom.parser.cpp.CPPFunctionType;
|
||||
import org.eclipse.cdt.internal.core.dom.parser.cpp.CPPPointerToMemberType;
|
||||
import org.eclipse.cdt.internal.core.dom.parser.cpp.CPPPointerType;
|
||||
|
@ -44,6 +47,7 @@ import org.eclipse.cdt.internal.core.dom.parser.cpp.CPPQualifierType;
|
|||
import org.eclipse.cdt.internal.core.dom.parser.cpp.CPPTemplateArgument;
|
||||
import org.eclipse.cdt.internal.core.dom.parser.cpp.ICPPDeferredClassInstance;
|
||||
import org.eclipse.cdt.internal.core.dom.parser.cpp.OverloadableOperator;
|
||||
import org.eclipse.cdt.internal.core.index.IIndexType;
|
||||
|
||||
/**
|
||||
*
|
||||
|
@ -290,6 +294,42 @@ public class SemanticUtil {
|
|||
type.setType(newNestedType);
|
||||
return type;
|
||||
}
|
||||
|
||||
public static IType mapToAST(IType type, IASTNode node) {
|
||||
if (!(type instanceof IIndexType))
|
||||
return type;
|
||||
|
||||
try {
|
||||
if (type instanceof IFunctionType) {
|
||||
final ICPPFunctionType ft = (ICPPFunctionType) type;
|
||||
final IType r = ft.getReturnType();
|
||||
final IType ret = mapToAST(r, node);
|
||||
if (ret == r) {
|
||||
return type;
|
||||
}
|
||||
return new CPPFunctionType(ret, ft.getParameterTypes(), ft.isConst(), ft.isVolatile());
|
||||
}
|
||||
if (type instanceof ITypeContainer) {
|
||||
final ITypeContainer tc = (ITypeContainer) type;
|
||||
final IType nestedType= tc.getType();
|
||||
if (nestedType == null)
|
||||
return type;
|
||||
|
||||
IType newType= mapToAST(nestedType, node);
|
||||
if (newType != nestedType) {
|
||||
return replaceNestedType(tc, newType);
|
||||
}
|
||||
return type;
|
||||
} else if (type instanceof ICPPClassType && type instanceof IIndexType) {
|
||||
IASTTranslationUnit tu = node.getTranslationUnit();
|
||||
if (tu instanceof CPPASTTranslationUnit) {
|
||||
return ((CPPASTTranslationUnit) tu).mapToAST((ICPPClassType) type);
|
||||
}
|
||||
}
|
||||
} catch (DOMException e) {
|
||||
}
|
||||
return type;
|
||||
}
|
||||
|
||||
public static IType[] getSimplifiedTypes(IType[] types) {
|
||||
// Don't create a new array until it's really needed.
|
||||
|
|
Loading…
Add table
Reference in a new issue