| /* |
| * Licensed to the Apache Software Foundation (ASF) under one |
| * or more contributor license agreements. See the NOTICE file |
| * distributed with this work for additional information |
| * regarding copyright ownership. The ASF licenses this file |
| * to you under the Apache License, Version 2.0 (the |
| * "License"); you may not use this file except in compliance |
| * with the License. You may obtain a copy of the License at |
| * |
| * http://www.apache.org/licenses/LICENSE-2.0 |
| * |
| * Unless required by applicable law or agreed to in writing, |
| * software distributed under the License is distributed on an |
| * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY |
| * KIND, either express or implied. See the License for the |
| * specific language governing permissions and limitations |
| * under the License. |
| */ |
| package org.codehaus.groovy.classgen.asm.sc; |
| |
| import org.codehaus.groovy.ast.ASTNode; |
| import org.codehaus.groovy.ast.ClassHelper; |
| import org.codehaus.groovy.ast.ClassNode; |
| import org.codehaus.groovy.ast.GenericsType; |
| import org.codehaus.groovy.ast.MethodNode; |
| import org.codehaus.groovy.ast.Parameter; |
| import org.codehaus.groovy.ast.expr.ArgumentListExpression; |
| import org.codehaus.groovy.ast.expr.ArrayExpression; |
| import org.codehaus.groovy.ast.expr.ClassExpression; |
| import org.codehaus.groovy.ast.expr.Expression; |
| import org.codehaus.groovy.ast.expr.MethodCallExpression; |
| import org.codehaus.groovy.ast.expr.MethodReferenceExpression; |
| import org.codehaus.groovy.ast.tools.GeneralUtils; |
| import org.codehaus.groovy.classgen.asm.BytecodeHelper; |
| import org.codehaus.groovy.classgen.asm.MethodReferenceExpressionWriter; |
| import org.codehaus.groovy.classgen.asm.WriterController; |
| import org.codehaus.groovy.control.MultipleCompilationErrorsException; |
| import org.codehaus.groovy.syntax.RuntimeParserException; |
| import org.codehaus.groovy.transform.sc.StaticCompilationMetadataKeys; |
| import org.codehaus.groovy.transform.stc.ExtensionMethodNode; |
| import org.codehaus.groovy.transform.stc.StaticTypesMarker; |
| import org.objectweb.asm.Handle; |
| import org.objectweb.asm.Opcodes; |
| |
| import java.util.Arrays; |
| import java.util.Iterator; |
| import java.util.List; |
| import java.util.Map; |
| |
| import static java.util.Comparator.comparingInt; |
| import static java.util.stream.Collectors.joining; |
| import static org.codehaus.groovy.ast.tools.GeneralUtils.callX; |
| import static org.codehaus.groovy.ast.tools.GeneralUtils.classX; |
| import static org.codehaus.groovy.ast.tools.GeneralUtils.ctorX; |
| import static org.codehaus.groovy.ast.tools.GeneralUtils.getInterfacesAndSuperInterfaces; |
| import static org.codehaus.groovy.ast.tools.GeneralUtils.nullX; |
| import static org.codehaus.groovy.ast.tools.GeneralUtils.returnS; |
| import static org.codehaus.groovy.ast.tools.GeneralUtils.varX; |
| import static org.codehaus.groovy.ast.tools.GenericsUtils.extractPlaceholders; |
| import static org.codehaus.groovy.ast.tools.ParameterUtils.isVargs; |
| import static org.codehaus.groovy.ast.tools.ParameterUtils.parametersCompatible; |
| import static org.codehaus.groovy.runtime.DefaultGroovyMethods.last; |
| import static org.codehaus.groovy.transform.stc.StaticTypeCheckingSupport.allParametersAndArgumentsMatch; |
| import static org.codehaus.groovy.transform.stc.StaticTypeCheckingSupport.filterMethodsByVisibility; |
| import static org.codehaus.groovy.transform.stc.StaticTypeCheckingSupport.findDGMMethodsForClassNode; |
| import static org.codehaus.groovy.transform.stc.StaticTypeCheckingSupport.isAssignableTo; |
| import static org.codehaus.groovy.transform.stc.StaticTypeCheckingSupport.resolveClassNodeGenerics; |
| |
| /** |
| * Generates bytecode for method reference expressions in statically-compiled code. |
| * |
| * @since 3.0.0 |
| */ |
| public class StaticTypesMethodReferenceExpressionWriter extends MethodReferenceExpressionWriter implements AbstractFunctionalInterfaceWriter { |
| |
| public StaticTypesMethodReferenceExpressionWriter(final WriterController controller) { |
| super(controller); |
| } |
| |
| @Override |
| public void writeMethodReferenceExpression(final MethodReferenceExpression methodReferenceExpression) { |
| // functional interface target is required for native method reference generation |
| ClassNode functionalType = methodReferenceExpression.getNodeMetaData(StaticTypesMarker.PARAMETER_TYPE); |
| MethodNode abstractMethod = ClassHelper.findSAM(functionalType); |
| if (abstractMethod == null || !functionalType.isInterface()) { |
| // generate the default bytecode -- most likely a method closure |
| super.writeMethodReferenceExpression(methodReferenceExpression); |
| return; |
| } |
| |
| ClassNode classNode = controller.getClassNode(); |
| Expression typeOrTargetRef = methodReferenceExpression.getExpression(); |
| boolean isClassExpression = (typeOrTargetRef instanceof ClassExpression); |
| boolean targetIsArgument = false; // implied argument for expr::staticMethod? |
| ClassNode typeOrTargetRefType = isClassExpression ? typeOrTargetRef.getType() |
| : controller.getTypeChooser().resolveType(typeOrTargetRef, classNode); |
| |
| ClassNode[] methodReferenceParamTypes = methodReferenceExpression.getNodeMetaData(StaticTypesMarker.CLOSURE_ARGUMENTS); |
| Parameter[] parametersWithExactType = createParametersWithExactType(abstractMethod, methodReferenceParamTypes); |
| String methodRefName = methodReferenceExpression.getMethodName().getText(); |
| boolean isConstructorReference = isConstructorReference(methodRefName); |
| |
| MethodNode methodRefMethod; |
| if (isConstructorReference) { |
| methodRefName = controller.getContext().getNextConstructorReferenceSyntheticMethodName(controller.getMethodNode()); |
| methodRefMethod = addSyntheticMethodForConstructorReference(methodRefName, typeOrTargetRefType, parametersWithExactType); |
| } else { |
| // TODO: move the findMethodRefMethod and checking to StaticTypeCheckingVisitor |
| methodRefMethod = findMethodRefMethod(methodRefName, parametersWithExactType, typeOrTargetRef, typeOrTargetRefType); |
| } |
| |
| validate(methodReferenceExpression, typeOrTargetRefType, methodRefName, methodRefMethod, parametersWithExactType, |
| resolveClassNodeGenerics(extractPlaceholders(functionalType), null, abstractMethod.getReturnType())); |
| |
| if (isExtensionMethod(methodRefMethod)) { |
| ExtensionMethodNode extensionMethodNode = (ExtensionMethodNode) methodRefMethod; |
| methodRefMethod = extensionMethodNode.getExtensionMethodNode(); |
| boolean isStatic = extensionMethodNode.isStaticExtension(); |
| if (isStatic) { // create adapter method to pass extra argument |
| methodRefMethod = addSyntheticMethodForDGSM(methodRefMethod); |
| } |
| if (isStatic || isClassExpression) { |
| // replace expression with declaring type |
| typeOrTargetRefType = methodRefMethod.getDeclaringClass(); |
| typeOrTargetRef = makeClassTarget(typeOrTargetRefType, typeOrTargetRef); |
| } else { // GROOVY-10653 |
| targetIsArgument = true; // ex: "string"::size |
| } |
| } else if (isVargs(methodRefMethod.getParameters())) { |
| int mParameters = abstractMethod.getParameters().length; |
| int nParameters = methodRefMethod.getParameters().length; |
| if (isTypeReferringInstanceMethod(typeOrTargetRef, methodRefMethod)) nParameters += 1; |
| if (mParameters > nParameters || mParameters == nParameters-1 || (mParameters == nParameters |
| && !isAssignableTo(last(parametersWithExactType).getType(), last(methodRefMethod.getParameters()).getType()))) { |
| // GROOVY-9813: reference to variadic method which needs adapter method to match runtime arguments to its parameters |
| if (!isClassExpression && !methodRefMethod.isStatic() && !methodRefMethod.getDeclaringClass().equals(classNode)) { |
| targetIsArgument = true; // GROOVY-10653: create static adapter in source class with target as first parameter |
| mParameters += 1; |
| } |
| methodRefMethod = addSyntheticMethodForVariadicReference(methodRefMethod, mParameters, isClassExpression || targetIsArgument); |
| if (methodRefMethod.isStatic() && !targetIsArgument) { |
| // replace expression with declaring type |
| typeOrTargetRefType = methodRefMethod.getDeclaringClass(); |
| typeOrTargetRef = makeClassTarget(typeOrTargetRefType, typeOrTargetRef); |
| } |
| } |
| } |
| |
| if (!isClassExpression) { |
| if (isConstructorReference) { // TODO: move this check to the parser |
| addFatalError("Constructor reference must be TypeName::new", methodReferenceExpression); |
| } else if (methodRefMethod.isStatic() && !targetIsArgument) { |
| // "string"::valueOf refers to static method, so instance is superfluous |
| typeOrTargetRef = makeClassTarget(typeOrTargetRefType, typeOrTargetRef); |
| isClassExpression = true; |
| } else { |
| typeOrTargetRef.visit(controller.getAcg()); |
| } |
| } |
| |
| int referenceKind; |
| if (isConstructorReference || methodRefMethod.isStatic()) { |
| referenceKind = Opcodes.H_INVOKESTATIC; |
| } else if (methodRefMethod.getDeclaringClass().isInterface()) { |
| referenceKind = Opcodes.H_INVOKEINTERFACE; // GROOVY-9853 |
| } else { |
| referenceKind = Opcodes.H_INVOKEVIRTUAL; |
| } |
| |
| String methodName = abstractMethod.getName(); |
| String methodDesc = BytecodeHelper.getMethodDescriptor(functionalType.redirect(), |
| isClassExpression ? Parameter.EMPTY_ARRAY : new Parameter[]{new Parameter(typeOrTargetRefType, "__METHODREF_EXPR_INSTANCE")}); |
| |
| Handle bootstrapMethod = createBootstrapMethod(classNode.isInterface(), false); |
| Object[] bootstrapArgs = createBootstrapMethodArguments( |
| createMethodDescriptor(abstractMethod), |
| referenceKind, |
| methodRefMethod.getDeclaringClass(), |
| methodRefMethod, |
| parametersWithExactType, |
| false |
| ); |
| controller.getMethodVisitor().visitInvokeDynamicInsn(methodName, methodDesc, bootstrapMethod, bootstrapArgs); |
| |
| if (isClassExpression) { |
| controller.getOperandStack().push(functionalType); |
| } else { |
| controller.getOperandStack().replace(functionalType, 1); |
| } |
| } |
| |
| private void validate(final MethodReferenceExpression methodReference, final ClassNode targetType, final String methodName, final MethodNode methodNode, final Parameter[] samParameters, final ClassNode samReturnType) { |
| if (methodNode == null) { |
| String error; |
| if (!(methodReference.getExpression() instanceof ClassExpression)) { |
| error = "Failed to find method '%s(%s)'"; |
| } else { |
| error = "Failed to find class method '%s(%s)'"; |
| if (samParameters.length > 0) |
| error += " or instance method '%1$s(" + Arrays.stream(samParameters).skip(1).map(e -> e.getType().toString(false)).collect(joining(",")) + ")'"; |
| } |
| error = String.format(error + " for the type: %s", methodName, Arrays.stream(samParameters).map(e -> e.getType().toString(false)).collect(joining(",")), targetType.toString(false)); |
| addFatalError(error, methodReference); |
| } else if (methodNode.isVoidMethod() && !ClassHelper.isPrimitiveVoid(samReturnType)) { |
| addFatalError("Invalid return type: void is not convertible to " + samReturnType.getText(), methodReference); |
| } else if (samParameters.length > 0 && isTypeReferringInstanceMethod(methodReference.getExpression(), methodNode) && !isAssignableTo(samParameters[0].getType(), targetType)) { |
| throw new RuntimeParserException("Invalid receiver type: " + samParameters[0].getType().getText() + " is not compatible with " + targetType.getText(), methodReference.getExpression()); |
| } |
| } |
| |
| private MethodNode addSyntheticMethodForDGSM(final MethodNode mn) { |
| Parameter[] parameters = removeFirstParameter(mn.getParameters()); |
| ArgumentListExpression args = new ArgumentListExpression(parameters); |
| args.getExpressions().add(0, nullX()); |
| |
| MethodCallExpression methodCall = callX(classX(mn.getDeclaringClass()), mn.getName(), args); |
| methodCall.setImplicitThis(false); |
| methodCall.setMethodTarget(mn); |
| methodCall.putNodeMetaData(StaticTypesMarker.DIRECT_METHOD_CALL_TARGET, mn); |
| |
| String methodName = "dgsm$$" + mn.getParameters()[0].getType().getName().replace('.', '$') + "$$" + mn.getName(); |
| |
| MethodNode delegateMethod = addSyntheticMethod(methodName, mn.getReturnType(), methodCall, parameters, mn.getExceptions()); |
| delegateMethod.putNodeMetaData(StaticCompilationMetadataKeys.STATIC_COMPILE_NODE, Boolean.TRUE); |
| return delegateMethod; |
| } |
| |
| private MethodNode addSyntheticMethodForVariadicReference(final MethodNode mn, final int samParameters, final boolean isStaticTarget) { |
| Parameter[] parameters = new Parameter[samParameters]; |
| Expression arguments, receiver; |
| if (mn.isStatic()) { |
| for (int i = 0, j = mn.getParameters().length-1; i < samParameters; i += 1) { |
| ClassNode t = mn.getParameters()[Math.min(i, j)].getType(); |
| if (i >= j) t = t.getComponentType(); // targets the array |
| parameters[i] = new Parameter(t, "p" + i); |
| } |
| arguments = new ArgumentListExpression(parameters); |
| receiver = classX(mn.getDeclaringClass()); |
| } else { |
| int p = 0; |
| if (isStaticTarget) parameters[p++] = new Parameter(mn.getDeclaringClass(), "target"); |
| for (int i = 0, j = mn.getParameters().length-1, n = samParameters-p; i < n; i += 1) { |
| ClassNode t = mn.getParameters()[Math.min(i, j)].getType(); |
| if (i >= j) t = t.getComponentType(); // targets the array |
| parameters[p] = new Parameter(t, "p" + p); p += 1; |
| } |
| if (isStaticTarget) { |
| arguments = new ArgumentListExpression(removeFirstParameter(parameters)); |
| receiver = varX(parameters[0]); |
| } else { |
| arguments = new ArgumentListExpression(parameters); |
| receiver = varX("this", controller.getClassNode()); |
| } |
| } |
| |
| MethodCallExpression methodCall = callX(receiver, mn.getName(), arguments); |
| methodCall.setImplicitThis(false); |
| methodCall.setMethodTarget(mn); |
| methodCall.putNodeMetaData(StaticTypesMarker.DIRECT_METHOD_CALL_TARGET, mn); |
| |
| String methodName = "adapt$" + mn.getDeclaringClass().getNameWithoutPackage() + "$" + mn.getName() + "$" + System.nanoTime(); |
| |
| MethodNode delegateMethod = addSyntheticMethod(methodName, mn.getReturnType(), methodCall, parameters, mn.getExceptions()); |
| if (!isStaticTarget && !mn.isStatic()) delegateMethod.setModifiers(delegateMethod.getModifiers() & ~Opcodes.ACC_STATIC); |
| delegateMethod.putNodeMetaData(StaticCompilationMetadataKeys.STATIC_COMPILE_NODE, Boolean.TRUE); |
| delegateMethod.setGenericsTypes(mn.getGenericsTypes()); |
| return delegateMethod; |
| } |
| |
| private MethodNode addSyntheticMethodForConstructorReference(final String methodName, final ClassNode returnType, final Parameter[] parametersWithExactType) { |
| ArgumentListExpression ctorArgs = new ArgumentListExpression(parametersWithExactType); |
| |
| Expression returnValue; |
| if (returnType.isArray()) { |
| returnValue = new ArrayExpression( |
| returnType.getComponentType(), |
| null, ctorArgs.getExpressions()); |
| } else { |
| returnValue = ctorX(returnType, ctorArgs); |
| } |
| |
| MethodNode delegateMethod = addSyntheticMethod(methodName, returnType, returnValue, parametersWithExactType, ClassNode.EMPTY_ARRAY); |
| // TODO: if StaticTypesMarker.DIRECT_METHOD_CALL_TARGET or |
| // OptimizingStatementWriter.StatementMeta.class metadatas |
| // can bet set for the ctorX above, then this can be TRUE: |
| delegateMethod.putNodeMetaData(StaticCompilationMetadataKeys.STATIC_COMPILE_NODE, Boolean.FALSE); |
| return delegateMethod; |
| } |
| |
| private MethodNode addSyntheticMethod(final String methodName, final ClassNode returnType, final Expression returnValue, final Parameter[] parameters, final ClassNode[] exceptions) { |
| return controller.getClassNode().addSyntheticMethod( |
| methodName, |
| Opcodes.ACC_PRIVATE | Opcodes.ACC_STATIC | Opcodes.ACC_FINAL, |
| returnType, |
| parameters, |
| exceptions, |
| returnS(returnValue)); |
| } |
| |
| private Parameter[] createParametersWithExactType(final MethodNode abstractMethod, final ClassNode[] inferredParamTypes) { |
| // MUST clone the parameters to avoid impacting the original parameter type of SAM |
| Parameter[] parameters = GeneralUtils.cloneParams(abstractMethod.getParameters()); |
| |
| if (inferredParamTypes != null) { |
| for (int i = 0, n = parameters.length; i < n; i += 1) { |
| ClassNode inferredParamType = inferredParamTypes[i]; |
| if (inferredParamType == null) continue; |
| |
| Parameter parameter = parameters[i]; |
| Parameter targetParameter = parameter; |
| |
| ClassNode type = convertParameterType(targetParameter.getType(), parameter.getType(), inferredParamType); |
| parameter.setOriginType(type); |
| parameter.setType(type); |
| } |
| } |
| |
| return parameters; |
| } |
| |
| private MethodNode findMethodRefMethod(final String methodName, final Parameter[] samParameters, final Expression typeOrTargetRef, final ClassNode typeOrTargetRefType) { |
| List<MethodNode> methods = findVisibleMethods(methodName, typeOrTargetRefType); |
| |
| java.util.function.ToIntFunction<Parameter[]> distance = (parameters) -> { // GROOVY-10972: select from closest matches |
| return allParametersAndArgumentsMatch(parameters, Arrays.stream(samParameters).map(Parameter::getType).toArray(ClassNode[]::new)); |
| }; |
| |
| int[] distances = methods.stream().mapToInt(method -> { |
| Parameter[] parameters = method.getParameters(); |
| if (isTypeReferringInstanceMethod(typeOrTargetRef, method)) { |
| // there is an implicit parameter for "String::length" |
| ClassNode firstParamType = method.getDeclaringClass(); |
| |
| int n = parameters.length; |
| Parameter[] plusOne = new Parameter[n + 1]; |
| plusOne[0] = new Parameter(firstParamType, ""); |
| System.arraycopy(parameters, 0, plusOne, 1, n); |
| |
| parameters = plusOne; |
| } |
| |
| if (parameters.length > 0 && !method.isStatic() && !isExtensionMethod(method) |
| && typeOrTargetRefType.redirect().getGenericsTypes() != null) { // GROOVY-10994 |
| Map<GenericsType.GenericsTypeName, GenericsType> spec = extractPlaceholders(typeOrTargetRefType); |
| parameters = Arrays.stream(parameters).map(p -> new Parameter(resolveClassNodeGenerics(spec, null, p.getType()), p.getName())).toArray(Parameter[]::new); |
| } |
| |
| // check direct match |
| if (parametersCompatible(samParameters, parameters)) return distance.applyAsInt(parameters); |
| |
| // check vararg match |
| if (isVargs(parameters)) { |
| int nParameters = parameters.length; |
| if (samParameters.length == nParameters - 1) { // 0 case |
| parameters = Arrays.copyOf(parameters, nParameters - 1); |
| if (parametersCompatible(samParameters, parameters)) return distance.applyAsInt(parameters); |
| } |
| else if (samParameters.length >= nParameters) { // 1+ case |
| Parameter p = new Parameter(parameters[nParameters - 1].getType().getComponentType(), ""); |
| parameters = Arrays.copyOf(parameters, samParameters.length); |
| for (int i = nParameters - 1; i < parameters.length; i += 1){ |
| parameters[i] = p; |
| } |
| if (parametersCompatible(samParameters, parameters)) return distance.applyAsInt(parameters); |
| } |
| } |
| |
| return -1; // no match |
| }).toArray(); |
| |
| int i = 0, bestDistance = Arrays.stream(distances).filter(d -> d >= 0).min().orElse(0); |
| for (Iterator<MethodNode> it = methods.iterator(); it.hasNext(); ) { |
| it.next(); if (distances[i++] != bestDistance) it.remove(); |
| } |
| |
| if (methods.isEmpty()) return null; |
| if (methods.size() == 1) return methods.get(0); |
| return chooseMethodRefMethod(methods, typeOrTargetRef, typeOrTargetRefType); |
| } |
| |
| private MethodNode chooseMethodRefMethod(final List<MethodNode> methods, final Expression typeOrTargetRef, final ClassNode typeOrTargetRefType) { |
| return methods.stream().max(comparingInt((MethodNode mn) -> { |
| int score = 9; |
| for (ClassNode cn = typeOrTargetRefType; cn != null && !cn.equals(mn.getDeclaringClass()); cn = cn.getSuperClass()) { |
| score -= 1; |
| } |
| if (score < 0) { |
| score = 0; |
| } |
| score *= 10; |
| if ((typeOrTargetRef instanceof ClassExpression) == isStaticMethod(mn)) { |
| score += 9; |
| } |
| return score; |
| }).thenComparing(StaticTypesMethodReferenceExpressionWriter::isExtensionMethod)).get(); |
| } |
| |
| private List<MethodNode> findVisibleMethods(final String name, final ClassNode type) { |
| List<MethodNode> methods = type.getMethods(name); |
| // GROOVY-10791: include interface default methods in search |
| for (ClassNode cn : getInterfacesAndSuperInterfaces(type)) { |
| for (MethodNode mn : cn.getDeclaredMethods(name)) { |
| if (mn.isDefault()) methods.add(mn); |
| } |
| } |
| methods.addAll(findDGMMethodsForClassNode(controller.getSourceUnit().getClassLoader(), type, name)); |
| return filterMethodsByVisibility(methods, controller.getClassNode()); |
| } |
| |
| private void addFatalError(final String msg, final ASTNode node) { |
| controller.getSourceUnit().addFatalError(msg, node); |
| // GRECLIPSE: addFatalError won't throw for quick parse |
| throw new MultipleCompilationErrorsException(controller.getSourceUnit().getErrorCollector()); |
| } |
| |
| //-------------------------------------------------------------------------- |
| |
| private static boolean isConstructorReference(final String name) { |
| return "new".equals(name); |
| } |
| |
| private static boolean isExtensionMethod(final MethodNode mn) { |
| return (mn instanceof ExtensionMethodNode); |
| } |
| |
| private static boolean isStaticMethod(final MethodNode mn) { |
| return isExtensionMethod(mn) ? ((ExtensionMethodNode) mn).isStaticExtension() : mn.isStatic(); |
| } |
| |
| private static boolean isTypeReferringInstanceMethod(final Expression typeOrTargetRef, final MethodNode mn) { |
| // class::instanceMethod |
| return (typeOrTargetRef instanceof ClassExpression) && (mn != null && !isStaticMethod(mn)); |
| } |
| |
| private static Expression makeClassTarget(final ClassNode target, final Expression source) { |
| Expression expression = classX(target); |
| expression.setSourcePosition(source); |
| return expression; |
| } |
| |
| private static Parameter[] removeFirstParameter(final Parameter[] parameters) { |
| return Arrays.copyOfRange(parameters, 1, parameters.length); |
| } |
| } |