Skip to content

Commit

Permalink
GROOVY-11090: STC: tuple spread across closure parameter(s)
Browse files Browse the repository at this point in the history
supports `withIndex()`

3_0_X backport
  • Loading branch information
eric-milles committed Aug 8, 2023
1 parent b8902bb commit 071f9b3
Show file tree
Hide file tree
Showing 3 changed files with 213 additions and 48 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -128,6 +128,7 @@
import java.util.LinkedHashSet;
import java.util.LinkedList;
import java.util.List;
import java.util.ListIterator;
import java.util.Map;
import java.util.Objects;
import java.util.Optional;
Expand Down Expand Up @@ -335,12 +336,13 @@ public class StaticTypeCheckingVisitor extends ClassCodeVisitorSupport {
protected static final ClassNode ITERABLE_TYPE = ClassHelper.make(Iterable.class);
private static final ClassNode SET_TYPE = ClassHelper.make(Set.class);

private static List<ClassNode> TUPLE_TYPES = Arrays.stream(ClassHelper.TUPLE_CLASSES).map(ClassHelper::makeWithoutCaching).collect(Collectors.toList());

public static final Statement GENERATED_EMPTY_STATEMENT = EmptyStatement.INSTANCE;

// Cache closure call methods
public static final MethodNode CLOSURE_CALL_NO_ARG = CLOSURE_TYPE.getDeclaredMethod("call", Parameter.EMPTY_ARRAY);
public static final MethodNode CLOSURE_CALL_NO_ARG = CLOSURE_TYPE.getDeclaredMethod("call", Parameter.EMPTY_ARRAY);
public static final MethodNode CLOSURE_CALL_ONE_ARG = CLOSURE_TYPE.getDeclaredMethod("call", new Parameter[]{new Parameter(OBJECT_TYPE, "arg")});
public static final MethodNode CLOSURE_CALL_VARGS = CLOSURE_TYPE.getDeclaredMethod("call", new Parameter[]{new Parameter(OBJECT_TYPE.makeArray(), "args")});
public static final MethodNode CLOSURE_CALL_VARGS = CLOSURE_TYPE.getDeclaredMethod("call", new Parameter[]{new Parameter(OBJECT_TYPE.makeArray(), "args")});

protected final ReturnAdder.ReturnStatementListener returnListener = new ReturnAdder.ReturnStatementListener() {
@Override
Expand Down Expand Up @@ -3025,7 +3027,7 @@ protected void inferClosureParameterTypes(final ClassNode receiver, final Expres
Expression value = annotation.getMember("value");
Expression options = annotation.getMember("options");
Expression conflictResolver = annotation.getMember("conflictResolutionStrategy");
doInferClosureParameterTypes(receiver, arguments, expression, method, value, conflictResolver, options);
processClosureParams(receiver, arguments, expression, method, value, conflictResolver, options);
}
} else if (isSAMType(target.getOriginType())) { // SAM-type coercion
Map<GenericsTypeName, GenericsType> context = extractPlaceHoldersVisibleToDeclaration(receiver, method, arguments);
Expand Down Expand Up @@ -3158,26 +3160,46 @@ private void checkParamType(final Parameter source, final ClassNode target, fina
addStaticTypeError("Expected type " + prettyPrintType(target) + " for " + (lambda ? "lambda" : "closure") + " parameter: " + source.getName(), source);
}

private void doInferClosureParameterTypes(final ClassNode receiver, final Expression arguments, final ClosureExpression expression, final MethodNode selectedMethod, final Expression hintClass, final Expression resolverClass, final Expression options) {
private void processClosureParams(final ClassNode receiver, final Expression arguments, final ClosureExpression expression, final MethodNode selectedMethod, final Expression hintClass, final Expression resolverClass, final Expression options) {
Parameter[] closureParams = hasImplicitParameter(expression) ? new Parameter[]{new Parameter(DYNAMIC_TYPE,"it")} : getParametersSafe(expression);

List<ClassNode[]> closureSignatures = getSignaturesFromHint(selectedMethod, hintClass, options, expression);
List<ClassNode[]> closureSignatures = new LinkedList<>(getSignaturesFromHint(selectedMethod, hintClass, options, expression));
List<ClassNode[]> candidates = new LinkedList<>();
for (ClassNode[] signature : closureSignatures) {
for (ListIterator<ClassNode[]> it = closureSignatures.listIterator(); it.hasNext(); ) { ClassNode[] signature = it.next();
resolveGenericsFromTypeHint(receiver, arguments, selectedMethod, signature);
if (closureParams.length == signature.length) {
candidates.add(signature);
}
if ((closureParams.length > 1 || closureParams.length == 1 && !closureParams[0].getOriginType().equals(OBJECT_TYPE))
&& signature.length == 1 && isOrImplements(signature[0], LIST_TYPE)) { // see ClosureMetaClass#invokeMethod
// list element(s) spread across the closure parameter(s)
int itemCount = TUPLE_TYPES.indexOf(signature[0]);
if (itemCount >= 0) { // GROOVY-11090: Tuple[0-16]
if (itemCount != closureParams.length) {
// for param count error messages
it.add(new ClassNode[itemCount]);
continue;
}
GenericsType[] spec = signature[0].getGenericsTypes();
if (spec != null) { // edge case: Tuple0 falls through
signature = Arrays.stream(spec).map(GenericsType::getType).toArray(ClassNode[]::new);
candidates.add(signature);
continue;
}
}
ClassNode itemType = inferLoopElementType(signature[0]);
signature = new ClassNode[closureParams.length];
Arrays.fill(signature, itemType);
candidates.add(signature);
}
}

if (candidates.isEmpty() && !closureSignatures.isEmpty()) {
String spec = closureSignatures.stream().mapToInt(sig -> sig.length).distinct()
.sorted().mapToObj(Integer::toString).collect(Collectors.joining(" or "));
addError("Incorrect number of parameters. Expected " + spec + " but found " + closureParams.length, expression);
}

if (candidates.size() > 1) {
closureSignatures = new ArrayList<>(candidates);
for (Iterator<ClassNode[]> candIt = candidates.iterator(); candIt.hasNext(); ) {
Expand Down
39 changes: 0 additions & 39 deletions src/test/groovy/bugs/Groovy8816.groovy

This file was deleted.

186 changes: 184 additions & 2 deletions src/test/groovy/transform/stc/ClosureParamTypeInferenceSTCTest.groovy
Original file line number Diff line number Diff line change
Expand Up @@ -149,6 +149,55 @@ class ClosureParamTypeInferenceSTCTest extends StaticTypeCheckingTestCase {
'''
}

// GROOVY-11090, GROOVY-11092
void testFromStringWithGenericType3() {
String foo = '''
void foo(@ClosureParams(value=FromString, options="Tuple2<String,Number>") Closure c) {
c.call( new Tuple2("",42) )
}
'''
/*
assertScript foo + '''
foo { string, number ->
number.doubleValue()
string.toUpperCase()
}
'''
*/
shouldFailWithMessages foo + '''
foo { one, two, xxx -> }
''',
'Incorrect number of parameters. Expected 1 or 2 but found 3'
}

// GROOVY-11090
void testFromStringWithGenericType4() {
assertScript '''
void foo(@ClosureParams(value=FromString, options="List<Tuple2<String,Number>>") Closure c) {
c.call(Collections.singletonList(Tuple.tuple("",(Number)42)))
}
foo {
it.each { string, number ->
number.doubleValue()
string.toUpperCase()
}
}
'''
}

void testFromStringWithGenericType5() {
assertScript '''
void foo(@ClosureParams(value=FromString, options="Optional<Tuple2<String,? extends Number>>") Closure c) {
c(Optional.of(Tuple.tuple("",42)))
}
foo { opt ->
opt.ifPresent {
it.v2.doubleValue()
}
}
'''
}

void testFromStringWithTypeParameter1() {
assertScript '''
def <T> void foo(T t, @ClosureParams(value=FromString, options="T") Closure c) { c.call(t) }
Expand Down Expand Up @@ -310,6 +359,109 @@ class ClosureParamTypeInferenceSTCTest extends StaticTypeCheckingTestCase {
'''
}

// GROOVY-6939
@NotYetImplemented
void testParamCountCheck1() {
shouldFailWithMessages '''
def m(o) {
o.each { x, y -> }
}
''',
'Incorrect number of parameters. Expected 1 but found 2'
}

// GROOVY-6939
@NotYetImplemented
void testParamCountCheck2() {
shouldFailWithMessages '''
def m(o) {
o.eachWithIndex { x, y, z -> }
}
''',
'Incorrect number of parameters. Expected 2 but found 3'
}

// GROOVY-6939
@NotYetImplemented
void testParamCountCheck3() {
shouldFailWithMessages '''
def m(o) {
o.eachWithIndex { print it }
}
''',
'Incorrect number of parameters. Expected 2 but found 1'
}

// GROOVY-6939
void testParamCountCheck4() {
shouldFailWithMessages '''
def m(... array) {
array.each { x, y -> }
}
''',
'Incorrect number of parameters. Expected 1 but found 2'
}

// GROOVY-6939
void testParamCountCheck5() {
shouldFailWithMessages '''
def m() {
[:].each { -> }
}
''',
'Incorrect number of parameters. Expected 1 or 2 but found 0'
}

// GROOVY-8499
void testParamCountCheck6() {
assertScript '''
def result = ['ab'.chars,'12'.chars].combinations { l,n -> "$l$n" }
assert result == ['a1','b1','a2','b2']
'''
// cannot know in advance how many list elements
def err = shouldFail '''
['ab'.chars,'12'.chars].combinations((l,n,x) -> "$l$n")
'''
assert err =~ /No signature of method.* is applicable for argument types: \(ArrayList\)/
}

// GROOVY-8816
void testParamCountCheck7() {
shouldFailWithMessages '''
def m() {
[].each { -> }
}
''',
'Incorrect number of parameters. Expected 1 but found 0'
}

// GROOVY-9854
@NotYetImplemented
void testParamCountCheck8() {
shouldFailWithMessages '''
switch (42) { case { -> }: break; }
''',
'Incorrect number of parameters. Expected 1 but found 0'
}

// GROOVY-9854
@NotYetImplemented
void testParamCountCheck9() {
shouldFailWithMessages '''
switch (42) { case { i, j -> }: break; }
''',
'Incorrect number of parameters. Expected 1 but found 2'
}

// GROOVY-11089
void testParamCountCheck10() {
shouldFailWithMessages '''
def array = new String[]{'a','b'}
array.with { a,b -> }
''',
'Incorrect number of parameters. Expected 1 but found 2'
}

// GROOVY-7141
void testInferenceWithSAMTypeCoercion1() {
String sam = '''
Expand Down Expand Up @@ -654,7 +806,24 @@ class ClosureParamTypeInferenceSTCTest extends StaticTypeCheckingTestCase {
assert [1234, 3.14].collect { it.intValue() } == [1234,3]
'''
}
void testDGM_collectMap() {
void testDGM_collectOnList() { // GROOVY-11090
assertScript '''
def list_of_tuple2 = ['a','b'].withIndex()
def list_of_string = list_of_tuple2.collect { it.v1 + it.v2 }
assert list_of_string == ['a0','b1']
'''

for (spec in ['s,i','String s,int i']) {
assertScript """
def list_of_tuple2 = ['a','b'].withIndex()
def list_of_string = list_of_tuple2.collect { $spec -> s + i }
assert list_of_string == ['a0','b1']
"""
}
}
void testDGM_collectOnMap() {
assertScript '''
assert [a: 'foo',b:'bar'].collect { k,v -> k+v } == ['afoo','bbar']
assert [a: 'foo',b:'bar'].collect { e -> e.key+e.value } == ['afoo','bbar']
Expand Down Expand Up @@ -1599,7 +1768,20 @@ class ClosureParamTypeInferenceSTCTest extends StaticTypeCheckingTestCase {
'''
}

void testDGM_with() {
@NotYetImplemented
void testDGM_with0() { // GROOVY-11090: edge case
assertScript '''
Tuple0.INSTANCE.with { -> }
'''
assertScript '''
Tuple0.INSTANCE.with {
assert it instanceof List
assert it instanceof Tuple
assert it === Tuple0.INSTANCE
}
'''
}
void testDGM_with1() {
assertScript '''
"string".with { it.toUpperCase() }
'''
Expand Down

0 comments on commit 071f9b3

Please sign in to comment.