/*
 *  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.transform.stc;

import groovy.lang.Closure;
import groovy.lang.DelegatesTo;
import groovy.lang.IntRange;
import groovy.lang.Tuple2;
import groovy.transform.NamedParam;
import groovy.transform.NamedParams;
import groovy.transform.TypeChecked;
import groovy.transform.TypeCheckingMode;
import groovy.transform.stc.ClosureParams;
import groovy.transform.stc.ClosureSignatureConflictResolver;
import groovy.transform.stc.ClosureSignatureHint;
import org.apache.groovy.util.SystemUtil;
import org.codehaus.groovy.GroovyBugError;
import org.codehaus.groovy.ast.ASTNode;
import org.codehaus.groovy.ast.AnnotatedNode;
import org.codehaus.groovy.ast.AnnotationNode;
import org.codehaus.groovy.ast.ClassCodeVisitorSupport;
import org.codehaus.groovy.ast.ClassHelper;
import org.codehaus.groovy.ast.ClassNode;
import org.codehaus.groovy.ast.CodeVisitorSupport;
import org.codehaus.groovy.ast.ConstructorNode;
import org.codehaus.groovy.ast.DynamicVariable;
import org.codehaus.groovy.ast.FieldNode;
import org.codehaus.groovy.ast.GenericsType;
import org.codehaus.groovy.ast.GenericsType.GenericsTypeName;
import org.codehaus.groovy.ast.GroovyCodeVisitor;
import org.codehaus.groovy.ast.InnerClassNode;
import org.codehaus.groovy.ast.MethodNode;
import org.codehaus.groovy.ast.MultipleAssignmentMetadata;
import org.codehaus.groovy.ast.Parameter;
import org.codehaus.groovy.ast.PropertyNode;
import org.codehaus.groovy.ast.RecordComponentNode;
import org.codehaus.groovy.ast.Variable;
import org.codehaus.groovy.ast.expr.AnnotationConstantExpression;
import org.codehaus.groovy.ast.expr.ArgumentListExpression;
import org.codehaus.groovy.ast.expr.ArrayExpression;
import org.codehaus.groovy.ast.expr.AttributeExpression;
import org.codehaus.groovy.ast.expr.BinaryExpression;
import org.codehaus.groovy.ast.expr.BitwiseNegationExpression;
import org.codehaus.groovy.ast.expr.BooleanExpression;
import org.codehaus.groovy.ast.expr.CastExpression;
import org.codehaus.groovy.ast.expr.ClassExpression;
import org.codehaus.groovy.ast.expr.ClosureExpression;
import org.codehaus.groovy.ast.expr.ClosureListExpression;
import org.codehaus.groovy.ast.expr.ConstantExpression;
import org.codehaus.groovy.ast.expr.ConstructorCallExpression;
import org.codehaus.groovy.ast.expr.DeclarationExpression;
import org.codehaus.groovy.ast.expr.ElvisOperatorExpression;
import org.codehaus.groovy.ast.expr.EmptyExpression;
import org.codehaus.groovy.ast.expr.Expression;
import org.codehaus.groovy.ast.expr.ExpressionTransformer;
import org.codehaus.groovy.ast.expr.FieldExpression;
import org.codehaus.groovy.ast.IntersectionTypeClassNode;
import org.codehaus.groovy.ast.expr.LambdaExpression;
import org.codehaus.groovy.ast.expr.ListExpression;
import org.codehaus.groovy.ast.expr.MapEntryExpression;
import org.codehaus.groovy.ast.expr.MapExpression;
import org.codehaus.groovy.ast.expr.MethodCall;
import org.codehaus.groovy.ast.expr.MethodCallExpression;
import org.codehaus.groovy.ast.expr.MethodPointerExpression;
import org.codehaus.groovy.ast.expr.MethodReferenceExpression;
import org.codehaus.groovy.ast.expr.NotExpression;
import org.codehaus.groovy.ast.expr.PostfixExpression;
import org.codehaus.groovy.ast.expr.PrefixExpression;
import org.codehaus.groovy.ast.expr.PropertyExpression;
import org.codehaus.groovy.ast.expr.RangeExpression;
import org.codehaus.groovy.ast.expr.SpreadExpression;
import org.codehaus.groovy.ast.expr.SpreadMapExpression;
import org.codehaus.groovy.ast.expr.StaticMethodCallExpression;
import org.codehaus.groovy.ast.expr.SwitchExpression;
import org.codehaus.groovy.ast.expr.TernaryExpression;
import org.codehaus.groovy.ast.expr.TupleExpression;
import org.codehaus.groovy.ast.expr.UnaryMinusExpression;
import org.codehaus.groovy.ast.expr.UnaryPlusExpression;
import org.codehaus.groovy.ast.expr.VariableExpression;
import org.codehaus.groovy.ast.stmt.BlockStatement;
import org.codehaus.groovy.ast.stmt.BreakStatement;
import org.codehaus.groovy.ast.stmt.CaseStatement;
import org.codehaus.groovy.ast.stmt.CatchStatement;
import org.codehaus.groovy.ast.stmt.ContinueStatement;
import org.codehaus.groovy.ast.stmt.EmptyStatement;
import org.codehaus.groovy.ast.stmt.ExpressionStatement;
import org.codehaus.groovy.ast.stmt.ForStatement;
import org.codehaus.groovy.ast.stmt.IfStatement;
import org.codehaus.groovy.ast.stmt.ReturnStatement;
import org.codehaus.groovy.ast.stmt.Statement;
import org.codehaus.groovy.ast.stmt.SwitchStatement;
import org.codehaus.groovy.ast.stmt.TryCatchStatement;
import org.codehaus.groovy.ast.stmt.WhileStatement;
import org.codehaus.groovy.ast.stmt.YieldStatement;
import org.codehaus.groovy.ast.tools.GeneralUtils;
import org.codehaus.groovy.ast.tools.GenericsUtils;
import org.codehaus.groovy.ast.tools.WideningCategories;
import org.codehaus.groovy.classgen.ReturnAdder;
import org.codehaus.groovy.classgen.asm.InvocationWriter;
import org.codehaus.groovy.control.CompilationUnit;
import org.codehaus.groovy.control.ErrorCollector;
import org.codehaus.groovy.control.ResolveVisitor;
import org.codehaus.groovy.control.SourceUnit;
import org.codehaus.groovy.control.messages.Message;
import org.codehaus.groovy.control.messages.WarningMessage;
import org.codehaus.groovy.runtime.DefaultGroovyMethods;
import org.codehaus.groovy.syntax.SyntaxException;
import org.codehaus.groovy.syntax.Token;
import org.codehaus.groovy.syntax.TokenUtil;
import org.codehaus.groovy.transform.RecordTypeASTTransformation;
import org.codehaus.groovy.transform.StaticTypesTransformation;
import org.codehaus.groovy.transform.trait.Traits;
import org.objectweb.asm.Opcodes;

import java.lang.reflect.Modifier;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.EnumMap;
import java.util.Enumeration;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.LinkedHashMap;
import java.util.LinkedHashSet;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Optional;
import java.util.Set;
import java.util.StringJoiner;
import java.util.TreeSet;
import java.util.concurrent.atomic.AtomicLong;
import java.util.function.BiPredicate;
import java.util.function.Consumer;
import java.util.function.Function;
import java.util.stream.Collectors;
import java.util.stream.IntStream;

import static org.apache.groovy.ast.tools.ClassNodeUtils.getNestHost;
import static org.apache.groovy.ast.tools.MethodNodeUtils.withDefaultArgumentMethods;
import static org.apache.groovy.ast.tools.SwitchExpressionUtils.enumConstantName;
import static org.apache.groovy.ast.tools.SwitchExpressionUtils.intConstant;
import static org.apache.groovy.ast.tools.SwitchExpressionUtils.isIntegralType;
import static org.apache.groovy.ast.tools.SwitchExpressionUtils.isOptimizedIntSwitch;
import static org.apache.groovy.ast.tools.SwitchExpressionUtils.stringConstant;
import static org.apache.groovy.ast.tools.SwitchExpressionUtils.unwrapEnumType;
import static org.apache.groovy.util.BeanUtils.capitalize;
import static org.apache.groovy.util.BeanUtils.decapitalize;
import static org.codehaus.groovy.ast.ClassHelper.AUTOCLOSEABLE_TYPE;
import static org.codehaus.groovy.ast.ClassHelper.BigDecimal_TYPE;
import static org.codehaus.groovy.ast.ClassHelper.BigInteger_TYPE;
import static org.codehaus.groovy.ast.ClassHelper.Byte_TYPE;
import static org.codehaus.groovy.ast.ClassHelper.CLASS_Type;
import static org.codehaus.groovy.ast.ClassHelper.CLOSURE_TYPE;
import static org.codehaus.groovy.ast.ClassHelper.Character_TYPE;
import static org.codehaus.groovy.ast.ClassHelper.Double_TYPE;
import static org.codehaus.groovy.ast.ClassHelper.Enum_Type;
import static org.codehaus.groovy.ast.ClassHelper.Float_TYPE;
import static org.codehaus.groovy.ast.ClassHelper.Integer_TYPE;
import static org.codehaus.groovy.ast.ClassHelper.Iterator_TYPE;
import static org.codehaus.groovy.ast.ClassHelper.LIST_TYPE;
import static org.codehaus.groovy.ast.ClassHelper.Long_TYPE;
import static org.codehaus.groovy.ast.ClassHelper.MAP_TYPE;
import static org.codehaus.groovy.ast.ClassHelper.METACLASS_TYPE;
import static org.codehaus.groovy.ast.ClassHelper.Number_TYPE;
import static org.codehaus.groovy.ast.ClassHelper.OBJECT_TYPE;
import static org.codehaus.groovy.ast.ClassHelper.PATTERN_TYPE;
import static org.codehaus.groovy.ast.ClassHelper.RANGE_TYPE;
import static org.codehaus.groovy.ast.ClassHelper.SET_TYPE;
import static org.codehaus.groovy.ast.ClassHelper.STREAM_TYPE;
import static org.codehaus.groovy.ast.ClassHelper.STRING_TYPE;
import static org.codehaus.groovy.ast.ClassHelper.Short_TYPE;
import static org.codehaus.groovy.ast.ClassHelper.VOID_TYPE;
import static org.codehaus.groovy.ast.ClassHelper.boolean_TYPE;
import static org.codehaus.groovy.ast.ClassHelper.byte_TYPE;
import static org.codehaus.groovy.ast.ClassHelper.char_TYPE;
import static org.codehaus.groovy.ast.ClassHelper.double_TYPE;
import static org.codehaus.groovy.ast.ClassHelper.dynamicType;
import static org.codehaus.groovy.ast.ClassHelper.findSAM;
import static org.codehaus.groovy.ast.ClassHelper.float_TYPE;
import static org.codehaus.groovy.ast.ClassHelper.getNextSuperClass;
import static org.codehaus.groovy.ast.ClassHelper.getUnwrapper;
import static org.codehaus.groovy.ast.ClassHelper.getWrapper;
import static org.codehaus.groovy.ast.ClassHelper.int_TYPE;
import static org.codehaus.groovy.ast.ClassHelper.isBigDecimalType;
import static org.codehaus.groovy.ast.ClassHelper.isBigIntegerType;
import static org.codehaus.groovy.ast.ClassHelper.isClassType;
import static org.codehaus.groovy.ast.ClassHelper.isDynamicTyped;
import static org.codehaus.groovy.ast.ClassHelper.isFunctionalInterface;
import static org.codehaus.groovy.ast.ClassHelper.isGStringType;
import static org.codehaus.groovy.ast.ClassHelper.isNumberType;
import static org.codehaus.groovy.ast.ClassHelper.isObjectType;
import static org.codehaus.groovy.ast.ClassHelper.isPrimitiveBoolean;
import static org.codehaus.groovy.ast.ClassHelper.isPrimitiveByte;
import static org.codehaus.groovy.ast.ClassHelper.isPrimitiveChar;
import static org.codehaus.groovy.ast.ClassHelper.isPrimitiveDouble;
import static org.codehaus.groovy.ast.ClassHelper.isPrimitiveFloat;
import static org.codehaus.groovy.ast.ClassHelper.isPrimitiveInt;
import static org.codehaus.groovy.ast.ClassHelper.isPrimitiveLong;
import static org.codehaus.groovy.ast.ClassHelper.isPrimitiveShort;
import static org.codehaus.groovy.ast.ClassHelper.isPrimitiveType;
import static org.codehaus.groovy.ast.ClassHelper.isPrimitiveVoid;
import static org.codehaus.groovy.ast.ClassHelper.isSAMType;
import static org.codehaus.groovy.ast.ClassHelper.isStringType;
import static org.codehaus.groovy.ast.ClassHelper.isWrapperByte;
import static org.codehaus.groovy.ast.ClassHelper.isWrapperCharacter;
import static org.codehaus.groovy.ast.ClassHelper.isWrapperDouble;
import static org.codehaus.groovy.ast.ClassHelper.isWrapperFloat;
import static org.codehaus.groovy.ast.ClassHelper.isWrapperInteger;
import static org.codehaus.groovy.ast.ClassHelper.isWrapperLong;
import static org.codehaus.groovy.ast.ClassHelper.isWrapperShort;
import static org.codehaus.groovy.ast.ClassHelper.long_TYPE;
import static org.codehaus.groovy.ast.ClassHelper.short_TYPE;
import static org.codehaus.groovy.ast.tools.ClosureUtils.getParametersSafe;
import static org.codehaus.groovy.ast.tools.ClosureUtils.getResolveStrategyName;
import static org.codehaus.groovy.ast.tools.ClosureUtils.hasImplicitParameter;
import static org.codehaus.groovy.ast.tools.GeneralUtils.args;
import static org.codehaus.groovy.ast.tools.GeneralUtils.binX;
import static org.codehaus.groovy.ast.tools.GeneralUtils.callX;
import static org.codehaus.groovy.ast.tools.GeneralUtils.castX;
import static org.codehaus.groovy.ast.tools.GeneralUtils.classX;
import static org.codehaus.groovy.ast.tools.GeneralUtils.constX;
import static org.codehaus.groovy.ast.tools.GeneralUtils.ctorSuperX;
import static org.codehaus.groovy.ast.tools.GeneralUtils.ctorX;
import static org.codehaus.groovy.ast.tools.GeneralUtils.defaultValueX;
import static org.codehaus.groovy.ast.tools.GeneralUtils.elvisX;
import static org.codehaus.groovy.ast.tools.GeneralUtils.getGetterName;
import static org.codehaus.groovy.ast.tools.GeneralUtils.getSetterName;
import static org.codehaus.groovy.ast.tools.GeneralUtils.inSamePackage;
import static org.codehaus.groovy.ast.tools.GeneralUtils.indexX;
import static org.codehaus.groovy.ast.tools.GeneralUtils.isOrImplements;
import static org.codehaus.groovy.ast.tools.GeneralUtils.nullX;
import static org.codehaus.groovy.ast.tools.GeneralUtils.propX;
import static org.codehaus.groovy.ast.tools.GeneralUtils.thisPropX;
import static org.codehaus.groovy.ast.tools.GeneralUtils.varX;
import static org.codehaus.groovy.ast.tools.ParameterUtils.isVargs;
import static org.codehaus.groovy.ast.tools.WideningCategories.isBigDecCategory;
import static org.codehaus.groovy.ast.tools.WideningCategories.isBigIntCategory;
import static org.codehaus.groovy.ast.tools.WideningCategories.isDouble;
import static org.codehaus.groovy.ast.tools.WideningCategories.isDoubleCategory;
import static org.codehaus.groovy.ast.tools.WideningCategories.isFloat;
import static org.codehaus.groovy.ast.tools.WideningCategories.isFloatingCategory;
import static org.codehaus.groovy.ast.tools.WideningCategories.isIntCategory;
import static org.codehaus.groovy.ast.tools.WideningCategories.isLongCategory;
import static org.codehaus.groovy.ast.tools.WideningCategories.isNumberCategory;
import static org.codehaus.groovy.ast.tools.WideningCategories.lowestUpperBound;
import static org.codehaus.groovy.runtime.ArrayGroovyMethods.asBoolean;
import static org.codehaus.groovy.runtime.ArrayGroovyMethods.init;
import static org.codehaus.groovy.runtime.ArrayGroovyMethods.last;
import static org.codehaus.groovy.syntax.Types.ASSIGN;
import static org.codehaus.groovy.syntax.Types.BITWISE_AND;
import static org.codehaus.groovy.syntax.Types.BITWISE_OR;
import static org.codehaus.groovy.syntax.Types.BITWISE_XOR;
import static org.codehaus.groovy.syntax.Types.COMPARE_EQUAL;
import static org.codehaus.groovy.syntax.Types.COMPARE_NOT_EQUAL;
import static org.codehaus.groovy.syntax.Types.COMPARE_NOT_IN;
import static org.codehaus.groovy.syntax.Types.COMPARE_NOT_INSTANCEOF;
import static org.codehaus.groovy.syntax.Types.COMPARE_TO;
import static org.codehaus.groovy.syntax.Types.DIVIDE;
import static org.codehaus.groovy.syntax.Types.DIVIDE_EQUAL;
import static org.codehaus.groovy.syntax.Types.ELVIS_EQUAL;
import static org.codehaus.groovy.syntax.Types.EQUAL;
import static org.codehaus.groovy.syntax.Types.FIND_REGEX;
import static org.codehaus.groovy.syntax.Types.INTDIV;
import static org.codehaus.groovy.syntax.Types.INTDIV_EQUAL;
import static org.codehaus.groovy.syntax.Types.KEYWORD_IN;
import static org.codehaus.groovy.syntax.Types.KEYWORD_INSTANCEOF;
import static org.codehaus.groovy.syntax.Types.LOGICAL_AND;
import static org.codehaus.groovy.syntax.Types.LOGICAL_OR;
import static org.codehaus.groovy.syntax.Types.MINUS_MINUS;
import static org.codehaus.groovy.syntax.Types.MOD;
import static org.codehaus.groovy.syntax.Types.MOD_EQUAL;
import static org.codehaus.groovy.syntax.Types.PLUS_PLUS;
import static org.codehaus.groovy.syntax.Types.REMAINDER;
import static org.codehaus.groovy.syntax.Types.REMAINDER_EQUAL;
import static org.codehaus.groovy.transform.stc.StaticTypeCheckingSupport.ArrayList_TYPE;
import static org.codehaus.groovy.transform.stc.StaticTypeCheckingSupport.Collection_TYPE;
import static org.codehaus.groovy.transform.stc.StaticTypeCheckingSupport.LinkedHashMap_TYPE;
import static org.codehaus.groovy.transform.stc.StaticTypeCheckingSupport.LinkedHashSet_TYPE;
import static org.codehaus.groovy.transform.stc.StaticTypeCheckingSupport.Matcher_TYPE;
import static org.codehaus.groovy.transform.stc.StaticTypeCheckingSupport.NUMBER_OPS;
import static org.codehaus.groovy.transform.stc.StaticTypeCheckingSupport.UNKNOWN_PARAMETER_TYPE;
import static org.codehaus.groovy.transform.stc.StaticTypeCheckingSupport.applyGenericsConnections;
import static org.codehaus.groovy.transform.stc.StaticTypeCheckingSupport.applyGenericsContext;
import static org.codehaus.groovy.transform.stc.StaticTypeCheckingSupport.boundUnboundedWildcards;
import static org.codehaus.groovy.transform.stc.StaticTypeCheckingSupport.checkCompatibleAssignmentTypes;
import static org.codehaus.groovy.transform.stc.StaticTypeCheckingSupport.checkPossibleLossOfPrecision;
import static org.codehaus.groovy.transform.stc.StaticTypeCheckingSupport.chooseBestMethod;
import static org.codehaus.groovy.transform.stc.StaticTypeCheckingSupport.evaluateExpression;
import static org.codehaus.groovy.transform.stc.StaticTypeCheckingSupport.extractGenericsConnections;
import static org.codehaus.groovy.transform.stc.StaticTypeCheckingSupport.extractGenericsParameterMapOfThis;
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.findTargetVariable;
import static org.codehaus.groovy.transform.stc.StaticTypeCheckingSupport.fullyResolve;
import static org.codehaus.groovy.transform.stc.StaticTypeCheckingSupport.fullyResolveType;
import static org.codehaus.groovy.transform.stc.StaticTypeCheckingSupport.getAssignOperationName;
import static org.codehaus.groovy.transform.stc.StaticTypeCheckingSupport.getCombinedBoundType;
import static org.codehaus.groovy.transform.stc.StaticTypeCheckingSupport.getCombinedGenericsType;
import static org.codehaus.groovy.transform.stc.StaticTypeCheckingSupport.getGenericsWithoutArray;
import static org.codehaus.groovy.transform.stc.StaticTypeCheckingSupport.getOperationName;
import static org.codehaus.groovy.transform.stc.StaticTypeCheckingSupport.implementsInterfaceOrIsSubclassOf;
import static org.codehaus.groovy.transform.stc.StaticTypeCheckingSupport.isArrayOp;
import static org.codehaus.groovy.transform.stc.StaticTypeCheckingSupport.isAssignableTo;
import static org.codehaus.groovy.transform.stc.StaticTypeCheckingSupport.isAssignment;
import static org.codehaus.groovy.transform.stc.StaticTypeCheckingSupport.isBeingCompiled;
import static org.codehaus.groovy.transform.stc.StaticTypeCheckingSupport.isBitOperator;
import static org.codehaus.groovy.transform.stc.StaticTypeCheckingSupport.isBoolIntrinsicOp;
import static org.codehaus.groovy.transform.stc.StaticTypeCheckingSupport.isClassClassNodeWrappingConcreteType;
import static org.codehaus.groovy.transform.stc.StaticTypeCheckingSupport.isCompareToBoolean;
import static org.codehaus.groovy.transform.stc.StaticTypeCheckingSupport.isGStringOrGStringStringLUB;
import static org.codehaus.groovy.transform.stc.StaticTypeCheckingSupport.isOperationInGroup;
import static org.codehaus.groovy.transform.stc.StaticTypeCheckingSupport.isParameterizedWithGStringOrGStringString;
import static org.codehaus.groovy.transform.stc.StaticTypeCheckingSupport.isParameterizedWithString;
import static org.codehaus.groovy.transform.stc.StaticTypeCheckingSupport.isPowerOperator;
import static org.codehaus.groovy.transform.stc.StaticTypeCheckingSupport.isShiftOperation;
import static org.codehaus.groovy.transform.stc.StaticTypeCheckingSupport.isTraitSelf;
import static org.codehaus.groovy.transform.stc.StaticTypeCheckingSupport.isUsingGenericsOrIsArrayUsingGenerics;
import static org.codehaus.groovy.transform.stc.StaticTypeCheckingSupport.isWildcardLeftHandSide;
import static org.codehaus.groovy.transform.stc.StaticTypeCheckingSupport.missesGenericsTypes;
import static org.codehaus.groovy.transform.stc.StaticTypeCheckingSupport.prettyPrintType;
import static org.codehaus.groovy.transform.stc.StaticTypeCheckingSupport.prettyPrintTypeName;
import static org.codehaus.groovy.transform.stc.StaticTypeCheckingSupport.resolveClassNodeGenerics;
import static org.codehaus.groovy.transform.stc.StaticTypeCheckingSupport.toMethodParametersString;
import static org.codehaus.groovy.transform.stc.StaticTypeCheckingSupport.typeCheckMethodArgumentWithGenerics;
import static org.codehaus.groovy.transform.stc.StaticTypeCheckingSupport.typeCheckMethodsWithGenerics;
import static org.codehaus.groovy.transform.stc.StaticTypesMarker.CLOSURE_ARGUMENTS;
import static org.codehaus.groovy.transform.stc.StaticTypesMarker.COMPOUND_ASSIGN_TARGET;
import static org.codehaus.groovy.transform.stc.StaticTypesMarker.DECLARATION_INFERRED_TYPE;
import static org.codehaus.groovy.transform.stc.StaticTypesMarker.DELEGATION_METADATA;
import static org.codehaus.groovy.transform.stc.StaticTypesMarker.DIRECT_METHOD_CALL_TARGET;
import static org.codehaus.groovy.transform.stc.StaticTypesMarker.DYNAMIC_RESOLUTION;
import static org.codehaus.groovy.transform.stc.StaticTypesMarker.IMPLICIT_RECEIVER;
import static org.codehaus.groovy.transform.stc.StaticTypesMarker.INFERRED_RETURN_TYPE;
import static org.codehaus.groovy.transform.stc.StaticTypesMarker.INFERRED_TYPE;
import static org.codehaus.groovy.transform.stc.StaticTypesMarker.LAMBDA_MARKERS;
import static org.codehaus.groovy.transform.stc.StaticTypesMarker.PARAMETER_TYPE;
import static org.codehaus.groovy.transform.stc.StaticTypesMarker.PRIMARY_FUNCTIONAL_TYPE;
import static org.codehaus.groovy.transform.stc.StaticTypesMarker.PV_FIELDS_ACCESS;
import static org.codehaus.groovy.transform.stc.StaticTypesMarker.PV_FIELDS_MUTATION;
import static org.codehaus.groovy.transform.stc.StaticTypesMarker.PV_METHODS_ACCESS;
import static org.codehaus.groovy.transform.stc.StaticTypesMarker.READONLY_PROPERTY;
import static org.codehaus.groovy.transform.stc.StaticTypesMarker.SUPER_MOP_METHOD_REQUIRED;
import static org.codehaus.groovy.transform.stc.StaticTypesMarker.TYPE;

/**
 * The main class code visitor responsible for static type checking. It will perform various inspections like checking
 * assignment types, type inference, ... Eventually, class nodes may be annotated with inferred type information.
 */
public class StaticTypeCheckingVisitor extends ClassCodeVisitorSupport {

    private static final boolean DEBUG_GENERATED_CODE = SystemUtil.getBooleanSafe("groovy.stc.debug");
    private static final AtomicLong UNIQUE_LONG = new AtomicLong();

    /** Metadata key used to store per-method error collectors. */
    protected static final Object ERROR_COLLECTOR = ErrorCollector.class;
    /** Shared empty method list. */
    protected static final List<MethodNode> EMPTY_METHODNODE_LIST = Collections.emptyList();
    /** Cached {@link TypeChecked} annotation type. */
    protected static final ClassNode TYPECHECKED_CLASSNODE = ClassHelper.make(TypeChecked.class);
    /** Type-checking annotations recognized by this visitor. */
    protected static final ClassNode[] TYPECHECKING_ANNOTATIONS = new ClassNode[]{TYPECHECKED_CLASSNODE};
    /** Cached {@link TypeChecked.TypeCheckingInfo} annotation type. */
    protected static final ClassNode TYPECHECKING_INFO_NODE = ClassHelper.make(TypeChecked.TypeCheckingInfo.class);
    /** Cached {@link DefaultGroovyMethods} type. */
    protected static final ClassNode DGM_CLASSNODE = ClassHelper.make(DefaultGroovyMethods.class);
    /** Current encoding protocol for stored type-checking signatures. */
    protected static final int CURRENT_SIGNATURE_PROTOCOL_VERSION = 1;
    /** Constant expression for the current signature protocol version. */
    protected static final Expression CURRENT_SIGNATURE_PROTOCOL = new ConstantExpression(CURRENT_SIGNATURE_PROTOCOL_VERSION, true);
    /** Cached {@code Closure#getDelegate()} method. */
    protected static final MethodNode GET_DELEGATE = CLOSURE_TYPE.getGetterMethod("getDelegate");
    /** Cached {@code Closure#getOwner()} method. */
    protected static final MethodNode GET_OWNER = CLOSURE_TYPE.getGetterMethod("getOwner");
    /** Cached {@code Closure#getThisObject()} method. */
    protected static final MethodNode GET_THISOBJECT = CLOSURE_TYPE.getGetterMethod("getThisObject");
    /** Cached {@link DelegatesTo} annotation type. */
    protected static final ClassNode DELEGATES_TO = ClassHelper.make(DelegatesTo.class);
    /** Cached {@link DelegatesTo.Target} annotation type. */
    protected static final ClassNode DELEGATES_TO_TARGET = ClassHelper.make(DelegatesTo.Target.class);
    /** Cached {@link ClosureParams} annotation type. */
    protected static final ClassNode CLOSUREPARAMS_CLASSNODE = ClassHelper.make(ClosureParams.class);
    /** Cached {@link NamedParams} annotation type. */
    protected static final ClassNode NAMED_PARAMS_CLASSNODE = ClassHelper.make(NamedParams.class);
    /** Cached {@link NamedParam} annotation type. */
    protected static final ClassNode NAMED_PARAM_CLASSNODE = ClassHelper.make(NamedParam.class);
    @Deprecated(forRemoval = true, since = "4.0.0")
    /** Deprecated alias for {@link #LinkedHashMap_TYPE}. */
    protected static final ClassNode LINKEDHASHMAP_CLASSNODE = LinkedHashMap_TYPE;
    /** Cached {@link Enumeration} type. */
    protected static final ClassNode ENUMERATION_TYPE = ClassHelper.make(Enumeration.class);
    /** Cached {@link CharSequence} type (GROOVY-9848: membership operator dispatch). */
    protected static final ClassNode CHAR_SEQUENCE_TYPE = ClassHelper.make(CharSequence.class);
    /** Cached {@link java.util.Map.Entry} type. */
    protected static final ClassNode MAP_ENTRY_TYPE = ClassHelper.make(Map.Entry.class);
    /** Cached {@link Iterable} type. */
    protected static final ClassNode ITERABLE_TYPE = ClassHelper.ITERABLE_TYPE;

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

    /** Cached zero-argument {@code Closure#call()} method. */
    public static final MethodNode CLOSURE_CALL_NO_ARG  = CLOSURE_TYPE.getDeclaredMethod("call", Parameter.EMPTY_ARRAY);
    /** Cached single-argument {@code Closure#call(Object)} method. */
    public static final MethodNode CLOSURE_CALL_ONE_ARG = CLOSURE_TYPE.getDeclaredMethod("call", new Parameter[]{new Parameter(OBJECT_TYPE, "arg")});
    /** Cached varargs {@code Closure#call(Object[])} method. */
    public static final MethodNode CLOSURE_CALL_VARGS   = CLOSURE_TYPE.getDeclaredMethod("call", new Parameter[]{new Parameter(OBJECT_TYPE.makeArray(), "args")});

    /** Empty statement reused for synthetic AST nodes. */
    public static final Statement GENERATED_EMPTY_STATEMENT = EmptyStatement.INSTANCE;

    protected final ReturnAdder.ReturnStatementListener returnListener = (returnStatement) -> {
        ClassNode returnType = checkReturnType(returnStatement);
        if (this.typeCheckingContext.getEnclosingClosure() != null) {
            addClosureReturnType(returnType);
        } else if (this.typeCheckingContext.getEnclosingMethod() == null) {
            throw new GroovyBugError("Unexpected return statement at " + returnStatement.getLineNumber() + ":" + returnStatement.getColumnNumber() + " " + returnStatement.getText());
        }
    };

    /** Return adder used to normalize implicit and explicit returns. */
    protected final ReturnAdder returnAdder = new ReturnAdder(returnListener);

    /** Field currently being visited. */
    protected FieldNode currentField;
    /** Property currently being visited. */
    protected PropertyNode currentProperty;
    /** Composite extension chain consulted during type checking. */
    protected DefaultTypeCheckingExtension extension;
    /** Mutable state for the current type-checking run. */
    protected TypeCheckingContext typeCheckingContext;

    /**
     * Creates a static type-checking visitor for the supplied source and class.
     */
    public StaticTypeCheckingVisitor(final SourceUnit source, final ClassNode classNode) {
        this.typeCheckingContext = new TypeCheckingContext(this);
        this.typeCheckingContext.pushEnclosingClassNode(classNode);
        this.typeCheckingContext.pushTemporaryTypeInfo();
        this.typeCheckingContext.pushErrorCollector(
                        source.getErrorCollector());
        this.typeCheckingContext.source = source;

        this.extension = new DefaultTypeCheckingExtension(this);
        this.extension.addHandler(new EnumTypeCheckingExtension(this));
        this.extension.addHandler(new TraitTypeCheckingExtension(this));
    }

    /**
     * Associates the visitor with a compilation unit.
     */
    public void setCompilationUnit(final CompilationUnit compilationUnit) {
        typeCheckingContext.setCompilationUnit(compilationUnit);
    }

    /** {@inheritDoc} */
    @Override
    protected SourceUnit getSourceUnit() {
        return typeCheckingContext.getSource();
    }

    /**
     * Initializes the registered type-checking extensions.
     */
    public void initialize() {
        extension.setup();
    }

    /**
     * Returns array of type checking annotations. Subclasses may override this
     * method in order to provide additional types which must be looked up when
     * checking if a method or a class node should be skipped.
     * <p>
     * The default implementation returns {@link TypeChecked}.
     */
    protected ClassNode[] getTypeCheckingAnnotations() {
        return TYPECHECKING_ANNOTATIONS;
    }

    /**
     * Returns the current type checking context. The context is used internally by the type
     * checker during type checking to store various state data.
     *
     * @return the type checking context
     */
    public TypeCheckingContext getTypeCheckingContext() {
        return typeCheckingContext;
    }

    /**
     * Adds a type-checking extension to the active extension chain.
     */
    public void addTypeCheckingExtension(final TypeCheckingExtension extension) {
        this.extension.addHandler(extension);
    }

    private List<TypeCheckingExtension> getTypeCheckingExtensions(final AnnotatedNode classOrMethod) {
        List<Expression> ex = new ArrayList<>();
        for (ClassNode type : getTypeCheckingAnnotations()) {
            for (AnnotationNode anno : classOrMethod.getAnnotations(type)) {
                Expression extensions = anno.getMember("extensions");
                if (extensions instanceof ConstantExpression) {
                    ex.add(extensions);
                } else if (extensions instanceof ListExpression) {
                    ex.addAll(((ListExpression) extensions).getExpressions());
                }
            }
        }
        if (ex.isEmpty()) return Collections.emptyList();
        return ex.stream().filter(e -> e instanceof ConstantExpression).map(pathOrType ->
            new GroovyTypeCheckingExtensionSupport(this, pathOrType.getText(), typeCheckingContext.getCompilationUnit())
        ).collect(Collectors.toList());
    }

    private <Node extends AnnotatedNode> void doWithTypeCheckingExtensions(final Node classOrMethod, final Consumer<Node> visitor) {
        List<TypeCheckingExtension> extensions = getTypeCheckingExtensions(classOrMethod);
        if (extensions.isEmpty()) {
            visitor.accept(classOrMethod);
        } else { // GROOVY-10770: extension composition
            List<TypeCheckingExtension> added = new ArrayList<>();
            for (TypeCheckingExtension ext : extensions) {
                if (extension.addHandler(ext)) {
                    added.add(ext);
                    ext.setup();
                }
            }
            try {
                visitor.accept(classOrMethod);
            } finally {
                for (TypeCheckingExtension ext : added) {
                    extension.removeHandler(ext);
                    ext.finish();
                }
            }
        }
    }

    //--------------------------------------------------------------------------

    /** {@inheritDoc} */
    @Override
    public void visitClass(final ClassNode node) {
        if (shouldSkipClassNode(node)) return;
        if (!extension.beforeVisitClass(node)) {
            Object type = node.getNodeMetaData(INFERRED_TYPE);
            if (type != null) {
                // transformation has already been run on this class node
                // so use a silent collector in order not to duplicate errors
                typeCheckingContext.pushErrorCollector();
            }
            typeCheckingContext.pushEnclosingClassNode(node);
            Set<MethodNode> oldSet = typeCheckingContext.alreadyVisitedMethods;
            typeCheckingContext.alreadyVisitedMethods = new LinkedHashSet<>();
            boolean oldStatic = typeCheckingContext.isInStaticContext;
            if (node instanceof InnerClassNode && ((InnerClassNode) node).isAnonymous()) {
                typeCheckingContext.isInStaticContext = false; // AIC body is an instance context
            }
            try {
                doWithTypeCheckingExtensions(node, super::visitClass);
                node.getInnerClasses().forEachRemaining(this::visitClass);
            } finally {
                typeCheckingContext.isInStaticContext = oldStatic;
                typeCheckingContext.alreadyVisitedMethods = oldSet;
                typeCheckingContext.popEnclosingClassNode();
                if (type != null) {
                    typeCheckingContext.popErrorCollector();
                }
            }

            node.putNodeMetaData(INFERRED_TYPE, node);
            node.putNodeMetaData(StaticTypeCheckingVisitor.class, Boolean.TRUE);
            // mark all methods as visited. We can't do this in visitMethod because the type checker
            // works in a two pass sequence and we don't want to skip the second pass
            node.getMethods().forEach(n -> n.putNodeMetaData(StaticTypeCheckingVisitor.class, Boolean.TRUE));
            node.getDeclaredConstructors().forEach(n -> n.putNodeMetaData(StaticTypeCheckingVisitor.class, Boolean.TRUE));
        }
        extension.afterVisitClass(node);
    }

    /**
     * Indicates whether the class should be skipped by this visitor.
     */
    protected boolean shouldSkipClassNode(final ClassNode node) {
        return Boolean.TRUE.equals(node.getNodeMetaData(StaticTypeCheckingVisitor.class)) || isSkipMode(node);
    }

    /**
     * Indicates whether the method should be skipped by this visitor.
     */
    protected boolean shouldSkipMethodNode(final MethodNode node) {
        return Boolean.TRUE.equals(node.getNodeMetaData(StaticTypeCheckingVisitor.class)) || isSkipMode(node);
    }

    /**
     * Indicates whether the annotated node is configured for {@link TypeCheckingMode#SKIP}.
     * <p>
     * The most specific annotation wins: a node whose own type-checking annotation has a
     * non-SKIP mode is never skipped, whatever an enclosing class or method declares.
     * Without an annotation of its own, a node inherits skip mode from its enclosing scope.
     */
    public boolean isSkipMode(final AnnotatedNode node) {
        if (node == null) return false;
        boolean explicitNonSkip = false;
        for (ClassNode tca : getTypeCheckingAnnotations()) {
            List<AnnotationNode> annotations = node.getAnnotations(tca);
            if (annotations != null) {
                for (AnnotationNode annotation : annotations) {
                    Expression value = annotation.getMember("value");
                    if (value != null) {
                        if (value instanceof ConstantExpression ce) {
                            if (TypeCheckingMode.SKIP.toString().equals(ce.getValue().toString())) return true;
                        } else if (value instanceof PropertyExpression pe) {
                            if (TypeCheckingMode.SKIP.toString().equals(pe.getPropertyAsString())) return true;
                        }
                    }
                    explicitNonSkip = true;
                }
            }
        }
        if (explicitNonSkip) return false; // GROOVY-12292
        if (node instanceof MethodNode) {
            return isSkipMode(node.getDeclaringClass());
        }
        return isSkippedInnerClass(node);
    }

    /**
     * Tests if a node is an inner class node, and if it is, then checks if the enclosing method is skipped.
     *
     * @return true if the inner class node should be skipped
     */
    protected boolean isSkippedInnerClass(final AnnotatedNode node) {
        if (node instanceof ClassNode type) {
            if (type.getOuterClass() != null) {
                MethodNode enclosingMethod = type.getEnclosingMethod();
                if (enclosingMethod != null && isSkipMode(enclosingMethod)) {
                    return true;
                }
            }
        }
        return false;
    }

    /** {@inheritDoc} */
    @Override
    public void visitClassExpression(final ClassExpression expression) {
        super.visitClassExpression(expression);
        ClassNode cn = expression.getNodeMetaData(INFERRED_TYPE);
        if (cn == null) {
            storeType(expression, getType(expression));
        }
    }

    /**
     * Determines if the two classes belong to the same nest, so a private-access
     * bridge can be generated between them. Identity comparison is deliberate:
     * nest-mates only arise within a single source unit, where the outer-class
     * chain shares {@code ClassNode} instances; name-based {@code equals} could
     * conflate same-named classes from different origins. Accessibility decisions
     * ({@link #isFieldAccessible}) must use the same predicate as bridge marking
     * ({@code checkOrMarkPrivateAccess}), or an access could be admitted for
     * which no bridge is generated.
     */
    private static boolean inSameNest(final ClassNode a, final ClassNode b) {
        return getNestHost(a) == getNestHost(b);
    }

    /**
     * Checks for private field access from closure or nestmate.
     */
    private void checkOrMarkPrivateAccess(final Expression source, final FieldNode fn, final boolean lhsOfAssignment) {
        if (fn != null && fn.isPrivate() && !fn.isSynthetic()) {
            ClassNode declaringClass = fn.getDeclaringClass();
            ClassNode enclosingClass = typeCheckingContext.getEnclosingClassNode();
            if (declaringClass == enclosingClass ? typeCheckingContext.getEnclosingClosure() != null : inSameNest(declaringClass, enclosingClass)) {
                source.putNodeMetaData(lhsOfAssignment ? PV_FIELDS_MUTATION : PV_FIELDS_ACCESS, fn);
            }
        }
    }

    /**
     * Checks for private or protected method access from closure or nestmate.
     */
    private void checkOrMarkPrivateAccess(final Expression source, final MethodNode mn) {
        ClassNode declaringClass = mn.getDeclaringClass();
        ClassNode enclosingClass = typeCheckingContext.getEnclosingClassNode();
        if (declaringClass != enclosingClass || typeCheckingContext.getEnclosingClosure() != null) {
            if (mn.isPrivate() && inSameNest(declaringClass, enclosingClass)) {
                if (source instanceof MethodReferenceExpression // GROOVY-11301: access bridge for lambda
                        && Float.parseFloat(getSourceUnit().getConfiguration().getTargetBytecode()) < 15)
                    declaringClass.getNodeMetaData(PV_METHODS_ACCESS, k -> new LinkedHashSet<MethodNode>()).add(mn);
                source.putNodeMetaData(PV_METHODS_ACCESS, mn);
            } else if (mn.isProtected() && !inSamePackage(enclosingClass, declaringClass)
                    && (!implementsInterfaceOrIsSubclassOf(enclosingClass, declaringClass)
                                    || typeCheckingContext.getEnclosingClosure() != null)) {
                ClassNode cn = enclosingClass;
                do {
                    if (implementsInterfaceOrIsSubclassOf(cn, declaringClass)) {
                        cn.getNodeMetaData(PV_METHODS_ACCESS, k -> new LinkedHashSet<MethodNode>()).add(mn);
                        source.putNodeMetaData(PV_METHODS_ACCESS, mn);
                        break;
                    }
                } while ((cn = cn.getOuterClass()) != null);
            }
        }
    }

    /** {@inheritDoc} */
    @Override
    public void visitVariableExpression(final VariableExpression vexp) {
        super.visitVariableExpression(vexp);
        if (storeTypeForSuper(vexp)) return;
        if (storeTypeForThis(vexp)) return;

        final String name = vexp.getName();
        final Variable accessedVariable = vexp.getAccessedVariable();
        final TypeCheckingContext.EnclosingClosure enclosingClosure = typeCheckingContext.getEnclosingClosure();

        if (accessedVariable instanceof DynamicVariable) {
            // a dynamic variable is either a closure property, a class member referenced from a closure, or an undeclared variable

            if (enclosingClosure != null) {
                switch (name) {
                  case "delegate":
                    var dm = getDelegationMetadata(enclosingClosure.getClosureExpression());
                    if (dm != null) {
                        vexp.putNodeMetaData(INFERRED_TYPE, dm.getType());
                        return;
                    }
                    // falls through
                  case "owner":
                    if (typeCheckingContext.getEnclosingClosureStack().size() > 1) {
                        vexp.putNodeMetaData(INFERRED_TYPE, CLOSURE_TYPE.getPlainNodeReference());
                        return;
                    }
                    // falls through
                  case "thisObject":
                    vexp.putNodeMetaData(INFERRED_TYPE, makeThis());
                    return;
                  case "parameterTypes":
                    vexp.putNodeMetaData(INFERRED_TYPE, CLASS_Type.getPlainNodeReference().makeArray());
                    return;
                  case "maximumNumberOfParameters":
                  case "resolveStrategy":
                  case "directive":
                    vexp.putNodeMetaData(INFERRED_TYPE, int_TYPE);
                    return;
                  case "metaClass": // GROOVY-11386
                    vexp.putNodeMetaData(INFERRED_TYPE, METACLASS_TYPE);
                    return;
                }
            }

            if (tryVariableExpressionAsProperty(vexp, name)) return;

            if (!extension.handleUnresolvedVariableExpression(vexp)) {
                addStaticTypeError("The variable [" + name + "] is undeclared.", vexp);
            }
        } else if (accessedVariable instanceof FieldNode) {
            if (tryVariableExpressionAsProperty(vexp, name)) {
                ClassNode temporaryType = getInferredTypeFromTempInfo(vexp, null); // GROOVY-9454
                if (temporaryType != null && !isObjectType(temporaryType)) {
                    vexp.putNodeMetaData(INFERRED_TYPE, temporaryType);
                }
                if (vexp.getAccessedVariable() != accessedVariable) { // GROOVY-11360: field hidden by dynamic property
                    if (vexp.getLineNumber() > 0 && !typeCheckingContext.reportedErrors.contains(((long) vexp.getLineNumber()) << 16 + vexp.getColumnNumber())) {
                        String text = "The field: " + accessedVariable.getName() + " of class: " + prettyPrintTypeName(((FieldNode) accessedVariable).getDeclaringClass()) +
                                                " is hidden by a dynamic property. A qualifier is required to reference it.";
                        Token token = new Token(0, name, vexp.getLineNumber(), vexp.getColumnNumber()); // ASTNode to CSTNode
                        typeCheckingContext.getErrorCollector().addWarning(new WarningMessage(WarningMessage.POSSIBLE_ERRORS, text, token, getSourceUnit()));
                    }
                }
            } else if (!extension.handleUnresolvedVariableExpression(vexp)) { // GROOVY-11356
                addStaticTypeError("No such property: " + name + " for class: " + prettyPrintTypeName(typeCheckingContext.getEnclosingClassNode()), vexp);
            }
        } else if (accessedVariable instanceof PropertyNode) {
            // we must be careful -- the property node may be of a wrong type:
            // if a class contains a getter and a setter of different types or
            // overloaded setters, the type of the property node is arbitrary!
            if (tryVariableExpressionAsProperty(vexp, name)) {
                BinaryExpression enclosingBinaryExpression = typeCheckingContext.getEnclosingBinaryExpression();
                if (enclosingBinaryExpression != null) {
                    Expression leftExpression = enclosingBinaryExpression.getLeftExpression();
                    SetterInfo setterInfo = removeSetterInfo(leftExpression);
                    if (setterInfo != null) {
                        Expression rightExpression = enclosingBinaryExpression.getRightExpression();
                        ensureValidSetter(vexp, leftExpression, rightExpression, setterInfo);
                    }
                }
            } else if (!extension.handleUnresolvedVariableExpression(vexp)) { // GROOVY-11356
                addStaticTypeError("No such property: " + name + " for class: " + prettyPrintTypeName(typeCheckingContext.getEnclosingClassNode()), vexp);
            }
        } else if (accessedVariable != null) {
            VariableExpression localVariable;
            if (accessedVariable instanceof Parameter prm) {
                localVariable = new ParameterVariableExpression(prm);
            } else {
                localVariable = (VariableExpression) accessedVariable;
            }

            ClassNode inferredType = getInferredTypeFromTempInfo(localVariable, localVariable.getNodeMetaData(INFERRED_TYPE));
            if (inferredType != null && !isObjectType(inferredType) && !inferredType.equals(isTraitSelf(vexp))) {
                vexp.putNodeMetaData(INFERRED_TYPE, inferredType);
            }
        }
    }

    private boolean storeTypeForSuper(final VariableExpression vexp) {
        if (vexp == VariableExpression.SUPER_EXPRESSION) return true;
        if (!vexp.isSuperExpression()) return false;
        storeType(vexp, makeSuper());
        return true;
    }

    private boolean storeTypeForThis(final VariableExpression vexp) {
        if (vexp == VariableExpression.THIS_EXPRESSION) return true;
        if (!vexp.isThisExpression()) return false;
        // GROOVY-6904, GROOVY-9422: non-static inner class constructor call sets type
        storeType(vexp, !isObjectType(vexp.getType()) ? vexp.getType() : makeThis());
        return true;
    }

    private boolean tryVariableExpressionAsProperty(final VariableExpression vexp, final String dynName) {
        PropertyExpression pexp = thisPropX(true, dynName);
        if (existsProperty(pexp, !typeCheckingContext.isTargetOfEnclosingAssignment(vexp))) {
            vexp.copyNodeMetaData(pexp.getObjectExpression());
            for (Object key : new Object[]{DIRECT_METHOD_CALL_TARGET, DYNAMIC_RESOLUTION, IMPLICIT_RECEIVER, PV_FIELDS_ACCESS, PV_FIELDS_MUTATION, READONLY_PROPERTY}) {
                vexp.putNodeMetaData(key, pexp.getNodeMetaData(key));
            }
            ClassNode type = pexp.getNodeMetaData(INFERRED_TYPE);
            if (vexp.isClosureSharedVariable()) {
                type = wrapTypeIfNecessary(type);
            }
            if (type == null) type = OBJECT_TYPE;
            vexp.putNodeMetaData(INFERRED_TYPE, type); // GROOVY-11007
            String receiver = pexp.getNodeMetaData(IMPLICIT_RECEIVER);
            Boolean dynamic = pexp.getNodeMetaData(DYNAMIC_RESOLUTION);
            // GROOVY-7701, GROOVY-7996: correct false assumption made by VariableScopeVisitor
            if (((receiver != null && !receiver.endsWith("owner")) || Boolean.TRUE.equals(dynamic))
                    && !(vexp.getAccessedVariable() instanceof DynamicVariable)) {
                vexp.setAccessedVariable(new DynamicVariable(dynName, false));
            }
            return true;
        }
        return false;
    }

    /** {@inheritDoc} */
    @Override
    public void visitPropertyExpression(final PropertyExpression expression) {
        boolean readOnly = !typeCheckingContext.isTargetOfEnclosingAssignment(expression);
        if (existsProperty(expression, readOnly)
                || extension.handleUnresolvedProperty(expression)) {
            if (readOnly) { // GROOVY-12305: record narrowed type of instanceof-guarded receiver for later use
                ClassNode temporaryType = getInferredTypeFromTempInfo(expression, expression.getNodeMetaData(INFERRED_TYPE));
                if (temporaryType != null && !isObjectType(temporaryType)) {
                    expression.putNodeMetaData(INFERRED_TYPE, temporaryType);
                }
            }
            return; // resolved or excused
        }
        recordMissingProperty(expression);
    }

    /** {@inheritDoc} */
    @Override
    public void visitAttributeExpression(final AttributeExpression expression) {
        boolean readOnly = true;
        if (existsProperty(expression, readOnly)
                || extension.handleUnresolvedAttribute(expression)) {
            return; // resolved or excused
        }
        recordMissingProperty(expression);
    }

    private void recordMissingProperty(final PropertyExpression expression) {
        FieldNode inaccessibleField = expression.getNodeMetaData("inaccessible field");
        if (inaccessibleField != null) { // GROOVY-12314: the property exists but Java access rules reject it
            addStaticTypeError("Cannot access field: " + inaccessibleField.getName() + " of class: " + prettyPrintTypeName(inaccessibleField.getDeclaringClass())
                    + " from class: " + prettyPrintTypeName(typeCheckingContext.getEnclosingClassNode()), expression.getLineNumber() > 0 ? expression : expression.getProperty());
            return;
        }

        var objectExpression = expression.getObjectExpression();
        var objectExpressionType = findCurrentInstanceOfClass(objectExpression, getType(objectExpression));

        String pattern;
        if (!isClassClassNodeWrappingConcreteType(objectExpressionType)) {
            pattern = "No such {0,choice,1#attribute|2#property}: {1} for class: {2}";
        } else {
            objectExpressionType = objectExpressionType.getGenericsTypes()[0].getType();
            pattern = "No such {0,choice,1#attribute|2#property}: {1} for Class or static {0,choice,1#field|2#property} for class: {2}";
        }

        String error = java.text.MessageFormat.format(pattern, expression instanceof AttributeExpression ? 1 : 2, expression.getPropertyAsString(), prettyPrintTypeName(wrapTypeIfNecessary(objectExpressionType)));
        ASTNode node = expression.getLineNumber() > 0 ? expression : expression.getProperty(); // GROOVY-11663
        addStaticTypeError(error, node);
    }

    /** {@inheritDoc} */
    @Override
    public void visitRangeExpression(final RangeExpression expression) {
        super.visitRangeExpression(expression);
        ClassNode fromType = getWrapper(getType(expression.getFrom()));
        ClassNode toType = getWrapper(getType(expression.getTo()));
        if (isWrapperInteger(fromType) && isWrapperInteger(toType)) {
            storeType(expression, ClassHelper.make(IntRange.class));
        } else {
            ClassNode rangeType = RANGE_TYPE.getPlainNodeReference();
            rangeType.setGenericsTypes(new GenericsType[]{new GenericsType(lowestUpperBound(fromType, toType))});
            storeType(expression, rangeType);
        }
    }

    /** {@inheritDoc} */
    @Override
    public void visitNotExpression(final NotExpression expression) {
        typeCheckingContext.pushTemporaryTypeInfo();
        super.visitNotExpression(expression);
        // GROOVY-9455: !(x instanceof T) shouldn't propagate T as inferred type
        typeCheckingContext.temporaryIfBranchTypeInformation.pop().forEach(this::putNotInstanceOfTypeInfo);
    }

    /** {@inheritDoc} */
    @Override
    public void visitBinaryExpression(final BinaryExpression expression) {
        BinaryExpression enclosingBinaryExpression = typeCheckingContext.getEnclosingBinaryExpression();
        typeCheckingContext.pushEnclosingBinaryExpression(expression);
        try {
            int op = expression.getOperation().getType();
            Expression leftExpression = expression.getLeftExpression();
            Expression rightExpression = expression.getRightExpression();

            if (isAssignment(op) && op != EQUAL) {
                // GEP-15: try a dedicated *Assign method on the LHS type before the legacy desugar.
                // When found, the receiver is mutated in place — no reassignment, setter is skipped.
                String assignName = getAssignOperationName(op);
                if (assignName != null) {
                    // Visit a clone of the LHS so that the property/variable visitor doesn't
                    // treat it as the assignment target (which would steer it to the setter and
                    // leave the type unresolved on the read side).
                    ExpressionTransformer readSideCloner = new ExpressionTransformer() {
                        @Override
                        public Expression transform(final Expression expr) {
                            if (expr instanceof VariableExpression) {
                                return ((VariableExpression) expr).clone();
                            }
                            return expr.transformExpression(this);
                        }
                    };
                    Expression lhsRead = readSideCloner.transform(leftExpression);
                    lhsRead.visit(this);
                    ClassNode lhsType = getType(lhsRead);
                    if (!isPrimitiveType(lhsType)) {
                        rightExpression.visit(this);
                        ClassNode rhsType = isNullConstant(rightExpression)
                                ? UNKNOWN_PARAMETER_TYPE
                                : getInferredTypeFromTempInfo(rightExpression, getType(rightExpression));
                        List<MethodNode> candidates = findMethod(lhsType, assignName, rhsType);
                        if (candidates.size() == 1) {
                            MethodNode target = candidates.get(0);
                            expression.putNodeMetaData(COMPOUND_ASSIGN_TARGET, target);
                            storeType(expression, lhsType);
                            // Visit the actual LHS now (without the cloned read-side context) so its
                            // own metadata is populated for codegen, but skip its setterInfo since
                            // *Assign mutates the receiver in place rather than going through a setter.
                            leftExpression.visit(this);
                            removeSetterInfo(leftExpression);
                            typeCheckMethodsWithGenericsOrFail(lhsType, new ClassNode[]{rhsType}, target, rightExpression);
                            return;
                        }
                    }
                }

                ExpressionTransformer cloneMaker = new ExpressionTransformer() {
                    @Override
                    public Expression transform(final Expression expr) {
                        if (expr instanceof VariableExpression) {
                            return ((VariableExpression) expr).clone();
                        }
                        return expr.transformExpression(this);
                    }
                };
                // GROOVY-10628, GROOVY-11563: for "x op= y", find type as if it was "x = x op y"
                rightExpression = (op == ELVIS_EQUAL ? elvisX(cloneMaker.transform(leftExpression), rightExpression) :
                        binX(cloneMaker.transform(leftExpression), Token.newSymbol(TokenUtil.removeAssignment(op), expression.getOperation().getStartLine(), expression.getOperation().getStartColumn()), rightExpression));
                rightExpression.setSourcePosition(expression);

                visitBinaryExpression(assignX(leftExpression, rightExpression, expression));
                expression.copyNodeMetaData(rightExpression);
                return;
            }

            // GROOVY-7971, GROOVY-8965, GROOVY-10096, GROOVY-10702, et al.
            if (op == LOGICAL_OR) typeCheckingContext.pushTemporaryTypeInfo();

            ClassNode lType;
            leftExpression.visit(this);
            var setterInfo = removeSetterInfo(leftExpression);
            if (setterInfo != null) { assert op != LOGICAL_OR;
                if (ensureValidSetter(expression, leftExpression, rightExpression, setterInfo)) {
                    return;
                }
                lType = getType(leftExpression); // GROOVY-11870
            } else {
                if (op == EQUAL) {
                    lType = getOriginalDeclarationType(leftExpression);
                    applyTargetType(lType, rightExpression);
                } else {
                    lType = getType(leftExpression);
                }
                if (op != LOGICAL_OR) {
                    rightExpression.visit(this);
                } else {
                    var lhs = typeCheckingContext.temporaryIfBranchTypeInformation.pop();
                    typeCheckingContext.pushTemporaryTypeInfo();
                    rightExpression.visit(this);

                    var rhs = typeCheckingContext.temporaryIfBranchTypeInformation.pop();
                    propagateTemporaryTypeInfo(lhs, rhs); // `instanceof` on either side?
                }
            }

            ClassNode rType = isNullConstant(rightExpression)
                    ? isPrimitiveType(lType) ? OBJECT_TYPE : UNKNOWN_PARAMETER_TYPE
                    : getInferredTypeFromTempInfo(rightExpression, getType(rightExpression));
            ClassNode resultType;

            if (op == KEYWORD_IN || op == COMPARE_NOT_IN) {
                // for the "in" or "!in" operator, the receiver and the arguments are reversed
                BinaryExpression reverseExpression = binX(rightExpression, expression.getOperation(), leftExpression);
                resultType = getResultType(rType, op, lType, reverseExpression);
                if (resultType == null) resultType = boolean_TYPE; // GROOVY-10239
                // GROOVY-9848: Map and CharSequence membership (and unknown/Object receivers, resolved at
                // runtime) route through ScriptBytecodeAdapter.isIn -> containsKey / contains. Every other
                // (concrete) receiver keeps the directly-resolved isCase target, so its membership is
                // unchanged and still benefits from static direct dispatch.
                MethodNode inTarget = reverseExpression.getNodeMetaData(DIRECT_METHOD_CALL_TARGET);
                if (inTarget != null && !isObjectType(rType)
                        && !isOrImplements(rType, MAP_TYPE)
                        && !isOrImplements(rType, CHAR_SEQUENCE_TYPE)) {
                    storeTargetMethod(expression, inTarget);
                }
            } else {
                resultType = getResultType(lType, op, rType, expression);
            }
            if (resultType == null) {
                resultType = lType;
            }

            if (isArrayOp(op)) {
                if (leftExpression instanceof VariableExpression) {//GROOVY-6782
                    if (leftExpression.getNodeMetaData(INFERRED_TYPE) == null) {
                        leftExpression.removeNodeMetaData(INFERRED_RETURN_TYPE);
                        storeType(leftExpression, lType);
                    }
                }
                if (!lType.isArray()
                        && enclosingBinaryExpression != null
                        && enclosingBinaryExpression.getLeftExpression() == expression
                        && isAssignment(enclosingBinaryExpression.getOperation().getType())) {
                    // left hand side of a subscript assignment: map['key'] = ...
                    Expression enclosingExpressionRHS = enclosingBinaryExpression.getRightExpression();
                    if (!(enclosingExpressionRHS instanceof ClosureExpression)) {
                        enclosingExpressionRHS.visit(this);
                    }
                    ClassNode[] arguments = {rType, isNullConstant(enclosingExpressionRHS) // GROOVY-11621
                                ? UNKNOWN_PARAMETER_TYPE : getType(enclosingExpressionRHS)};
                    List<MethodNode> methods = findMethod(lType, "putAt", arguments);
                    if (methods.isEmpty()) {
                        addNoMatchingMethodError(lType, "putAt", arguments, enclosingBinaryExpression);
                    } else if (methods.size() == 1) {
                        typeCheckMethodsWithGenericsOrFail(lType, arguments, methods.get(0), enclosingExpressionRHS);
                    }
                }
            }

            boolean isEmptyDeclaration = (expression instanceof DeclarationExpression
                    && (rightExpression instanceof EmptyExpression || rType == UNKNOWN_PARAMETER_TYPE));
            if (isEmptyDeclaration) {
                expression.putNodeMetaData(INFERRED_TYPE, lType);
                // GROOVY-11353: "def var = null" cannot be a primitive type
                if (isDynamicTyped(lType) && rType == UNKNOWN_PARAMETER_TYPE)
                    lType.putNodeMetaData("non-primitive type", Boolean.TRUE);
            } else if (isAssignment(op)) {
                if (rightExpression instanceof ConstructorCallExpression)
                    inferDiamondType((ConstructorCallExpression) rightExpression, lType);

                // handle unchecked assignment: List<Type> list = []
                resultType = adjustForTargetType(resultType, lType);

                ClassNode originType = getOriginalDeclarationType(leftExpression);
                typeCheckAssignment(expression, leftExpression, originType, rightExpression, resultType);
                // check for implicit conversion like "String a = 123", "int[] b = [1,2,3]", "List c = [].stream()", etc.
                if (!implementsInterfaceOrIsSubclassOf(wrapTypeIfNecessary(resultType), wrapTypeIfNecessary(originType))) {
                    resultType = originType;
                } else {
                    // GROOVY-7549: RHS type may not be accessible to enclosing class
                    int modifiers = resultType.getModifiers();
                    ClassNode enclosingType = typeCheckingContext.getEnclosingClassNode();
                    if (!Modifier.isPublic(modifiers) && !enclosingType.equals(resultType)
                            && !getNestHost(enclosingType).equals(getNestHost(resultType))
                            && (Modifier.isPrivate(modifiers) || !inSamePackage(enclosingType, resultType))) {
                        resultType = originType; // TODO: Find accessible type in hierarchy of resultType?
                    } else if (GenericsUtils.hasUnresolvedGenerics(resultType)) { // GROOVY-9033, GROOVY-10089, et al.
                        Map<GenericsTypeName, GenericsType> enclosing = extractGenericsParameterMapOfThis(typeCheckingContext);
                        resultType = fullyResolveType(resultType, Optional.ofNullable(enclosing).orElseGet(Collections::emptyMap));
                    }
                }

                // track conditional assignment
                if (leftExpression instanceof VariableExpression
                        && typeCheckingContext.ifElseForWhileAssignmentTracker != null) {
                    var targetVariable = ((VariableExpression) leftExpression).getAccessedVariable();
                    if (targetVariable instanceof Parameter) {
                        targetVariable = new ParameterVariableExpression((Parameter) targetVariable);
                    }
                    if (targetVariable instanceof VariableExpression) {
                        recordAssignment((VariableExpression) targetVariable, resultType);
                    }
                }

                storeType(leftExpression, resultType);

                // propagate closure parameter information
                if (leftExpression instanceof VariableExpression
                        // GROOVY-11400: save to declared variable; skip field, property, parameter or dynamic variable
                        && ((VariableExpression) leftExpression).getAccessedVariable() instanceof VariableExpression targetVariable) {
                    if (rightExpression instanceof ClosureExpression closure) {
                        if (!hasImplicitParameter(closure)) // GROOVY-11394: arrow means zero parameters
                            targetVariable.putNodeMetaData(CLOSURE_ARGUMENTS, getParametersSafe(closure));
                    } else if (rightExpression instanceof VariableExpression
                            && ((VariableExpression) rightExpression).getAccessedVariable() instanceof VariableExpression sourceVariable) {
                        targetVariable.putNodeMetaData(CLOSURE_ARGUMENTS, sourceVariable.getNodeMetaData(CLOSURE_ARGUMENTS));
                    }
                }
            } else if (op == KEYWORD_INSTANCEOF) {
                rType = rightExpression.getType();
                if (lType instanceof WideningCategories.LowestUpperBoundClassNode) lType = lType.getUnresolvedSuperClass();
                if (lType instanceof UnionTypeClassNode union) {
                    boolean compatible = rType.isInterface();
                    for (int i = 0; !compatible && i < union.getDelegates().length; i += 1) {
                        ClassNode delegate = union.getDelegates()[i];
                        compatible = delegate.isInterface() || delegate.isDerivedFrom(rType) || rType.isDerivedFrom(delegate);
                    }
                    if (!compatible) {
                        addError("Incompatible instanceof types: " + prettyPrintTypeName(lType) + " and " + prettyPrintTypeName(rType), expression);
                    }
                } else if (!lType.isInterface() && !rType.isInterface() && !(lType.isDerivedFrom(rType) || rType.isDerivedFrom(lType))) {
                    addError("Incompatible instanceof types: " + prettyPrintTypeName(lType) + " and " + prettyPrintTypeName(rType), expression);
                }
                pushInstanceOfTypeInfo(leftExpression, rightExpression);
            } else if (op == COMPARE_NOT_INSTANCEOF) { // GROOVY-6429, GROOVY-8321, GROOVY-8412, GROOVY-8523, GROOVY-9931
                putNotInstanceOfTypeInfo(extractTemporaryTypeInfoKey(leftExpression), Set.of(rightExpression.getType()));
            }
            if (!isEmptyDeclaration) {
                storeType(expression, resultType);
                validateResourceInARM(expression, resultType);
            }

            // GROOVY-5874: if left expression is a closure shared variable, a second pass should be done
            if (leftExpression instanceof VariableExpression vexp && vexp.isClosureSharedVariable()) {
                typeCheckingContext.secondPassExpressions.add(new SecondPassExpression<>(expression));
            }
        } finally {
            typeCheckingContext.popEnclosingBinaryExpression();
        }
    }

    private void propagateTemporaryTypeInfo(final Map<Object, List<ClassNode>> lhs,
                                            final Map<Object, List<ClassNode>> rhs) {
        // TODO: deal with (x !instanceof T)
        lhs.keySet().removeIf(k -> k instanceof Object[]);
        rhs.keySet().removeIf(k -> k instanceof Object[]);

        for (var entry : lhs.entrySet()) {
            if (rhs.containsKey(entry.getKey())) {
                // main case: (x instanceof A || x instanceof B) produces A|B type
                ClassNode t = newUnionTypeClassNode(DefaultGroovyMethods.plus(entry.getValue(), rhs.get(entry.getKey())));
                typeCheckingContext.peekTemporaryTypeInfo(entry.getKey()).add(t);
            } else if (entry.getKey() instanceof Variable v) {
                // edge case: (x instanceof A || ...) produces A|typeof(x) type
                ClassNode t = newUnionTypeClassNode(DefaultGroovyMethods.plus(entry.getValue(), v instanceof ASTNode n ? getType(n) : v.getType()));
                typeCheckingContext.peekTemporaryTypeInfo(v).add(t);
            }
        }

        rhs.keySet().removeAll(lhs.keySet());

        for (var entry : rhs.entrySet()) {
            if (entry.getKey() instanceof Variable v) {
                // edge case: (... || x instanceof B) produces typeof(x)|B type
                var types = new LinkedList<>(entry.getValue());
                types.addFirst(v instanceof ASTNode n ? getType(n) : v.getType());
                typeCheckingContext.peekTemporaryTypeInfo(v).add(newUnionTypeClassNode(types));
            }
        }
    }

    private void validateResourceInARM(final BinaryExpression expression, final ClassNode lType) {
        if (expression instanceof DeclarationExpression
                && TryCatchStatement.isResource(expression)
                && !isOrImplements(lType, AUTOCLOSEABLE_TYPE)) {
            addError("Resource[" + lType.getName() + "] in ARM should be of type AutoCloseable", expression);
        }
    }

    private void applyTargetType(final ClassNode target, final Expression source) {
        if (isClosureWithType(target)) {
            if (source instanceof ClosureExpression) {
                GenericsType returnType = target.getGenericsTypes()[0];
                storeInferredReturnType(source, getCombinedBoundType(returnType));
            }
        } else if (isFunctionalInterface(target)) {
            if (source instanceof ClosureExpression) {
                inferParameterAndReturnTypesOfClosureOnRHS(target, (ClosureExpression) source);
            } else if (source instanceof MapExpression) { // GROOVY-7141
                List<MapEntryExpression> spec = ((MapExpression) source).getMapEntryExpressions();
                if (spec.size() == 1 && spec.get(0).getValueExpression() instanceof ClosureExpression
                        && findSAM(target).getName().equals(spec.get(0).getKeyExpression().getText())) {
                    inferParameterAndReturnTypesOfClosureOnRHS(target, (ClosureExpression) spec.get(0).getValueExpression());
                }
            } else if (source instanceof MethodReferenceExpression) {
                LambdaExpression lambda = constructLambdaExpressionForMethodReference(target, (MethodReferenceExpression) source);

                inferParameterAndReturnTypesOfClosureOnRHS(target, lambda);
                source.putNodeMetaData(PARAMETER_TYPE, lambda.getNodeMetaData(PARAMETER_TYPE));
                source.putNodeMetaData(CLOSURE_ARGUMENTS, extractTypesFromParameters(lambda.getParameters()));
            }
        }
    }

    private void inferParameterAndReturnTypesOfClosureOnRHS(final ClassNode lhsType, final ClosureExpression rhsExpression) {
        ClassNode[] samParameterTypes;
        {
            Tuple2<ClassNode[], ClassNode> typeInfo = GenericsUtils.parameterizeSAM(lhsType);
            storeInferredReturnType(rhsExpression, typeInfo.getV2());
            samParameterTypes = typeInfo.getV1();
        }

        Parameter[] closureParameters = getParametersSafe(rhsExpression);
        if (samParameterTypes.length == 1 && hasImplicitParameter(rhsExpression)) {
            Variable it = rhsExpression.getVariableScope().getDeclaredVariable("it"); // GROOVY-7141
            closureParameters = new Parameter[]{it instanceof Parameter ? (Parameter) it : new Parameter(dynamicType(),"it")};
        }

        int n = closureParameters.length;
        ClassNode[] expectedTypes = samParameterTypes;
out:    if ((samParameterTypes.length == 1 && isOrImplements(samParameterTypes[0], LIST_TYPE)) // GROOVY-11092
                && (n != 1 || !typeCheckMethodArgumentWithGenerics(GenericsUtils.nonGeneric(closureParameters[0].getOriginType()), samParameterTypes[0], false))) {
            int itemCount = TUPLE_TYPES.indexOf(samParameterTypes[0]);
            if (itemCount >= 0) {//Tuple[0-16]
                if (itemCount != n) break out;
                GenericsType[] spec = samParameterTypes[0].getGenericsTypes();
                if (spec != null) { // edge case: raw type or Tuple0 falls through
                    expectedTypes = Arrays.stream(spec).map(GenericsType::getType).toArray(ClassNode[]::new);
                    break out;
                }
            }
            expectedTypes = new ClassNode[n];
            Arrays.fill(expectedTypes, inferLoopElementType(samParameterTypes[0]));
        }

        if (n == expectedTypes.length) {
            for (int i = 0; i < n; i += 1) {
                Parameter parameter = closureParameters[i];
                if (parameter.isDynamicTyped()) {
                    ClassNode type = expectedTypes[i].getPlainNodeReference(false); // GROOVY-11479
                    if (!expectedTypes[i].isGenericsPlaceHolder())
                        type.setGenericsTypes(expectedTypes[i].getGenericsTypes());
                    parameter.setType(type); // GROOVY-11083, GROOVY-11085, et al.
                } else {
                    checkParamType(parameter, expectedTypes[i], i == n-1, rhsExpression instanceof LambdaExpression);
                }
            }
            rhsExpression.putNodeMetaData(CLOSURE_ARGUMENTS, expectedTypes);
        } else {
            addStaticTypeError("Wrong number of parameters for method target: " + toMethodParametersString(findSAM(lhsType).getName(), samParameterTypes), rhsExpression);
        }

        rhsExpression.putNodeMetaData(PARAMETER_TYPE, expectedTypes == samParameterTypes ? lhsType : null);
    }

    private void checkParamType(final Parameter source, final ClassNode target, final boolean isLast, final boolean lambda) {
        if (/*lambda ? !source.getOriginType().isDerivedFrom(target) :*/!typeCheckMethodArgumentWithGenerics(source.getOriginType(), target, isLast))
            addStaticTypeError("Expected type " + prettyPrintType(target) + " for " + (lambda ? "lambda" : "closure") + " parameter: " + source.getName(), source);
    }

    /**
     * Given a binary expression corresponding to an assignment, will check that
     * the type of the RHS matches one of the possible setters and if not, throw
     * a type checking error.
     *
     * @param expression      the assignment expression
     * @param leftExpression  left expression of the assignment
     * @param rightExpression right expression of the assignment
     * @param setterInfo      possible setters
     * @return {@code false} if valid setter found or {@code true} if type checking error created
     */
    private boolean ensureValidSetter(final Expression expression, final Expression leftExpression, final Expression rightExpression, final SetterInfo setterInfo) {
        // for expressions like foo = { ... }
        // we know that the RHS type is a closure
        // but we must check if the binary expression is an assignment
        // because we need to check if a setter uses @DelegatesTo
        VariableExpression receiver = varX("%", setterInfo.receiverType);
        receiver.setType(setterInfo.receiverType); // same as origin type

        Function<Expression, MethodNode> setterCall = (value) -> {
            typeCheckingContext.pushEnclosingBinaryExpression(null); // GROOVY-10628: LHS re-purposed
            try {
                MethodCallExpression call = callX(receiver, setterInfo.name, value);
                call.setImplicitThis(false);
                visitMethodCallExpression(call);
                return call.getNodeMetaData(DIRECT_METHOD_CALL_TARGET);
            } finally {
                typeCheckingContext.popEnclosingBinaryExpression();
            }
        };

        Function<MethodNode, ClassNode> setterType = (setter) -> {
            ClassNode type = setter.getParameters()[0].getOriginType();
            if (!setter.isStatic() && !(setter instanceof ExtensionMethodNode) && GenericsUtils.hasUnresolvedGenerics(type)) {
                type = applyGenericsContext(extractPlaceHolders(setterInfo.receiverType, setter.getDeclaringClass()), type);
            }
            return type;
        };

        Expression valueExpression = rightExpression;
        // for "x op= y", find type as if it was "x = x op y"
        if (isCompoundAssignment(expression)) {
            Token op = ((BinaryExpression) expression).getOperation();
            if (op.getType() == ELVIS_EQUAL) { // GROOVY-10419: "x ?= y"
                valueExpression = elvisX(leftExpression, rightExpression);
            } else {
                op = Token.newSymbol(TokenUtil.removeAssignment(op.getType()), op.getStartLine(), op.getStartColumn());
                valueExpression = binX(leftExpression, op, rightExpression);
            }
        }

        MethodNode methodTarget = setterCall.apply(valueExpression);
        if (methodTarget == null && !isCompoundAssignment(expression)) {
            // if no direct match, try implicit conversion
            for (MethodNode setter : setterInfo.setters) {
                ClassNode lType = setterType.apply(setter);
                ClassNode rType = getDeclaredOrInferredType(valueExpression);
                if (lType.isArray() && valueExpression instanceof ListExpression) {
                    rType = inferLoopElementType(rType).makeArray(); // GROOVY-7506
                }
                if (checkCompatibleAssignmentTypes(lType, rType, valueExpression, false)) {
                    methodTarget = setterCall.apply(castX(lType, valueExpression));
                    if (methodTarget != null) {
                        break;
                    }
                }
            }
        }

        if (methodTarget != null) {
            for (MethodNode setter : setterInfo.setters) {
                if (setter == methodTarget) {
                    leftExpression.putNodeMetaData(DIRECT_METHOD_CALL_TARGET, methodTarget);
                    leftExpression.removeNodeMetaData(INFERRED_TYPE); // clear assumption
                    storeType(leftExpression, setterType.apply(methodTarget));
                    break;
                }
            }
            return false;
        } else {
            List<MethodNode> visibleSetters = filterMethodsByVisibility(setterInfo.setters, typeCheckingContext.getEnclosingClassNode());
            if (visibleSetters.isEmpty()) {
                String message; // GROOVY-11223
                if (setterInfo.setters.size() == 1) {
                    message = String.format("Cannot access method: %2$s of class: %1$s",
                        prettyPrintTypeName(setterInfo.setters.get(0).getDeclaringClass()),
                        toMethodParametersString(setterInfo.name, extractTypesFromParameters(setterInfo.setters.get(0).getParameters())));
                } else {
                    message = "Cannot access methods: " + prettyPrintMethodList(setterInfo.setters);
                }
                addStaticTypeError(message, leftExpression);
            } else {
                List<ClassNode> targetTypes = visibleSetters.stream().map(setterType).toList();
                addAssignmentError(newUnionTypeClassNode(targetTypes), getType(valueExpression), expression);
            }
            return true;
        }
    }

    private static boolean isClosureWithType(final ClassNode type) {
        return CLOSURE_TYPE.equals(type) && Optional.ofNullable(type.getGenericsTypes()).filter(gts -> gts != null && gts.length == 1).isPresent();
    }

    private static boolean isCompoundAssignment(final Expression exp) {
        if (exp instanceof BinaryExpression) {
            Token op = ((BinaryExpression) exp).getOperation();
            return isAssignment(op.getType()) && op.getType() != EQUAL;
        }
        return false;
    }

    /**
     * Returns the declared type of the left-hand side before any flow-based refinement.
     */
    protected ClassNode getOriginalDeclarationType(final Expression lhs) {
        Variable var = null;
        if (lhs instanceof FieldExpression) {
            var = ((FieldExpression) lhs).getField();
        } else if (lhs instanceof VariableExpression) {
            var = findTargetVariable((VariableExpression) lhs);
        }
        return var != null && !(var instanceof DynamicVariable) ? var.getOriginType() : getType(lhs);
    }

    /**
     * Infers generic arguments for constructor calls that use the diamond operator.
     */
    protected void inferDiamondType(final ConstructorCallExpression cce, final ClassNode lType) {
        ClassNode cceType = cce.getType(), inferredType = lType;
        boolean isAIC = cce.isUsingAnonymousInnerClass();
        if (isAIC) {
            ClassNode diamond = GenericsUtils.diamondTargetOfAnonymousClass(cce.getType());
            if (diamond != null) cceType = diamond;
        }
        // check if constructor call expression makes use of the diamond operator
        if (cceType.getGenericsTypes() != null && cceType.getGenericsTypes().length == 0) {
            ArgumentListExpression argumentList = InvocationWriter.makeArgumentList(cce.getArguments());
            ConstructorNode constructor = cce.getNodeMetaData(DIRECT_METHOD_CALL_TARGET);
            if (isAIC && constructor != null && constructor.getDeclaringClass() != cceType.redirect()) {
                constructor = matchingConstructor(cceType.redirect(), argumentList.getExpressions().size());
            }
            if (!argumentList.getExpressions().isEmpty() && constructor != null) {
                ClassNode type = GenericsUtils.parameterizeType(cceType, cceType);
                type = inferReturnTypeGenerics(type, constructor, argumentList);
                if (lType.getGenericsTypes() != null // GROOVY-10367: nothing to inspect
                        && (type.toString(false).indexOf('#') > 0 // GROOVY-9983, GROOVY-10291, GROOVY-10368: unresolved generic
                        // GROOVY-6232, GROOVY-9956: if cce not assignment compatible, process target as additional type witness
                        || checkCompatibleAssignmentTypes(lType, type, cce) && !GenericsUtils.buildWildcardType(lType).isCompatibleWith(type))) {
                    // allow covariance of each type parameter, but maintain semantics for nested generics

                    ClassNode pType = GenericsUtils.parameterizeType(lType, type);
                    GenericsType[] lhs = pType.getGenericsTypes(), rhs = type.getGenericsTypes();
                    if (lhs == null || rhs == null || lhs.length != rhs.length) throw new GroovyBugError(
                            "Parameterization failed: " + prettyPrintType(pType) + " ~ " + prettyPrintType(type));

                    if (IntStream.range(0, lhs.length).allMatch(i ->
                            GenericsUtils.buildWildcardType(getCombinedBoundType(lhs[i])).isCompatibleWith(rhs[i].getType()))) {
                        type = pType; // lType proved to be a viable type witness
                    }
                }
                inferredType = type;
            }
            // GROOVY-10344, GROOVY-10847: "T t = new C<>()" where "T" may extend another type parameter
            while (inferredType.isGenericsPlaceHolder() && asBoolean(inferredType.getGenericsTypes())) {
                inferredType = getCombinedBoundType(inferredType.getGenericsTypes()[0]);
            }
            adjustGenerics(inferredType, cceType);
            storeType(cce, isAIC ? cce.getType() : cceType);
        }
    }

    private static ConstructorNode matchingConstructor(final ClassNode type, final int argumentCount) {
        if (type == null || type.isInterface()) return null;
        for (ConstructorNode ctor : type.getDeclaredConstructors()) {
            if (ctor.getParameters().length == argumentCount) return ctor;
        }
        return null;
    }

    private void adjustGenerics(final ClassNode source, final ClassNode target) {
        GenericsType[] genericsTypes = source.getGenericsTypes();
        if (genericsTypes == null) { // Map foo = new HashMap<>()
            genericsTypes = target.redirect().getGenericsTypes().clone();
            for (int i = 0, n = genericsTypes.length; i < n; i += 1) {
                GenericsType gt = genericsTypes[i];
                // GROOVY-10055: handle diamond or raw
                ClassNode cn = gt.getUpperBounds() != null
                        ? gt.getUpperBounds()[0] : gt.getType().redirect();
                genericsTypes[i] = cn.getPlainNodeReference().asGenericsType();
            }
        } else {
            // GROOVY-11192: mapping between source and target type parameter(s)
            if (!source.equals(target)) {
                assert source.isInterface() ? target.implementsInterface(source) : target.isDerivedFrom(source);
                ClassNode mapped = adjustForTargetType(target, source);
                genericsTypes = mapped.getGenericsTypes();
            }
            genericsTypes = genericsTypes.clone();
            for (int i = 0, n = genericsTypes.length; i < n; i += 1) {
                GenericsType gt = genericsTypes[i];
                genericsTypes[i] = new GenericsType(gt.getType(),
                        gt.getUpperBounds(), gt.getLowerBound());
                genericsTypes[i].setWildcard(gt.isWildcard()); // GROOVY-10310
            }
        }
        target.setGenericsTypes(genericsTypes);
    }

    private boolean typeCheckMultipleAssignmentAndContinue(final Expression leftExpression, final Expression rightExpression) {
        // GEP-20 dispatch by LHS shape. Detect rest binders and map-style binders up front;
        // the existing literal-RHS pathway lives inside typeCheckMultipleAssignmentPositional.
        // The rest- and map-style helpers are currently scaffolding that delegates back to the
        // positional path; subsequent slices fill them in (rest-aware size handling, container-
        // type inference, property-typed map-style binders, declared-type RHS support).
        TupleExpression tuple = (TupleExpression) leftExpression;
        boolean hasRest = false, hasMapKey = false;
        for (Expression e : tuple.getExpressions()) {
            if (Boolean.TRUE.equals(e.getNodeMetaData(MultipleAssignmentMetadata.REST_BINDING))) hasRest = true;
            if (e.getNodeMetaData(MultipleAssignmentMetadata.MAP_KEY) != null) hasMapKey = true;
        }
        if (hasMapKey) {
            return typeCheckMultipleAssignmentMapStyle(leftExpression, rightExpression);
        }
        if (hasRest) {
            return typeCheckMultipleAssignmentRest(leftExpression, rightExpression);
        }
        return typeCheckMultipleAssignmentPositional(leftExpression, rightExpression);
    }

    private boolean typeCheckMultipleAssignmentPositional(final Expression leftExpression, Expression rightExpression) {
        if (rightExpression instanceof VariableExpression || rightExpression instanceof PropertyExpression || rightExpression instanceof MethodCall) {
            ClassNode inferredType = Optional.ofNullable(getType(rightExpression)).orElseGet(rightExpression::getType);
            GenericsType[] genericsTypes = inferredType.getGenericsTypes();
            ListExpression listExpression = new ListExpression();
            listExpression.setSourcePosition(rightExpression);

            // convert Tuple[1-16] bearing expressions to mock list for checking
            for (int n = TUPLE_TYPES.indexOf(inferredType), i = 0; i < n; i += 1) {
                ClassNode type = (genericsTypes != null ? genericsTypes[i].getType() : OBJECT_TYPE);
                listExpression.addExpression(varX("v" + (i + 1), type));
            }
            if (!listExpression.getExpressions().isEmpty()) {
                rightExpression = listExpression;
            } else {
                rightExpression = synthesizeIndexableRhs(leftExpression, rightExpression, inferredType);
            }
        } else if (rightExpression instanceof RangeExpression) {
            ListExpression listExpression = new ListExpression();
            listExpression.setSourcePosition(rightExpression);
            ClassNode type = getType(((RangeExpression) rightExpression).getFrom());
            TupleExpression tuple = (TupleExpression) leftExpression;
            for (int i = 0; i < tuple.getExpressions().size(); i++) {
                Expression expression = indexX(rightExpression, constX(i, true));
                expression.putNodeMetaData(INFERRED_TYPE, type);
                listExpression.addExpression(expression);
            }
            if (!listExpression.getExpressions().isEmpty()) {
                rightExpression = listExpression;
            }
        }

        // Final indexable-RHS fallback: anything still not a ListExpression (e.g. String /
        // GString constants, closure-call results that didn't match Tuple1..16) — try the
        // indexable synthesis based on the RHS's static type.
        if (!(rightExpression instanceof ListExpression)) {
            ClassNode rhsType = Optional.ofNullable(getType(rightExpression)).orElseGet(rightExpression::getType);
            rightExpression = synthesizeIndexableRhs(leftExpression, rightExpression, rhsType);
        }

        if (!(rightExpression instanceof ListExpression values)) {
            addStaticTypeError("Multiple assignments without list or tuple on the right-hand side are unsupported in static type checking mode", rightExpression);
            return false;
        }

        TupleExpression tuple = (TupleExpression) leftExpression;
        List<Expression> tupleExpressions = tuple.getExpressions();
        List<Expression> valueExpressions = values.getExpressions();

        if (tupleExpressions.size() > valueExpressions.size()) {
            addStaticTypeError("Incorrect number of values. Expected:" + tupleExpressions.size() + " Was:" + valueExpressions.size(), values);
            return false;
        }

        for (int i = 0, n = tupleExpressions.size(); i < n; i += 1) {
            Expression tupleExpression = tupleExpressions.get(i);
            ClassNode valueType = getType(valueExpressions.get(i));
            ClassNode targetType = getType(tupleExpression);
            if (!isAssignableTo(valueType, targetType)) {
                addStaticTypeError("Cannot assign value of type " + prettyPrintType(valueType) + " to variable of type " + prettyPrintType(targetType), rightExpression);
                return false;
            }
            // GROOVY-12228: check for implicit conversion like "String a = 123" or "Long b = 1";
            // as for single assignment, the target then holds its declared type, not the value type
            ClassNode originType = getOriginalDeclarationType(tupleExpression);
            if (!implementsInterfaceOrIsSubclassOf(wrapTypeIfNecessary(valueType), wrapTypeIfNecessary(originType))) {
                valueType = originType;
            }
            storeType(tupleExpression, valueType);
        }

        return true;
    }

    /**
     * GEP-20 rest binding under static checking. Handles both list-literal RHS and
     * declared-type RHS (List, array, String, Range, Set, Iterator, Stream — anything with a
     * typed {@code getAt(int)} or {@code iterator()} inferable element type). The rest
     * variable's inferred type tracks the runtime dispatch in
     * {@code MultipleAssignmentSupport.tailRest}: {@code List<T>} for Path B receivers
     * (those that support {@code getAt(IntRange)}); {@code Stream<T>} for {@code Stream}
     * RHS (Path A, tail rest only); {@code Iterator<T>} for any other Path C receiver
     * (tail rest only). Head and middle rest reject Path C and Path A receivers as a
     * static type error.
     */
    private boolean typeCheckMultipleAssignmentRest(final Expression leftExpression, final Expression rightExpression) {
        if (rightExpression instanceof ListExpression values) {
            return typeCheckRestAgainstListLiteral((TupleExpression) leftExpression, values, rightExpression);
        }
        // Any RHS with a statically-resolvable getAt(int) (or known shape via declared type)
        // routes through the declared-type path; this covers variables, properties, method
        // calls, constants (e.g. String literals), closure invocations, etc.
        return typeCheckRestAgainstDeclaredType((TupleExpression) leftExpression, rightExpression);
    }

    private boolean typeCheckRestAgainstListLiteral(final TupleExpression tuple, final ListExpression values, final Expression rightExpression) {
        List<Expression> tupleExpressions = tuple.getExpressions();
        List<Expression> valueExpressions = values.getExpressions();
        int tupleSize = tupleExpressions.size();
        int valueSize = valueExpressions.size();

        int restIndex = restIndexOf(tuple);
        int fixedAfter = tupleSize - restIndex - 1;

        // Per GEP-20: lower sizes are forgiving (getAt(int) returns null for OOB,
        // getAt(IntRange) returns empty for inverted ranges). Skip the strict size check.

        // Fixed slots before the rest binding: pairwise from index 0.
        for (int i = 0; i < restIndex; i++) {
            if (i >= valueSize) break;
            ClassNode valueType = getType(valueExpressions.get(i));
            ClassNode targetType = getType(tupleExpressions.get(i));
            if (!isAssignableTo(valueType, targetType)) {
                addStaticTypeError("Cannot assign value of type " + prettyPrintType(valueType) + " to variable of type " + prettyPrintType(targetType), rightExpression);
                return false;
            }
            storeType(tupleExpressions.get(i), valueType);
        }

        // Fixed slots after the rest binding: anchored to the right of the value list.
        for (int j = 0; j < fixedAfter; j++) {
            int lhsIdx = restIndex + 1 + j;
            int rhsIdx = valueSize - fixedAfter + j;
            if (rhsIdx < 0 || rhsIdx >= valueSize) continue;
            ClassNode valueType = getType(valueExpressions.get(rhsIdx));
            ClassNode targetType = getType(tupleExpressions.get(lhsIdx));
            if (!isAssignableTo(valueType, targetType)) {
                addStaticTypeError("Cannot assign value of type " + prettyPrintType(valueType) + " to variable of type " + prettyPrintType(targetType), rightExpression);
                return false;
            }
            storeType(tupleExpressions.get(lhsIdx), valueType);
        }

        // Rest position: container type derived from the LUB of absorbed value types.
        int restFromIdx = Math.min(restIndex, valueSize);
        int restToIdx = Math.max(restFromIdx, valueSize - fixedAfter);
        Expression restExpr = tupleExpressions.get(restIndex);
        ClassNode elementType;
        if (restToIdx > restFromIdx) {
            elementType = getType(valueExpressions.get(restFromIdx));
            for (int k = restFromIdx + 1; k < restToIdx; k++) {
                elementType = lowestUpperBound(elementType, getType(valueExpressions.get(k)));
            }
        } else {
            // Rest absorbs zero values (e.g. `def (Integer a, List<Integer> *t, Integer b) = [1, 2]`).
            // Default to OBJECT_TYPE, but if the rest binder is typed with a single generic
            // argument, use that as the element type so `List<Integer> *t` doesn't get rejected
            // as `List<Object>` not assignable to `List<Integer>`.
            elementType = OBJECT_TYPE;
            if (restExpr instanceof Variable v) {
                ClassNode declared = v.getOriginType();
                if (declared != null && !ClassHelper.isDynamicTyped(declared) && !declared.equals(OBJECT_TYPE)) {
                    GenericsType[] gts = declared.getGenericsTypes();
                    if (gts != null && gts.length == 1) {
                        elementType = getCombinedBoundType(gts[0]);
                    }
                }
            }
        }
        // Generics can't take primitives; box (int → Integer) so List<Integer> matches.
        elementType = ClassHelper.getWrapper(elementType);
        ClassNode restType = GenericsUtils.makeClassSafeWithGenerics(LIST_TYPE, new GenericsType(elementType));
        if (!checkRestAssignability(restExpr, restType, rightExpression)) return false;
        storeType(restExpr, restType);

        return true;
    }

    private boolean typeCheckRestAgainstDeclaredType(final TupleExpression tuple, final Expression rightExpression) {
        ClassNode rhsType = Optional.ofNullable(getType(rightExpression)).orElseGet(rightExpression::getType);
        ClassNode elementType = inferComponentType(rhsType, int_TYPE);
        if (elementType == null) {
            // Non-indexable RHS — let positional surface the existing rejection.
            return typeCheckMultipleAssignmentPositional(tuple, rightExpression);
        }

        List<Expression> tupleExpressions = tuple.getExpressions();
        int tupleSize = tupleExpressions.size();
        int restIndex = restIndexOf(tuple);
        boolean tailRest = restIndex == tupleSize - 1;
        // Path B is now defined by capability, not class membership: anything with a resolvable
        // getAt(IntRange) participates. Probe and reuse the inferred slice type as the rest's
        // declared type so it matches the actual runtime return (BitSet → BitSet, String →
        // String, MyContainer → MyContainer, List<T>/T[] → List<T>, ...).
        ClassNode pathASliceType = inferRangeSliceType(rhsType);
        boolean pathA = pathASliceType != null;

        if (!tailRest && !pathA) {
            addStaticTypeError("Head or middle rest binding requires an indexable right-hand side; "
                    + prettyPrintType(rhsType) + " does not support getAt(IntRange)", rightExpression);
            return false;
        }

        // Non-rest positions all share the element type derived from getAt(int).
        for (int i = 0; i < tupleSize; i++) {
            if (i == restIndex) continue;
            ClassNode targetType = getType(tupleExpressions.get(i));
            if (!isAssignableTo(elementType, targetType)) {
                addStaticTypeError("Cannot assign value of type " + prettyPrintType(elementType) + " to variable of type " + prettyPrintType(targetType), rightExpression);
                return false;
            }
            storeType(tupleExpressions.get(i), elementType);
        }

        // Rest type tracks runtime dispatch:
        //   Path B     → the actual getAt(IntRange) return type (probed above).
        //   Stream RHS → Stream<T> (Path A, tail rest only — laziness + onClose preserved).
        //   Path C     → Iterator<T> (tail rest only).
        // Box primitives so generic containers match (e.g. List<Integer>, not List<int>).
        ClassNode restType;
        if (pathA) {
            restType = pathASliceType;
        } else if (GeneralUtils.isOrImplements(rhsType, STREAM_TYPE)) {
            restType = GenericsUtils.makeClassSafeWithGenerics(STREAM_TYPE,
                    new GenericsType(ClassHelper.getWrapper(elementType)));
        } else {
            restType = GenericsUtils.makeClassSafeWithGenerics(Iterator_TYPE,
                    new GenericsType(ClassHelper.getWrapper(elementType)));
        }
        Expression restExpr = tupleExpressions.get(restIndex);
        if (!checkRestAssignability(restExpr, restType, rightExpression)) return false;
        storeType(restExpr, restType);

        return true;
    }

    /**
     * GEP-20 typed rest support: when the user pins a type on a rest binder
     * ({@code def (h, List<Integer> *t) = list}), verify that the runtime container type
     * (derived from RHS shape and Path B/C selection) is assignable to it. Untyped rest
     * binders ({@code originType == dynamicType}) skip the check.
     */
    private boolean checkRestAssignability(final Expression restExpr, final ClassNode inferredRestType, final Expression rightExpression) {
        if (!(restExpr instanceof Variable v)) return true;
        ClassNode declared = v.getOriginType();
        if (declared == null || ClassHelper.isDynamicTyped(declared) || declared.equals(OBJECT_TYPE)) return true;
        if (!isAssignableTo(inferredRestType, declared)) {
            addStaticTypeError("Cannot assign rest value of type " + prettyPrintType(inferredRestType)
                    + " to variable of type " + prettyPrintType(declared), rightExpression);
            return false;
        }
        return true;
    }

    /**
     * Build a synthesized ListExpression of {@code rhs.getAt(i)} accesses, one per LHS slot,
     * tagged with the inferred element type. Used by {@link #typeCheckMultipleAssignmentPositional}
     * when the RHS isn't a literal list/range/Tuple-typed expression. Returns the original
     * RHS unchanged if its static type doesn't support {@code getAt(int)}.
     */
    private Expression synthesizeIndexableRhs(final Expression leftExpression, final Expression rightExpression, final ClassNode rhsType) {
        ClassNode elementType = inferComponentType(rhsType, int_TYPE);
        if (elementType == null) return rightExpression;
        TupleExpression tuple = (TupleExpression) leftExpression;
        ListExpression synth = new ListExpression();
        synth.setSourcePosition(rightExpression);
        for (int i = 0; i < tuple.getExpressions().size(); i++) {
            Expression idxExpr = indexX(rightExpression, constX(i, true));
            idxExpr.putNodeMetaData(INFERRED_TYPE, elementType);
            synth.addExpression(idxExpr);
        }
        return synth;
    }

    private static int restIndexOf(final TupleExpression tuple) {
        List<Expression> tupleExpressions = tuple.getExpressions();
        for (int i = 0; i < tupleExpressions.size(); i++) {
            if (Boolean.TRUE.equals(tupleExpressions.get(i).getNodeMetaData(MultipleAssignmentMetadata.REST_BINDING))) {
                return i;
            }
        }
        return -1;
    }

    /**
     * GEP-20 map-style destructuring under static checking. Handles both {@code MapExpression}
     * literal RHS and declared-type RHS:
     * <ul>
     *   <li>{@code Map<K, V>} (and subtypes) — binder type derived from the map's V generic.</li>
     *   <li>Bean / class with declared properties — resolved via STC's existing property lookup.</li>
     *   <li>{@code GroovyObject} / dynamic — STC reports the standard "No such property" error
     *       for keys that aren't statically declared (matches GEP failure-modes table for the
     *       static case).</li>
     * </ul>
     */
    private boolean typeCheckMultipleAssignmentMapStyle(final Expression leftExpression, final Expression rightExpression) {
        if (rightExpression instanceof MapExpression mapExpr) {
            return typeCheckMapStyleAgainstMapLiteral((TupleExpression) leftExpression, mapExpr, rightExpression);
        }
        // Any RHS with a statically-resolvable type (variable, parameter, method call,
        // closure invocation, constant) routes through the declared-type path so STC's
        // property-resolution machinery can resolve the map keys against it.
        return typeCheckMapStyleAgainstDeclaredType((TupleExpression) leftExpression, rightExpression);
    }

    private boolean typeCheckMapStyleAgainstMapLiteral(final TupleExpression tuple, final MapExpression mapExpr, final Expression rightExpression) {
        // Build key→value lookup from constant-key entries; non-constant keys are skipped
        // so the binder falls through to the missing-key path (Object).
        Map<String, Expression> rhsByKey = new HashMap<>();
        for (MapEntryExpression entry : mapExpr.getMapEntryExpressions()) {
            if (entry.getKeyExpression() instanceof ConstantExpression key) {
                rhsByKey.put(String.valueOf(key.getValue()), entry.getValueExpression());
            }
        }
        for (Expression e : tuple.getExpressions()) {
            String mapKey = e.getNodeMetaData(MultipleAssignmentMetadata.MAP_KEY);
            Expression valueExpr = rhsByKey.get(mapKey);
            ClassNode targetType = getType(e);
            if (valueExpr != null) {
                ClassNode valueType = getType(valueExpr);
                if (!isAssignableTo(valueType, targetType)) {
                    addStaticTypeError("Cannot assign value of type " + prettyPrintType(valueType) + " to variable of type " + prettyPrintType(targetType), rightExpression);
                    return false;
                }
                storeType(e, valueType);
            } else {
                // Missing key — runtime semantics give null. Store Object as inferred type so
                // any read uses the broadest type; declared types on binders are preserved.
                storeType(e, OBJECT_TYPE);
            }
        }
        return true;
    }

    private boolean typeCheckMapStyleAgainstDeclaredType(final TupleExpression tuple, final Expression rightExpression) {
        ClassNode rhsType = Optional.ofNullable(getType(rightExpression)).orElseGet(rightExpression::getType);

        // Map<K, V> RHS: every binder resolves to V. STC won't complain about static "missing keys"
        // because Map keys aren't statically known.
        if (isOrImplements(rhsType, MAP_TYPE)) {
            ClassNode parameterized = rhsType.equals(MAP_TYPE) ? rhsType : GenericsUtils.parameterizeType(rhsType, MAP_TYPE);
            GenericsType[] gts = parameterized.getGenericsTypes();
            ClassNode valueType = (gts != null && gts.length == 2) ? getCombinedBoundType(gts[1]) : OBJECT_TYPE;
            for (Expression e : tuple.getExpressions()) {
                ClassNode targetType = getType(e);
                if (!isAssignableTo(valueType, targetType)) {
                    addStaticTypeError("Cannot assign value of type " + prettyPrintType(valueType) + " to variable of type " + prettyPrintType(targetType), rightExpression);
                    return false;
                }
                storeType(e, valueType);
            }
            return true;
        }

        // Bean / GroovyObject / arbitrary class RHS: resolve each MAP_KEY as a property on the
        // RHS's static type. STC's normal property-resolution path handles bean accessors and
        // surfaces "No such property" errors for keys that aren't statically declared.
        boolean ok = true;
        for (Expression e : tuple.getExpressions()) {
            String mapKey = e.getNodeMetaData(MultipleAssignmentMetadata.MAP_KEY);
            PropertyExpression pexp = new PropertyExpression(rightExpression, mapKey);
            pexp.setSourcePosition(e);
            visitPropertyExpression(pexp); // resolves type or records "No such property"
            ClassNode resolvedType = getType(pexp);
            ClassNode targetType = getType(e);
            if (resolvedType != null && !isAssignableTo(resolvedType, targetType)) {
                addStaticTypeError("Cannot assign value of type " + prettyPrintType(resolvedType) + " to variable of type " + prettyPrintType(targetType), rightExpression);
                ok = false;
                continue;
            }
            storeType(e, resolvedType != null ? resolvedType : OBJECT_TYPE);
        }
        return ok;
    }

    private ClassNode adjustTypeForSpreading(final ClassNode rightExpressionType, final Expression leftExpression) {
        // given "list*.foo = 100" or "map*.value = 100", then the assignment must be checked against [100], not 100
        if (leftExpression instanceof PropertyExpression && ((PropertyExpression) leftExpression).isSpreadSafe()) {
            return extension.buildListType(rightExpressionType);
        }
        return rightExpressionType;
    }

    private boolean addedReadOnlyPropertyError(final Expression expr) {
        // if expr is of READONLY_PROPERTY_RETURN type, then it means we are on a missing property
        if (expr.getNodeMetaData(READONLY_PROPERTY) == null) return false;
        String name;
        if (expr instanceof VariableExpression) {
            name = ((VariableExpression) expr).getName();
        } else {
            name = ((PropertyExpression) expr).getPropertyAsString();
        }
        addStaticTypeError("Cannot set read-only property: " + name, expr);
        return true;
    }

    private void addPrecisionErrors(final ClassNode leftRedirect, final ClassNode lhsType, final ClassNode rhsType, final Expression rightExpression) {
        if (isNumberType(leftRedirect)) {
            if (isNumberType(rhsType) && checkPossibleLossOfPrecision(leftRedirect, rhsType, rightExpression)) {
                addStaticTypeError("Possible loss of precision from " + prettyPrintType(rhsType) + " to " + prettyPrintType(lhsType), rightExpression);
            }
            return;
        }
        if (!leftRedirect.isArray()) return;
        // left type is an array, check the right component types
        ClassNode leftItemType = leftRedirect.getComponentType();
        if (rhsType.redirect().isArray()) {
            ClassNode rightItemType = rhsType.redirect().getComponentType();
            if (!checkCompatibleAssignmentTypes(leftItemType, rightItemType)) {
                addStaticTypeError("Cannot assign value of type " + prettyPrintType(rightItemType) + " into array of type " + prettyPrintType(lhsType), rightExpression);
            }
        } else if (rightExpression instanceof ListExpression) {
            for (Expression expression : ((ListExpression) rightExpression).getExpressions()) {
                ClassNode rightItemType = getType(expression);
                if (leftItemType.isArray() && expression instanceof ListExpression) { // GROOVY-8566
                    addPrecisionErrors(leftItemType.redirect(), leftItemType, rightItemType, expression);
                    addListAssignmentConstructorErrors(leftItemType.redirect(), leftItemType, rightItemType, expression, expression);
                } else if (!checkCompatibleAssignmentTypes(leftItemType, rightItemType)
                        && !(isNullConstant(expression) && !isPrimitiveType(leftItemType))) {
                    addStaticTypeError("Cannot assign value of type " + prettyPrintType(rightItemType) + " into array of type " + prettyPrintType(lhsType), rightExpression);
                }
            }
        }
    }

    private void addListAssignmentConstructorErrors(final ClassNode leftRedirect, final ClassNode leftExpressionType, final ClassNode inferredRightExpressionType, final Expression rightExpression, final Expression assignmentExpression) {
        ClassNode concreteType;
        List<Expression> expressions = ((ListExpression) rightExpression).getExpressions();
        if (implementsInterfaceOrIsSubclassOf((concreteType = ArrayList_TYPE), leftRedirect)
                || implementsInterfaceOrIsSubclassOf((concreteType = LinkedHashSet_TYPE), leftRedirect)) {
            // GROOVY-11309: (? super ArrayList or LinkedHashSet) x = [ ]
            if (expressions.isEmpty()) { // skip SBA.createList for empty
                rightExpression.putNodeMetaData(DIRECT_METHOD_CALL_TARGET,
                    concreteType.getDeclaredConstructor(Parameter.EMPTY_ARRAY));
            }
        } else if (isWildcardLeftHandSide(leftRedirect) && !isClassType(leftRedirect)) {
            // GROOVY-6802, GROOVY-6803: ([Bb]oolean or String) x = [ ]
        } else if (leftRedirect.isArray() || !leftRedirect.isAbstract()) {
            // if left type is not a list but right type is a list, then we're in the case of a groovy
            // constructor type : Dimension d = [100,200]
            // In that case, more checks can be performed
            ClassNode[] types = getArgumentTypes(args(expressions));
            MethodNode constructor = checkGroovyStyleConstructor(leftRedirect, types, assignmentExpression);
            if (constructor != null) {
                rightExpression.putNodeMetaData(DIRECT_METHOD_CALL_TARGET, constructor);
            }
        } else if (implementsInterfaceOrIsSubclassOf(inferredRightExpressionType, LIST_TYPE)
                && !implementsInterfaceOrIsSubclassOf(inferredRightExpressionType, leftRedirect)
                && !extension.handleIncompatibleAssignment(leftExpressionType, inferredRightExpressionType, assignmentExpression)) {
            addAssignmentError(leftExpressionType, inferredRightExpressionType, assignmentExpression);
        }
    }

    private void addMapAssignmentConstructorErrors(final ClassNode leftRedirect, final Expression leftExpression, final MapExpression rightExpression) {
        if (implementsInterfaceOrIsSubclassOf(LinkedHashMap_TYPE, leftRedirect)) {
            // GROOVY-11309: (? super LinkedHashMap) x = map literal
        } else if (isWildcardLeftHandSide(leftRedirect) && !isClassType(leftRedirect)) {
            // GROOVY-6802, GROOVY-6803: [Bb]oolean or String target
        } else {
            // groovy constructor shorthand: T t = [x:1, y:2]
            ClassNode[] argTypes = {getType(rightExpression)};
            checkGroovyStyleConstructor(leftRedirect, argTypes, rightExpression);
            // perform additional type checking on arguments
            checkGroovyConstructorMap(leftExpression, leftRedirect, rightExpression);
        }
    }

    private void checkTypeGenerics(final ClassNode leftExpressionType, final ClassNode rightExpressionType, final Expression rightExpression) {
        if (leftExpressionType.isUsingGenerics()
                && !missesGenericsTypes(rightExpressionType)
                && !(rightExpression instanceof ClosureExpression) // GROOVY-10277
                && !(rightExpression instanceof ConstructorCallExpression && ((ConstructorCallExpression) rightExpression).isUsingAnonymousInnerClass()) // GROOVY-12319: AIC type is the generated class, not the diamond super type
                && !isNullConstant(rightExpression) && !UNKNOWN_PARAMETER_TYPE.equals(rightExpressionType)
                && !GenericsUtils.buildWildcardType(leftExpressionType).isCompatibleWith(wrapTypeIfNecessary(rightExpressionType)))
            addStaticTypeError("Incompatible generic argument types. Cannot assign " + prettyPrintType(rightExpressionType) + " to: " + prettyPrintType(leftExpressionType), rightExpression);
    }

    private boolean hasGStringStringError(final ClassNode leftExpressionType, final ClassNode wrappedRHS, final Expression rightExpression) {
        if (isParameterizedWithString(leftExpressionType) && isParameterizedWithGStringOrGStringString(wrappedRHS)) {
            addStaticTypeError("You are trying to use a GString in place of a String in a type which explicitly declares accepting String. " +
                    "Make sure to call toString() on all GString values.", rightExpression);
            return true;
        }
        return false;
    }

    private static boolean isConstructorAbbreviation(final ClassNode leftType, final Expression rightExpression) {
        if (rightExpression instanceof ListExpression) {
            return !(ArrayList_TYPE.isDerivedFrom(leftType) || ArrayList_TYPE.implementsInterface(leftType)
                    || LinkedHashSet_TYPE.isDerivedFrom(leftType) || LinkedHashSet_TYPE.implementsInterface(leftType));
        }
        if (rightExpression instanceof MapExpression) {
            return !(LinkedHashMap_TYPE.isDerivedFrom(leftType) || LinkedHashMap_TYPE.implementsInterface(leftType));
        }
        return false;
    }

    /**
     * Checks assignment compatibility and reports any associated type errors.
     */
    protected void typeCheckAssignment(final BinaryExpression assignmentExpression, final Expression leftExpression, final ClassNode leftExpressionType, final Expression rightExpression, final ClassNode rightExpressionType) {
        if (leftExpression instanceof TupleExpression) {
            if (!typeCheckMultipleAssignmentAndContinue(leftExpression, rightExpression)) return;
        }

        // TODO: need errors for write-only too!
        if (addedReadOnlyPropertyError(leftExpression)) return;

        ClassNode rType = adjustTypeForSpreading(rightExpressionType, leftExpression);

        if (!checkCompatibleAssignmentTypes(leftExpressionType, rType, rightExpression)) {
            if (!extension.handleIncompatibleAssignment(leftExpressionType, rType, assignmentExpression)) {
                addAssignmentError(leftExpressionType, rightExpressionType, rightExpression);
            }
        } else {
            ClassNode lTypeRedirect = leftExpressionType.redirect();
            addPrecisionErrors(lTypeRedirect, leftExpressionType, rType, rightExpression);
            if (rightExpression instanceof ListExpression) {
                addListAssignmentConstructorErrors(lTypeRedirect, leftExpressionType, rightExpressionType, rightExpression, assignmentExpression);
            } else if (rightExpression instanceof MapExpression) {
                addMapAssignmentConstructorErrors(lTypeRedirect, leftExpression, (MapExpression) rightExpression);
            }
            if (!hasGStringStringError(leftExpressionType, rType, rightExpression) && !isConstructorAbbreviation(leftExpressionType, rightExpression)) {
                checkTypeGenerics(leftExpressionType, rType, rightExpression);
            }
        }
    }

    /**
     * Validates map-style constructor arguments against writable properties.
     */
    protected void checkGroovyConstructorMap(final Expression receiver, final ClassNode receiverType, final MapExpression mapExpression) {
        for (MapEntryExpression entryExpression : mapExpression.getMapEntryExpressions()) {
            Expression keyExpression = entryExpression.getKeyExpression();
            if (!(keyExpression instanceof ConstantExpression)) {
                addStaticTypeError("Dynamic keys in map-style constructors are unsupported in static type checking", keyExpression);
            } else {
                String propName = keyExpression.getText();
                Set<ClassNode> propertyTypes = new HashSet<>();
                Expression valueExpression = entryExpression.getValueExpression();
                PropertyExpression dummyExpression = propX(varX("", receiverType), propName);
                dummyExpression.setSourcePosition(keyExpression);
                typeCheckingContext.pushEnclosingBinaryExpression(assignX(keyExpression, valueExpression, entryExpression));
                if (!existsProperty(dummyExpression, /*read*/false, new PropertyLookup(receiverType, propertyTypes::add))) {
                    typeCheckingContext.popEnclosingBinaryExpression();
                    addStaticTypeError("No such property: " + propName + " for class: " + prettyPrintTypeName(receiverType), keyExpression);
                } else if (isRecord(receiverType) || !addedReadOnlyPropertyError(dummyExpression)) { // GROOVY-11956
                    SetterInfo setterInfo = removeSetterInfo(keyExpression);
                    if (setterInfo != null) { // GROOVY-11451: select setter
                        ensureValidSetter(entryExpression, keyExpression, valueExpression, setterInfo);
                    }
                    ClassNode valueType = getType(valueExpression);
                    BinaryExpression kv = typeCheckingContext.popEnclosingBinaryExpression();
                    if (propertyTypes.stream().noneMatch(targetType -> checkCompatibleAssignmentTypes(targetType, getResultType(targetType, ASSIGN, valueType, kv), valueExpression))) {
                        ClassNode targetType = propertyTypes.isEmpty() ? VOID_TYPE : newUnionTypeClassNode(propertyTypes);
                        if (!extension.handleIncompatibleAssignment(targetType, valueType, entryExpression)) {
                            addAssignmentError(targetType, valueType, entryExpression);
                        }
                    }
                }
            }
        }
    }

    private static boolean isRecord(final ClassNode node) {
        return node.isRecord() || !node.getAnnotations(RecordTypeASTTransformation.MY_TYPE).isEmpty();
    }

    /**
     * Indicates whether the inferred type still contains unresolved placeholder generics.
     */
    @Deprecated(forRemoval = true, since = "4.0.0")
    protected static boolean hasRHSIncompleteGenericTypeInfo(final ClassNode inferredRightExpressionType) {
        boolean replaceType = false;
        GenericsType[] genericsTypes = inferredRightExpressionType.getGenericsTypes();
        if (genericsTypes != null) {
            for (GenericsType genericsType : genericsTypes) {
                if (genericsType.isPlaceholder()) {
                    replaceType = true;
                    break;
                }
            }
        }
        return replaceType;
    }

    /**
     * Checks that a constructor style expression is valid regarding the number
     * of arguments and the argument types.
     *
     * @param node      the class node for which we will try to find a matching constructor
     * @param arguments the constructor arguments
     */
    protected MethodNode checkGroovyStyleConstructor(final ClassNode node, final ClassNode[] arguments, final ASTNode origin) {
        if (isObjectType(node)) {
            // we are facing a list expression assigned to a def, var, final or Object
            return null;
        }
        var constructors = findMethod(node, "<init>", node.isArray() ? new ClassNode[]{int_TYPE} : arguments);
        if (constructors.isEmpty()) {
            if (isBeingCompiled(node) && !node.isAbstract() && arguments.length == 1 && arguments[0].equals(LinkedHashMap_TYPE)) {
                // there will be a default hash map constructor added later
                return new ConstructorNode(Opcodes.ACC_PUBLIC, new Parameter[]{new Parameter(LinkedHashMap_TYPE, "args")}, ClassNode.EMPTY_ARRAY, GENERATED_EMPTY_STATEMENT);
            } else {
                addNoMatchingMethodError(node, "<init>", arguments, origin);
                return null;
            }
        } else if (constructors.size() > 1) {
            addStaticTypeError("Ambiguous constructor call " + prettyPrintTypeName(node) + toMethodParametersString("", arguments), origin);
            return null;
        }
        return constructors.get(0);
    }

    /**
     * Checks whether the property exists for the current access mode.
     */
    protected boolean existsProperty(final PropertyExpression pexp, final boolean checkForReadOnly) {
        return existsProperty(pexp, checkForReadOnly, null);
    }

    /**
     * Checks whether a property exists on the receiver, or on any of the possible receiver classes (found in the
     * temporary type information table)
     *
     * @param pexp     a property expression
     * @param readMode if true, look for property read, else for property set
     * @param visitor  if not null, when the property node is found, visit it with the provided visitor
     * @return true if the property is defined in any of the possible receiver classes
     */
    protected boolean existsProperty(final PropertyExpression pexp, final boolean readMode, final ClassCodeVisitorSupport visitor) {
        super.visitPropertyExpression(pexp);

        final String propertyName = pexp.getPropertyAsString();
        if (propertyName == null) return false;

        Expression objectExpression = pexp.getObjectExpression();
        if (objectExpression instanceof ConstructorCallExpression) {
            ClassNode rawType = getType(objectExpression).getPlainNodeReference();
            inferDiamondType((ConstructorCallExpression) objectExpression, rawType);
        }
        // enclosing excludes classes that skip STC
        Set<ClassNode> enclosingTypes = new LinkedHashSet<>();
        enclosingTypes.add(typeCheckingContext.getEnclosingClassNode());
        enclosingTypes.addAll(enclosingTypes.iterator().next().getOuterClasses());

        if (objectExpression instanceof ClassExpression) {
            if (propertyName.equals("this")) {
                // handle "Outer.this" for any level of nesting
                ClassNode outer = getType(objectExpression).getGenericsTypes()[0].getType();

                ClassNode found = null;
                for (ClassNode enclosingType : enclosingTypes) {
                    if (!enclosingType.isStaticClass() && outer.equals(enclosingType.getOuterClass())) {
                        found = enclosingType;
                        break;
                    }
                }
                if (found != null) {
                    storeType(pexp, outer);
                    return true;
                }
            } else if (propertyName.equals("super")) {
                // GROOVY-8299: handle "Iface.super" for interface default methods
                ClassNode enclosingType = typeCheckingContext.getEnclosingClassNode();
                ClassNode accessingType = getType(objectExpression).getGenericsTypes()[0].getType();
                if (accessingType.isInterface() && enclosingType.implementsInterface(accessingType)) {
                    storeType(pexp, accessingType);
                    return true;
                }
                return false;
            }
        }

        final String isserName  = getGetterName(propertyName, Boolean.TYPE);
        final String getterName = getGetterName(propertyName);
        final String setterName = getSetterName(propertyName);

        boolean foundGetterOrSetter = false;
        Set<ClassNode> handledTypes = new HashSet<>();
        List<Receiver<String>> receivers = new ArrayList<>();
        addReceivers(receivers, makeOwnerList(objectExpression), pexp.isImplicitThis());

    on: for (Receiver<String> receiver : receivers) {
            ClassNode receiverType = receiver.getType();
            if (receiverType instanceof UnionTypeClassNode ut) {
                List<ClassNode> propertyTypes = new ArrayList<>();
                Consumer<ClassNode> add = t->propertyTypes.add(wrapTypeIfNecessary(t));
                for (ClassNode type : ut.getDelegates()) { // GROOVY-8965, GROOVY-10702
                    var copy = (PropertyExpression) pexp.transformExpression(ex -> ex);
                    copy.setObjectExpression(varX("_", type));
                    if (!existsProperty(copy, readMode, new PropertyLookup(type, add)))
                        continue on;
                }
                pexp.putNodeMetaData(INFERRED_TYPE, lowestUpperBound(propertyTypes));
                pexp.putNodeMetaData(DYNAMIC_RESOLUTION, Boolean.TRUE);
                return true;
            }
            if (receiverType.isArray() && propertyName.equals("length")) {
                pexp.putNodeMetaData(READONLY_PROPERTY, Boolean.TRUE);
                storeType(pexp, int_TYPE);
                if (visitor != null) {
                    FieldNode length = new FieldNode("length", Opcodes.ACC_PUBLIC | Opcodes.ACC_FINAL, int_TYPE, receiverType, null);
                    length.setDeclaringClass(receiverType);
                    visitor.visitField(length);
                }
                return true;
            }

            // in case of a lookup on java.lang.Class, look for instance methods on Class
            // in case of static property access, Type (static) and Class<Type> are tried
            boolean staticOnly = !receiver.isObject();
            // GROOVY-11367, GROOVY-11384, GROOVY-11387: entry before a non-public member
            boolean publicOnly = !staticOnly && isOrImplements(receiverType, MAP_TYPE);

            List<MethodNode> setters = new ArrayList<>(4);
            for (MethodNode method : findMethodsWithGenerated(wrapTypeIfNecessary(receiverType), setterName)) {
                if (method.getParameters().length == 1 && (!staticOnly || method.isStatic()) && (!publicOnly || method.isPublic())
                        // GROOVY-11319:
                        && hasAccessToMember(typeCheckingContext.getEnclosingClassNode(), method.getDeclaringClass(), method.getModifiers())) {
                    setters.add(method);
                }
            }
            // GROOVY-11372:
            var loader = getSourceUnit().getClassLoader();
            var dgmSet = (TreeSet<MethodNode>) findDGMMethodsForClassNode(loader,            receiverType,  setterName);
            if (isPrimitiveType(receiverType)) findDGMMethodsForClassNode(loader, getWrapper(receiverType), setterName, dgmSet);
            for (MethodNode method : dgmSet) {
                if (method.getParameters().length == 1 && (!staticOnly || method.isStatic())) {
                    setters.add(method);
                }
            }

            var queue = new LinkedList<ClassNode>();
            queue.add(receiverType);
            if (isPrimitiveType(receiverType)) {
                queue.add(getWrapper(receiverType));
            } else if (receiverType.isInterface()) {
                queue.add(OBJECT_TYPE);//GROOVY-5585
            }
            while (!queue.isEmpty()) {
                ClassNode current = queue.remove();
                if (!handledTypes.add(current)) continue;

                FieldNode field = current.getDeclaredField(propertyName);
                if (field == null) {
                    if (current.getSuperClass() != null)
                        queue.addFirst(current.getSuperClass());
                    Collections.addAll(queue, current.getInterfaces());
                }
                else field = allowStaticAccessToMember(field, staticOnly);

                // skip property/accessor checks for "x.@field"
                if (pexp instanceof AttributeExpression) {
                    if (field != null && storeField(field, pexp, receiverType, visitor, receiver.getData(), !readMode)) {
                        return true;
                    }
                    continue;
                }

                if (field != null && publicOnly && !field.isPublic()
                        // GROOVY-11367, GROOVY-11402, GROOVY-11403:
                        && (!receiverType.equals(current) || !isThisExpression(objectExpression) || typeCheckingContext.getEnclosingClosure() != null)) {
                    field = null;
                }
                // skip property/accessor checks for "field", "this.field", "this.with { field }", etc. within the declaring class of the field
                else if (field != null && enclosingTypes.contains(current) && storeField(field, pexp, receiverType, visitor, receiver.getData(), !readMode)) {
                    return true;
                }
                // GROOVY-8283: accessible field shadows getter of super class; GROOVY-11381: inaccessible field stops the loop, so search supers for getter
                boolean checkUp = field != null && !hasAccessToMember(typeCheckingContext.getEnclosingClassNode(), field.getDeclaringClass(), field.getModifiers());

                MethodNode getter = getGetterMethod(current, isserName, checkUp);
                getter = allowStaticAccessToMember(getter, staticOnly);
                if (getter == null) {
                    getter = getGetterMethod(current, getterName, checkUp);
                    getter = allowStaticAccessToMember(getter, staticOnly);
                }
                if (getter == null && current.isRecord()) {
                    // GROOVY-12225: a precompiled record's component accessor is named for the
                    // component (e.g. "x()"), so there is no get/is getter and no PropertyNode
                    for (RecordComponentNode rcn : current.getRecordComponents()) {
                        if (rcn.getName().equals(propertyName)) {
                            getter = allowStaticAccessToMember(current.getDeclaredMethod(propertyName, Parameter.EMPTY_ARRAY), staticOnly);
                            break;
                        }
                    }
                }
                if (getter != null && ((publicOnly && (!getter.isPublic() || propertyName.equals("class") || propertyName.equals("empty")))
                        // GROOVY-11319:
                        || !hasAccessToMember(typeCheckingContext.getEnclosingClassNode(), getter.getDeclaringClass(), getter.getModifiers()))) {
                    getter = null;
                }

                if (readMode && getter != null && visitor != null) visitor.visitMethod(getter);

                PropertyNode property = current.getProperty(propertyName);
                property = allowStaticAccessToMember(property, staticOnly);
                // prefer explicit getter/setter for out-of-scope references
                if (property == null || !enclosingTypes.contains(current)) {
                    if (readMode) {
                        if (getter != null) {
                            ClassNode returnType = inferReturnTypeGenerics(receiverType, getter, ArgumentListExpression.EMPTY_ARGUMENTS);
                            storeInferredTypeForPropertyExpression(pexp, returnType);
                            storeTargetMethod(pexp, getter);
                            String delegationData = receiver.getData();
                            if (delegationData != null) {
                                pexp.putNodeMetaData(IMPLICIT_RECEIVER, delegationData);
                            }
                            return true;
                        }
                    } else {
                        if (!setters.isEmpty() && (checkUp || setters.stream().map(MethodNode::getDeclaringClass).anyMatch(current::equals))) {
                            if (visitor != null) {
                                for (MethodNode setter : setters) {
                                    // visiting setter will not infer the property type since return type is void, so visit a dummy field instead
                                    FieldNode virtual = new FieldNode(propertyName, 0, setter.getParameters()[0].getOriginType(), current, null);
                                    virtual.setDeclaringClass(setter.getDeclaringClass());
                                    visitor.visitField(virtual);
                                }
                            }
                            BinaryExpression enclosingBinaryExpression = typeCheckingContext.getEnclosingBinaryExpression();
                            if (enclosingBinaryExpression != null) {
                                putSetterInfo(enclosingBinaryExpression.getLeftExpression(), new SetterInfo(receiverType, setterName, setters));
                            }
                            String delegationData = receiver.getData();
                            if (delegationData != null) {
                                pexp.putNodeMetaData(IMPLICIT_RECEIVER, delegationData);
                            }
                            pexp.removeNodeMetaData(READONLY_PROPERTY);
                            return true;
                        } else if (getter != null && setters.isEmpty() && (field == null || field.isFinal()
                                || !isFieldAccessible(field, receiverType, pexp, receiver.getData()))) { // GROOVY-12290: inaccessible field cannot make the property writable
                            pexp.putNodeMetaData(READONLY_PROPERTY, Boolean.TRUE); // GROOVY-9127
                        }
                    }
                }

                if (property != null && storeProperty(property, pexp, receiverType, visitor, receiver.getData(), !readMode)) return true;

                if (field != null && storeField(field, pexp, receiverType, visitor, receiver.getData(), !readMode)) return true;

                foundGetterOrSetter = (foundGetterOrSetter || getter != null || !setters.isEmpty());
            }

            // GROOVY-5568, GROOVY-9115, GROOVY-9123: the property may be defined by an extension
            for (ClassNode dgmReceiver : isPrimitiveType(receiverType) ? new ClassNode[]{receiverType, getWrapper(receiverType)} : new ClassNode[]{receiverType}) {
                Set<MethodNode> methods = findDGMMethodsForClassNode(loader, dgmReceiver, getterName);
                for (MethodNode method : findDGMMethodsForClassNode(loader, dgmReceiver, isserName)) {
                    if (isPrimitiveBoolean(method.getReturnType())) methods.add(method);
                }
                if (!receiver.isObject()) {
                    // GROOVY-10820: ensure static extension when property accessed in static manner
                    methods.removeIf(method -> !((ExtensionMethodNode) method).isStaticExtension());
                }
                if (isUsingGenericsOrIsArrayUsingGenerics(dgmReceiver)) {
                    methods.removeIf(method -> // GROOVY-10075: "List<Integer>" vs "List<String>"
                        !typeCheckMethodsWithGenerics(dgmReceiver, ClassNode.EMPTY_ARRAY, method)
                    );
                }
                List<MethodNode> bestMethods = chooseBestMethod(dgmReceiver, methods, ClassNode.EMPTY_ARRAY);
                if (bestMethods.size() == 1) {
                    if (readMode) {
                        MethodNode getter = bestMethods.get(0);
                        if (visitor != null) {
                            visitor.visitMethod(getter);
                        }
                        ClassNode returnType = inferReturnTypeGenerics(dgmReceiver, getter, ArgumentListExpression.EMPTY_ARGUMENTS);
                        storeInferredTypeForPropertyExpression(pexp, returnType);
                        storeTargetMethod(pexp, getter);
                        return true;
                    } else if (setters.isEmpty()) { // GROOVY-11372
                        pexp.putNodeMetaData(READONLY_PROPERTY, Boolean.TRUE);
                    }
                }
            }

            // GROOVY-7996: check if receiver declares get(String), getProperty(String), propertyMissing(String) or $static_propertyMissing(String)
            if (pexp.isImplicitThis() && !receiverType.isArray() && !receiverType.isScriptBody() && !isPrimitiveType(getUnwrapper(receiverType))) {
                MethodNode mopMethod;
                if (readMode) {
                    Parameter[] name = {new Parameter(STRING_TYPE, "name")};
                    mopMethod = receiverType.getMethod("get", name);
                    if (mopMethod == null) mopMethod = receiverType.getMethod("getProperty", name);
                    if (mopMethod == null || mopMethod.isStatic() || mopMethod.isSynthetic()) mopMethod = receiverType.getMethod("propertyMissing", name);
                } else {
                    Parameter[] nameAndValue = {new Parameter(STRING_TYPE, "name"), new Parameter(OBJECT_TYPE, "value")};
                    mopMethod = receiverType.getMethod("set", nameAndValue);
                    if (mopMethod == null) mopMethod = receiverType.getMethod("setProperty", nameAndValue);
                    if (mopMethod == null || mopMethod.isStatic() || mopMethod.isSynthetic()) mopMethod = receiverType.getMethod("propertyMissing", nameAndValue);
                }
                if (mopMethod != null && !mopMethod.isStatic() && !mopMethod.isSynthetic()) {
                    pexp.putNodeMetaData(DYNAMIC_RESOLUTION, Boolean.TRUE);
                    pexp.removeNodeMetaData(DECLARATION_INFERRED_TYPE);
                    pexp.removeNodeMetaData(INFERRED_TYPE);
                    if (visitor != null)
                        visitor.visitMethod(mopMethod);
                    return true;
                }
            }

            // GROOVY-11401
            if (!staticOnly && isOrImplements(receiverType, MAP_TYPE)) break;
        }

        for (Receiver<String> receiver : receivers) {
            if (receiver.isObject()) {
                ClassNode receiverType = receiver.getType();
                ClassNode propertyType = getTypeForMapPropertyExpression(receiverType, pexp);
                if (propertyType == null)
                    propertyType = getTypeForListPropertyExpression(receiverType, pexp);
                if (propertyType == null)
                    propertyType = getTypeForSpreadExpression(receiverType, pexp);
                if (propertyType == null)
                    continue;
                if (visitor != null) {
                    PropertyNode node = new PropertyNode(propertyName, Opcodes.ACC_PUBLIC, propertyType, receiverType, null, null, null);
                    node.setDeclaringClass(receiverType);
                    visitor.visitProperty(node);
                }
                storeType(pexp, propertyType);
                String delegationData = receiver.getData();
                if (delegationData != null) pexp.putNodeMetaData(IMPLICIT_RECEIVER, delegationData);
                return true;
            }
        }

        if (pexp.isImplicitThis() && isThisExpression(objectExpression)) {
            Iterator<ClassNode> iter = enclosingTypes.iterator(); // first enclosing is "this" type
            boolean staticOnly = Modifier.isStatic(iter.next().getModifiers()) || typeCheckingContext.isInStaticContext;
            while (iter.hasNext()) {
                ClassNode outer = iter.next();
                // GROOVY-7994, GROOVY-11198: try "this.propertyName" as "Outer.propertyName" or "Outer.this.propertyName"
                PropertyExpression pe = propX(staticOnly ? classX(outer) : propX(classX(outer), "this"), pexp.getProperty());
                if (existsProperty(pe, readMode, visitor)) {
                    pexp.copyNodeMetaData(pe);
                    return true;
                }
                staticOnly = staticOnly || Modifier.isStatic(outer.getModifiers());
            }
        }

        return foundGetterOrSetter;
    }

    private static MethodNode getGetterMethod(final ClassNode classNode, final String getterName, final boolean searchSupers) {
        MethodNode getter = classNode.getGetterMethod(getterName, searchSupers);
        if (getter != null && (getter.getModifiers() & Opcodes.ACC_BRIDGE) != 0) getter = null; // GROOVY-11341
        return getter;
    }

    private static boolean hasAccessToMember(final ClassNode accessor, final ClassNode receiver, final int modifiers) {
        if (Modifier.isPublic(modifiers)
                || accessor.equals(receiver)
                || accessor.getOuterClasses().contains(receiver)) {
            return true;
        }
        if (!Modifier.isPrivate(modifiers) && inSamePackage(accessor, receiver)) {
            return true;
        }
        if (Modifier.isProtected(modifiers) && accessor.isDerivedFrom(receiver)) {
            return true;
        }
        return false;
    }

    /**
     * Determines if the given type itself may be referenced from the accessor
     * (JLS 6.6.1): a member of an inaccessible type is out of reach no matter
     * what the member's own modifiers say, because resolving any reference to
     * the type fails first. A nested type is accessible when its enclosing
     * type is and its own modifiers admit the accessor.
     */
    private static boolean hasAccessToClass(final ClassNode accessor, final ClassNode type) {
        ClassNode outer = type.getOuterClass();
        if (outer != null && !hasAccessToClass(accessor, outer)) {
            return false;
        }
        return hasAccessToMember(accessor, outer != null ? outer : type, type.getModifiers());
    }

    private ClassNode getTypeForMultiValueExpression(final ClassNode compositeType, final Expression prop) {
        GenericsType[] gts = compositeType.getGenericsTypes();
        ClassNode itemType = (gts != null && gts.length == 1 ? getCombinedBoundType(gts[0]) : OBJECT_TYPE);

        List<ClassNode> propertyTypes = new ArrayList<>();
        if (existsProperty(propX(varX("_", itemType), prop), true, new PropertyLookup(itemType, propertyTypes::add))) {
            return extension.buildListType(propertyTypes.get(0));
        }
        return null;
    }

    private ClassNode getTypeForSpreadExpression(final ClassNode testClass, final PropertyExpression pexp) {
        if (pexp.isSpreadSafe()) {
            MethodCallExpression mce = callX(varX("_", testClass), "iterator");
            mce.setImplicitThis(false);
            mce.visit(this);
            ClassNode iteratorType = getType(mce);
            if (isOrImplements(iteratorType, Iterator_TYPE)) {
                return getTypeForMultiValueExpression(iteratorType, pexp.getProperty());
            }
        }
        return null;
    }

    private ClassNode getTypeForListPropertyExpression(final ClassNode testClass, final PropertyExpression pexp) {
        if (isOrImplements(testClass, LIST_TYPE)) {
            ClassNode listType = testClass.equals(LIST_TYPE) ? testClass
                    : GenericsUtils.parameterizeType(testClass, LIST_TYPE);
            return getTypeForMultiValueExpression(listType, pexp.getProperty());
        }
        return null;
    }

    private ClassNode getTypeForMapPropertyExpression(final ClassNode testClass, final PropertyExpression pexp) {
        if (isOrImplements(testClass, MAP_TYPE)) {
            ClassNode mapType = testClass.equals(MAP_TYPE) ? testClass
                    : GenericsUtils.parameterizeType(testClass, MAP_TYPE);
            GenericsType[] gts = mapType.getGenericsTypes();//<K,V> params
            if (gts == null || gts.length != 2) gts = new GenericsType[] {
                OBJECT_TYPE.asGenericsType(), OBJECT_TYPE.asGenericsType()
            };

            if (!pexp.isSpreadSafe()) {
                if (pexp.isImplicitThis() && isThisExpression(pexp.getObjectExpression()))
                    pexp.putNodeMetaData(DYNAMIC_RESOLUTION,Boolean.TRUE); // GROOVY-11387
                return getCombinedBoundType(gts[1]);
            } else {
                // map*.property syntax acts on Entry
                switch (pexp.getPropertyAsString()) {
                  case "key":
                    pexp.putNodeMetaData(READONLY_PROPERTY,Boolean.TRUE); // GROOVY-10326
                    return GenericsUtils.makeClassSafe0(LIST_TYPE, gts[0]);
                  case "value":
                    GenericsType v = gts[1];
                    if (!v.isWildcard()
                            && !Modifier.isFinal(v.getType().getModifiers())
                            && typeCheckingContext.isTargetOfEnclosingAssignment(pexp)) {
                        v = GenericsUtils.buildWildcardType(v.getType()); // GROOVY-10325
                    }
                    return GenericsUtils.makeClassSafe0(LIST_TYPE, v);
                  default:
                    addStaticTypeError("Spread operator on map only allows one of [key,value]", pexp);
                }
            }
        }
        return null;
    }

    /**
     * Filters search result to prevent access to instance members from a static
     * context.
     * <p>
     * Return null if the given member is not static, but we want to access in a
     * static way (staticOnly=true). If we want to access in a non-static way we
     * always return the member, since then access to static members and
     * non-static members is allowed.
     *
     * @return {@code member} or null
     */
    private <T> T allowStaticAccessToMember(final T member, final boolean staticOnly) {
        if (member == null || !staticOnly) return member;

        if (member instanceof List) {
            @SuppressWarnings("unchecked")
            T list = (T) ((List<MethodNode>) member).stream()
                    .map(m -> allowStaticAccessToMember(m, true))
                    .filter(Objects::nonNull).collect(Collectors.toList());
            return list;
        }

        boolean isStatic;
        if (member instanceof FieldNode) {
            isStatic = ((FieldNode) member).isStatic();
        } else if (member instanceof PropertyNode) {
            isStatic = ((PropertyNode) member).isStatic();
        } else { // assume member instanceof MethodNode
            isStatic = isStaticInContext((MethodNode) member);
        }
        return (isStatic ? member : null);
    }

    /**
     * Is the method called in a static or non-static manner?
     */
    private boolean isStaticInContext(final MethodNode method) {
        return method instanceof ExtensionMethodNode ? ((ExtensionMethodNode) method).isStaticExtension() : method.isStatic();
    }

    /**
     * Determines if the field can back the given property (or attribute)
     * expression: accessible by Java rules, or admitted by the receiver-type
     * leniency (GROOVY-7300, GROOVY-11358) that models dynamic Groovy's
     * permissive private access. Since GROOVY-12290 (private) and GROOVY-12314
     * (package-private, protected) that leniency no longer admits plain property
     * syntax to a field of a foreign nest that Java rules would reject — a
     * direct field access that static compilation cannot honour (no access
     * bridge exists). The deliberate dynamic escape hatches remain: attribute
     * access (.@), and closure bodies (GROOVY-9195) — including delegate-resolved
     * access — whose property dispatch stays dynamic-capable under static compilation.
     */
    private boolean isFieldAccessible(final FieldNode field, final ClassNode receiver, final PropertyExpression expression, final String delegationData) {
        // GROOVY-12310: when the qualifying type itself is out of reach (e.g. a public
        // enum constant of a package-private nested enum), no static emission can name
        // it -- the JVM rejects the class resolution before it looks at the member
        if (!hasAccessToClass(typeCheckingContext.getEnclosingClassNode(), receiver)) {
            return false;
        }
        boolean superField = isSuperExpression(expression.getObjectExpression());
        boolean exactReceiver = (!superField && receiver.equals(field.getDeclaringClass()) && !field.getDeclaringClass().isAbstract()); // GROOVY-7300, GROOVY-11358
        if (exactReceiver && delegationData == null
                && typeCheckingContext.getEnclosingClosure() == null
                && !(expression instanceof AttributeExpression)
                && !hasAccessToMember(typeCheckingContext.getEnclosingClassNode(), field.getDeclaringClass(), field.getModifiers()) // GROOVY-12314
                && !inSameNest(field.getDeclaringClass(), typeCheckingContext.getEnclosingClassNode())) {
            exactReceiver = false; // GROOVY-12290
        }
        return exactReceiver
                || hasAccessToMember(typeCheckingContext.getEnclosingClassNode(), field.getDeclaringClass(), field.getModifiers());
    }

    private boolean storeField(final FieldNode field, final PropertyExpression expressionToStoreOn, final ClassNode receiver, final ClassCodeVisitorSupport visitor, final String delegationData, final boolean lhsOfAssignment) {
        boolean accessible = isFieldAccessible(field, receiver, expressionToStoreOn, delegationData);
        if (!accessible) {
            if (expressionToStoreOn instanceof AttributeExpression) {
                addStaticTypeError("Cannot access field: " + field.getName() + " of class: " + prettyPrintTypeName(field.getDeclaringClass()), expressionToStoreOn.getProperty());
            } else { // GROOVY-12314: fall through to accessor, extension or map/list resolution
                // remember a rejected package-private or protected field declared by the
                // receiver's own class: if nothing else resolves the property, the error
                // reports the access violation rather than a missing property. A private
                // field stays hidden (GROOVY-12290), and an inaccessible inherited field is
                // not a member of the subclass (JLS 8.2; GROOVY-9093, GROOVY-9293).
                if (!field.isPrivate() && receiver.equals(field.getDeclaringClass())) {
                    expressionToStoreOn.putNodeMetaData("inaccessible field", field);
                }
                return false;
            }
        }

        if (visitor != null) visitor.visitField(field);
        checkOrMarkPrivateAccess(expressionToStoreOn, field, lhsOfAssignment);
        storeWithResolve(field.getOriginType(), receiver, field.getDeclaringClass(), field.isStatic(), expressionToStoreOn);
        if (delegationData != null) {
            expressionToStoreOn.putNodeMetaData(IMPLICIT_RECEIVER, delegationData);
        }
        if (field.isFinal()) {
            MethodNode enclosing = typeCheckingContext.getEnclosingMethod();
            if (enclosing == null || !enclosing.getName().endsWith("init>"))
                expressionToStoreOn.putNodeMetaData(READONLY_PROPERTY, Boolean.TRUE); // GROOVY-5450
        } else if (accessible) {
            expressionToStoreOn.removeNodeMetaData(READONLY_PROPERTY); // GROOVY-9127
        }
        return true;
    }

    private boolean storeProperty(final PropertyNode property, final PropertyExpression expression, final ClassNode receiver, final ClassCodeVisitorSupport visitor, final String delegationData, final boolean lhsOfAssignment) {
        if (visitor != null) visitor.visitProperty(property);

        ClassNode propertyType = property.getOriginType();
        storeWithResolve(propertyType, receiver, property.getDeclaringClass(), property.isStatic(), expression);

        if (delegationData != null) {
            expression.putNodeMetaData(IMPLICIT_RECEIVER, delegationData);
        }
        if (property.isFinal()) {
            expression.putNodeMetaData(READONLY_PROPERTY, Boolean.TRUE);
        } else {
            expression.removeNodeMetaData(READONLY_PROPERTY);
        }

        String methodName;
        ClassNode returnType;
        Parameter[] parameters;
        if (!lhsOfAssignment) {
            methodName = property.getGetterNameOrDefault();
            returnType = propertyType;
            parameters = Parameter.EMPTY_ARRAY;
        } else {
            methodName = property.getSetterNameOrDefault();
            returnType = VOID_TYPE;
            parameters = new Parameter[]{new Parameter(propertyType, "value")};
        }
        MethodNode accessMethod = new MethodNode(methodName, Opcodes.ACC_PUBLIC | (property.isStatic() ? Opcodes.ACC_STATIC : 0), returnType, parameters, ClassNode.EMPTY_ARRAY, null);
        accessMethod.setDeclaringClass(property.getDeclaringClass());
        accessMethod.setSynthetic(true);

        expression.putNodeMetaData(DIRECT_METHOD_CALL_TARGET, accessMethod); // GROOVY-11029
        extension.onMethodSelection(expression, accessMethod);
        return true;
    }

    private void storeWithResolve(ClassNode type, final ClassNode receiver, final ClassNode declaringClass, final boolean isStatic, final Expression expressionToStoreOn) {
        if (!isStatic && GenericsUtils.hasUnresolvedGenerics(type)) {
            type = resolveGenericsWithContext(extractPlaceHolders(receiver, declaringClass), type);
        }
        if (expressionToStoreOn instanceof PropertyExpression) {
            storeInferredTypeForPropertyExpression((PropertyExpression) expressionToStoreOn, type);
        } else {
            storeType(expressionToStoreOn, type);
        }
    }

    private ClassNode resolveGenericsWithContext(final Map<GenericsTypeName, GenericsType> resolvedPlaceholders, final ClassNode currentType) {
        Map<GenericsTypeName, GenericsType> placeholdersFromContext = extractGenericsParameterMapOfThis(typeCheckingContext);
        return resolveClassNodeGenerics(resolvedPlaceholders, placeholdersFromContext, currentType);
    }

    private void storeInferredTypeForPropertyExpression(final PropertyExpression pexp, final ClassNode type) {
        if (pexp.isSpreadSafe()) {
            storeType(pexp, extension.buildListType(type));
        } else {
            storeType(pexp, type);
        }
    }

    /** {@inheritDoc} */
    @Override
    public void visitProperty(final PropertyNode node) {
        boolean osc = typeCheckingContext.isInStaticContext;
        typeCheckingContext.pushTemporaryTypeInfo(); // GROOVY-12166: member scope
        try {
            typeCheckingContext.isInStaticContext = node.isInStaticContext();
            currentProperty = node;
            visitAnnotations(node);
            visitClassCodeContainer(node.getGetterBlock());
            visitClassCodeContainer(node.getSetterBlock());
        } finally {
            currentProperty = null;
            typeCheckingContext.popTemporaryTypeInfo();
            typeCheckingContext.isInStaticContext = osc;
        }
    }

    /** {@inheritDoc} */
    @Override
    public void visitField(final FieldNode node) {
        boolean osc = typeCheckingContext.isInStaticContext;
        typeCheckingContext.pushTemporaryTypeInfo(); // GROOVY-12166: member scope
        try {
            typeCheckingContext.isInStaticContext = node.isInStaticContext();
            currentField = node;
            visitAnnotations(node);
            visitInitialExpression(node.getInitialExpression(), new FieldExpression(node), node);
        } finally {
            currentField = null;
            typeCheckingContext.popTemporaryTypeInfo();
            typeCheckingContext.isInStaticContext = osc;
        }
    }

    private void visitInitialExpression(final Expression value, final Expression target, final ASTNode origin) {
        if (value != null) {
            ClassNode lType = target.getType();
            applyTargetType(lType, value); // GROOVY-9977

            typeCheckingContext.pushEnclosingBinaryExpression(assignX(target, value, origin));

            value.visit(this);
            ClassNode rType = getType(value);
            if (value instanceof ConstructorCallExpression) {
                inferDiamondType((ConstructorCallExpression) value, lType);
            }

            BinaryExpression dummy = typeCheckingContext.popEnclosingBinaryExpression();
            typeCheckAssignment(dummy, target, lType, value, getResultType(lType, ASSIGN, rType, dummy));
        }
    }

    /** {@inheritDoc} */
    @Override
    public void visitForLoop(final ForStatement forLoop) {
        // collect every variable expression used in the loop body
        Map<VariableExpression, ClassNode> varTypes = new HashMap<>();
        forLoop.getLoopBlock().visit(new VariableExpressionTypeMemoizer(varTypes));

        // visit body
        Map<VariableExpression, List<ClassNode>> oldTracker = pushAssignmentTracking();
        Expression collectionExpression = forLoop.getCollectionExpression();
        if (collectionExpression instanceof ClosureListExpression) {
            // for (int i=0; i<...; i++) style loop
            super.visitForLoop(forLoop);
        } else {
            visitStatement(forLoop);
            collectionExpression.visit(this);
            ClassNode collectionType = getType(collectionExpression);
            ClassNode forLoopVariableType = forLoop.getValueVariable().getType();
            ClassNode componentType;
            if (isStringType(collectionType)) {
                if (isWrapperCharacter(getWrapper(forLoopVariableType))) {
                    // we support auto-coercion for char
                    componentType = forLoopVariableType;
                } else {
                    componentType = STRING_TYPE;
                }
            } else if (isClassType(collectionType) && collectionType.getGenericsTypes() != null
                  && collectionType.getGenericsTypes()[0].getType().isDerivedFrom(Enum_Type)) {
                componentType = collectionType.getGenericsTypes()[0].getType(); // GROOVY-11597
            } else {
                componentType = inferComponentType(collectionType, null);
                if (componentType == null) {
                    componentType = OBJECT_TYPE;
                } else if (getUnwrapper(componentType).equals(forLoopVariableType)) {
                    componentType = forLoopVariableType; // prefer the primitive type
                }
            }
            if (!checkCompatibleAssignmentTypes(forLoopVariableType, componentType)) {
                addStaticTypeError("Cannot loop with element of type " + prettyPrintType(forLoopVariableType) + " with collection of type " + prettyPrintType(collectionType), forLoop);
            }
            if (!isDynamicTyped(forLoopVariableType)) { // user-supplied type
                componentType = forLoopVariableType;
            }
            typeCheckingContext.controlStructureVariables.put(forLoop.getValueVariable(), componentType);
            try {
                forLoop.getLoopBlock().visit(this);
            } finally {
                typeCheckingContext.controlStructureVariables.remove(forLoop.getValueVariable());
            }
        }
        if (isSecondPassNeededForControlStructure(varTypes, oldTracker)) {
            visitForLoop(forLoop);
        }
    }

    /**
     * Returns the inferred loop element type given a loop collection type. Used,
     * for example, to infer the element type of a {@code for (e in list)} loop.
     *
     * @param collectionType the type of the collection
     * @return the inferred component type
     * @see #inferComponentType
     */
    public static ClassNode inferLoopElementType(final ClassNode collectionType) {
        ClassNode componentType;
        if (collectionType.isArray()) { // GROOVY-11335
            componentType = collectionType.getComponentType();

        } else if (isOrImplements(collectionType, ITERABLE_TYPE)) {
            ClassNode col = GenericsUtils.parameterizeType(collectionType, ITERABLE_TYPE);
            componentType = getCombinedBoundType(col.getGenericsTypes()[0]);

        } else if (isOrImplements(collectionType, MAP_TYPE)) { // GROOVY-6240
            ClassNode col = GenericsUtils.parameterizeType(collectionType, MAP_TYPE);
            componentType = GenericsUtils.makeClassSafe0(MAP_ENTRY_TYPE, col.getGenericsTypes());

        } else if (isOrImplements(collectionType, STREAM_TYPE)) { // GROOVY-10476
            ClassNode col = GenericsUtils.parameterizeType(collectionType, STREAM_TYPE);
            componentType = getCombinedBoundType(col.getGenericsTypes()[0]);

        } else if (isOrImplements(collectionType, Iterator_TYPE)) { // GROOVY-10712
            ClassNode col = GenericsUtils.parameterizeType(collectionType, Iterator_TYPE);
            componentType = getCombinedBoundType(col.getGenericsTypes()[0]);

        } else if (isOrImplements(collectionType, ENUMERATION_TYPE)) { // GROOVY-6123
            ClassNode col = GenericsUtils.parameterizeType(collectionType, ENUMERATION_TYPE);
            componentType = getCombinedBoundType(col.getGenericsTypes()[0]);

        } else if (isStringType(collectionType)) {
            componentType = STRING_TYPE;
        } else {
            componentType = OBJECT_TYPE;
        }
        return componentType;
    }

    /**
     * Indicates whether a second pass is needed after visiting a control structure.
     */
    protected boolean isSecondPassNeededForControlStructure(final Map<VariableExpression, ClassNode> startTypes, final Map<VariableExpression, List<ClassNode>> oldTracker) {
        for (Map.Entry<VariableExpression, ClassNode> entry : popAssignmentTracking(oldTracker).entrySet()) {
            Variable key = findTargetVariable(entry.getKey());
            if (startTypes.containsKey(key)
                    && !startTypes.get(key).equals(entry.getValue())) {
                return true;
            }
        }
        return false;
    }

    /** {@inheritDoc} */
    @Override
    public void visitWhileLoop(final WhileStatement loop) {
        Map<VariableExpression, ClassNode> startTypes = new HashMap<>();
        loop.getLoopBlock().visit(new VariableExpressionTypeMemoizer(startTypes));
        Map<VariableExpression, List<ClassNode>> oldTracker = pushAssignmentTracking();

        super.visitWhileLoop(loop);

        if (isSecondPassNeededForControlStructure(startTypes, oldTracker)) { // GROOVY-11474
            visitWhileLoop(loop);
        }
    }

    /** {@inheritDoc} */
    @Override
    public void visitBitwiseNegationExpression(final BitwiseNegationExpression expression) {
        super.visitBitwiseNegationExpression(expression);
        ClassNode type = getType(expression);
        ClassNode typeRe = type.redirect();
        ClassNode resultType;
        if (isBigIntCategory(typeRe)) {
            // allow any internal number that is not a floating point one
            resultType = type;
        } else if (isStringType(typeRe) || isGStringType(typeRe)) {
            resultType = PATTERN_TYPE;
        } else if (typeRe.equals(ArrayList_TYPE)) {
            resultType = ArrayList_TYPE;
        } else if (typeRe.equals(PATTERN_TYPE)) {
            resultType = PATTERN_TYPE;
        } else {
            MethodNode mn = findMethodOrFail(expression, type, "bitwiseNegate");
            if (mn != null) {
                resultType = mn.getReturnType();
            } else {
                resultType = OBJECT_TYPE;
            }
        }
        storeType(expression, resultType);
    }

    /** {@inheritDoc} */
    @Override
    public void visitUnaryPlusExpression(final UnaryPlusExpression expression) {
        super.visitUnaryPlusExpression(expression);
        negativeOrPositiveUnary(expression, "positive");
    }

    /** {@inheritDoc} */
    @Override
    public void visitUnaryMinusExpression(final UnaryMinusExpression expression) {
        super.visitUnaryMinusExpression(expression);
        negativeOrPositiveUnary(expression, "negative");
    }

    /** {@inheritDoc} */
    @Override
    public void visitPostfixExpression(final PostfixExpression expression) {
        Expression operand = expression.getExpression();
        int operator = expression.getOperation().getType();
        visitPrefixOrPostixExpression(expression, operand, operator);
    }

    /** {@inheritDoc} */
    @Override
    public void visitPrefixExpression(final PrefixExpression expression) {
        Expression operand = expression.getExpression();
        int operator = expression.getOperation().getType();
        visitPrefixOrPostixExpression(expression, operand, operator);
    }

    private void visitPrefixOrPostixExpression(final Expression origin, final Expression operand, final int operator) {
        Optional<Token> token = TokenUtil.asAssignment(operator);
        // push "operand += 1" or "operand -= 1" onto stack for LHS checks
        token.ifPresent(value -> typeCheckingContext.pushEnclosingBinaryExpression(binX(operand, value, constX(1))));
        try {
            operand.visit(this);
            SetterInfo setterInfo = removeSetterInfo(operand);
            if (setterInfo != null) {
                BinaryExpression rewrite = typeCheckingContext.getEnclosingBinaryExpression();
                rewrite.setSourcePosition(origin);
                if (ensureValidSetter(rewrite, operand, rewrite.getRightExpression(), setterInfo)) {
                    return;
                }
            }

            ClassNode operandType = getType(operand);
            boolean isPostfix = (origin instanceof PostfixExpression);
            String name = (operator == PLUS_PLUS ? "next" : operator == MINUS_MINUS ? "previous" : null);

            if (name != null && isNumberType(operandType)) {
                if (!isPrimitiveType(operandType)) {
                    MethodNode node = findMethodOrFail(varX("_dummy_", operandType), operandType, name);
                    if (node != null) {
                        storeTargetMethod(origin, node);
                        storeType(origin, isPostfix ? operandType : getMathWideningClassNode(operandType));
                        return;
                    }
                }
                storeType(origin, operandType);
                return;
            }
            if (name != null && operandType.isDerivedFrom(Number_TYPE)) {
                // special case for numbers, improve type checking as we can expect ++ and -- to return the same type
                MethodNode node = findMethodOrFail(operand, operandType, name);
                if (node != null) {
                    storeTargetMethod(origin, node);
                    storeType(origin, getMathWideningClassNode(operandType));
                    return;
                }
            }
            if (name == null) {
                addUnsupportedPreOrPostfixExpressionError(origin);
                return;
            }

            MethodNode node = findMethodOrFail(operand, operandType, name);
            if (node != null) {
                storeTargetMethod(origin, node);
                storeType(origin, isPostfix ? operandType : inferReturnTypeGenerics(operandType, node, ArgumentListExpression.EMPTY_ARGUMENTS));
            }
        } finally {
            if (token.isPresent()) typeCheckingContext.popEnclosingBinaryExpression();
        }
    }

    private static ClassNode getMathWideningClassNode(final ClassNode type) {
        if (isPrimitiveByte(type) || isPrimitiveShort(type) || isPrimitiveInt(type)) {
            return int_TYPE;
        }
        if (isWrapperByte(type) || isWrapperShort(type) || isWrapperInteger(type)) {
            return Integer_TYPE;
        }
        if (isPrimitiveFloat(type)) {
            return double_TYPE;
        }
        if (isWrapperFloat(type)) {
            return Double_TYPE;
        }
        return type;
    }

    private void negativeOrPositiveUnary(final Expression expression, final String name) {
        ClassNode type = getType(expression);
        ClassNode typeRe = type.redirect();
        ClassNode resultType;
        if (isDoubleCategory(getUnwrapper(typeRe))) {
            resultType = type;
        } else if (typeRe.equals(ArrayList_TYPE)) {
            resultType = ArrayList_TYPE;
        } else {
            MethodNode mn = findMethodOrFail(expression, type, name);
            if (mn != null) {
                resultType = mn.getReturnType();
            } else {
                resultType = type;
            }
        }
        storeType(expression, resultType);
    }

    /** {@inheritDoc} */
    @Override
    public void visitExpressionStatement(final ExpressionStatement statement) {
        typeCheckingContext.pushTemporaryTypeInfo();
        super.visitExpressionStatement(statement);
        Map<?,List<ClassNode>> tti = typeCheckingContext.temporaryIfBranchTypeInformation.pop();
        if (!tti.isEmpty() && !typeCheckingContext.temporaryIfBranchTypeInformation.isEmpty()) {
            tti.forEach((key, tempTypes) -> {
                if (tempTypes.contains(VOID_TYPE))
                    typeCheckingContext.peekTemporaryTypeInfo(key).add(VOID_TYPE);
            });
        }
    }

    /** {@inheritDoc} */
    @Override
    public void visitReturnStatement(final ReturnStatement statement) {
        if (typeCheckingContext.getEnclosingClosure() == null) {
            MethodNode method = typeCheckingContext.getEnclosingMethod();
            if (method != null && !method.isVoidMethod() && !method.isDynamicReturnType()) {
                applyTargetType(method.getReturnType(), statement.getExpression()); // GROOVY-10660
            }
        }
        super.visitReturnStatement(statement);
        returnListener.returnStatementAdded(statement);
    }

    /**
     * Validates the current return statement against the enclosing closure or method.
     */
    protected ClassNode checkReturnType(final ReturnStatement statement) {
        Expression expression = statement.getExpression();
        var type = statement.isReturningNullOrVoid() ? VOID_TYPE : getType(expression); // GROOVY-11685

        TypeCheckingContext.EnclosingClosure enclosingClosure = typeCheckingContext.getEnclosingClosure();
        if (enclosingClosure != null) {
            if (enclosingClosure.getClosureExpression().getNodeMetaData(INFERRED_TYPE) != null) return null; // re-visit

            ClassNode inferredReturnType = getInferredReturnType(enclosingClosure.getClosureExpression());
            // GROOVY-9995: return ctor call with diamond operator
            if (expression instanceof ConstructorCallExpression) {
                inferDiamondType((ConstructorCallExpression) expression, inferredReturnType != null ? inferredReturnType : /*GROOVY-10080:*/dynamicType());
            }
            if (inferredReturnType != null
                    && !inferredReturnType.equals(type)
                    && !isObjectType(inferredReturnType)
                    && !isPrimitiveVoid(inferredReturnType)
                    && !isPrimitiveBoolean(inferredReturnType)
                    && !GenericsUtils.hasUnresolvedGenerics(inferredReturnType)) {
                if (isStringType(inferredReturnType) && isGStringOrGStringStringLUB(type)) {
                    type = STRING_TYPE; // GROOVY-9971: convert GString to String at point of return
                } else if (statement.isReturningNullOrVoid() ? !isPrimitiveType(inferredReturnType) // GROOVY-7713, GROOVY-8202
                     : GenericsUtils.buildWildcardType(wrapTypeIfNecessary(inferredReturnType)).isCompatibleWith(wrapTypeIfNecessary(type))) {
                    type = inferredReturnType; // GROOVY-8310, GROOVY-10082, GROOVY-10091, GROOVY-10128, GROOVY-10306: allow simple covariance
                } else if ((statement.isReturningNullOrVoid() || !isPrimitiveVoid(type)) && !extension.handleIncompatibleReturnType(statement, type)) {
                    // GROOVY-10277, GROOVY-11490: incompatible return statement (null for primitive or inconvertible type)
                    String value = (statement.isReturningNullOrVoid() ? "null" : "value of type " + prettyPrintType(type));
                    String which = (enclosingClosure.getClosureExpression() instanceof LambdaExpression ? "lambda" : "closure");
                    addStaticTypeError("Cannot return " + value + " for " + which + " expecting " + prettyPrintType(inferredReturnType), expression);
                }
            }
            return type;
        }

        MethodNode enclosingMethod = typeCheckingContext.getEnclosingMethod();
        if (enclosingMethod != null && !enclosingMethod.isVoidMethod() && !enclosingMethod.isDynamicReturnType()) {
            ClassNode returnType = enclosingMethod.getReturnType();
            if (!isPrimitiveVoid(getUnwrapper(type)) && !checkCompatibleAssignmentTypes(returnType, type, expression, false)) {
                if (!extension.handleIncompatibleReturnType(statement, type)) {
                    addStaticTypeError("Cannot return value of type " + prettyPrintType(type) + " for method returning " + prettyPrintType(returnType), expression);
                }
            } else if (implementsInterfaceOrIsSubclassOf(type, returnType)) {
                BinaryExpression dummy = assignX(varX("{target}", returnType), expression, statement);
                ClassNode resultType = getResultType(returnType, ASSIGN, type, dummy); // GROOVY-10295
                checkTypeGenerics(returnType, resultType, expression);
            } else { // GROOVY-11623: check array items or number bounds
                addPrecisionErrors(returnType.redirect(), returnType, type, expression);
            }
        }
        return null;
    }

    /**
     * Records a non-void return type for the current enclosing closure.
     */
    protected void addClosureReturnType(final ClassNode returnType) {
        if (returnType != null && !isPrimitiveVoid(returnType)) // GROOVY-8202
            typeCheckingContext.getEnclosingClosure().addReturnType(returnType);
    }

    @Override
    public void visitConstructorCallExpression(final ConstructorCallExpression call) {
        typeCheckingContext.pushEnclosingMethodCall(call);
        try {
            if (!extension.beforeMethodCall(call)) {
                ClassNode receiver;
                if (call.isThisCall()) {
                    receiver = makeThis();
                } else if (call.isSuperCall()) {
                    receiver = makeSuper();
                } else {
                    receiver = call.getType();
                }
                Expression arguments = call.getArguments();
                ArgumentListExpression argumentList = InvocationWriter.makeArgumentList(arguments);

                // visit functional arguments *after* method has been chosen
                visitMethodCallArguments(receiver, argumentList, false, null);
                final ClassNode[] argumentTypes = getArgumentTypes(argumentList);

                MethodNode ctor;
                if (looksLikeNamedArgConstructor(receiver, argumentTypes)
                        && findMethod(receiver, "<init>", argumentTypes).isEmpty()
                        && findMethod(receiver, "<init>", init(argumentTypes)).size() == 1) {
                    ctor = typeCheckMapConstructor(call, receiver, arguments);
                } else {
                    ctor = findMethodOrFail(call, receiver, "<init>", argumentTypes);
                    visitMethodCallArguments(receiver, argumentList, true, ctor);
                    if (ctor != null) {
                        Parameter[] parameters = ctor.getParameters();
                        GenericsType[] typeParameters = ctor.getDeclaringClass().getGenericsTypes();
                        if (typeParameters != null) { // GROOVY-10283, GROOVY-10316, GROOVY-10482, GROOVY-10624, GROOVY-10698
                            Map<GenericsTypeName, GenericsType> context = extractGenericsConnectionsFromArguments(typeParameters, parameters, argumentList, receiver.getGenericsTypes());
                            if (!context.isEmpty()) parameters = Arrays.stream(parameters).map(p -> new Parameter(applyGenericsContext(context, p.getType()), p.getName())).toArray(Parameter[]::new);
                        }
                        resolvePlaceholdersFromImplicitTypeHints(argumentTypes, argumentList, parameters);
                        typeCheckMethodsWithGenericsOrFail(receiver, argumentTypes, ctor, call);
                        checkForbiddenSpreadArgument(argumentList, parameters);
                    } else {
                        checkForbiddenSpreadArgument(argumentList);
                    }
                }
                if (ctor != null) {
                    storeTargetMethod(call, ctor);
                    if (call.isUsingGenerics()) {
                        GenericsType[] typeParameters = ctor.getGenericsTypes();
                        if (typeParameters != null && typeParameters.length != call.getGenericsTypes().length) {
                            addStaticTypeError("Incorrect number of constructor type arguments; required " + typeParameters.length, call);
                        }
                    }
                }
            }

            // GROOVY-9327: check for AIC in STC method with non-STC enclosing class
            if (call.isUsingAnonymousInnerClass()) {
                ClassNode anonType = call.getType();
                ClassNode diamondTarget = GenericsUtils.diamondTargetOfAnonymousClass(anonType);
                if (diamondTarget != null && diamondTarget.getGenericsTypes() != null && diamondTarget.getGenericsTypes().length == 0) {
                    BinaryExpression enclosingBinaryExpression = typeCheckingContext.getEnclosingBinaryExpression();
                    ClassNode lType = null;
                    if (enclosingBinaryExpression != null && isAssignment(enclosingBinaryExpression.getOperation().getType())) {
                        lType = getType(enclosingBinaryExpression.getLeftExpression());
                    }
                    if (lType != null) { // assignment target type is the primary JLS 15.9.3 witness
                        inferDiamondType(call, lType);
                    }
                    if (diamondTarget.getGenericsTypes() != null && diamondTarget.getGenericsTypes().length == 0) {
                        // no target yet (or it was not enough): recover T from overrides so the AIC body is checked against a filled diamond
                        Map<String, ClassNode> spec = GenericsUtils.inferGenericsSpecFromOverrides(anonType, diamondTarget, Collections.emptyMap());
                        if (!spec.isEmpty()) {
                            GenericsUtils.applyGenericsSpec(diamondTarget, spec);
                        }
                    }
                }
                Set<MethodNode> methods = typeCheckingContext.methodsToBeVisited;
                if (!methods.isEmpty()) { // indicates specific methods have STC
                    typeCheckingContext.methodsToBeVisited = Collections.emptySet();

                    visitClass(anonType); // visit anon. inner class inline with method
                    anonType.putNodeMetaData(StaticTypeCheckingVisitor.class, Boolean.TRUE);

                    typeCheckingContext.methodsToBeVisited = methods;
                }
            }

            extension.afterMethodCall(call);
        } finally {
            typeCheckingContext.popEnclosingMethodCall();
        }
    }

    private boolean looksLikeNamedArgConstructor(final ClassNode receiver, final ClassNode[] argumentTypes) {
        if (argumentTypes.length == 1 || argumentTypes.length == 2 && argumentTypes[0].equals(receiver.getOuterClass())) {
            return isOrImplements(argumentTypes[argumentTypes.length - 1], MAP_TYPE);
        }
        return false;
    }

    /**
     * Selects and validates a synthetic map-style constructor target.
     */
    protected MethodNode typeCheckMapConstructor(final ConstructorCallExpression call, final ClassNode receiver, final Expression arguments) {
        MethodNode node = null;
        if (arguments instanceof TupleExpression texp) {
            List<Expression> expressions = texp.getExpressions();
            // should only get here with size = 2 when inner class constructor
            if (expressions.size() == 1 || expressions.size() == 2) {
                Expression expression = expressions.get(expressions.size() - 1);
                if (expression instanceof MapExpression argList) {
                    checkGroovyConstructorMap(call, receiver, argList);
                    Parameter[] params = expressions.size() == 1
                            ? new Parameter[]{new Parameter(MAP_TYPE, "map")}
                            : new Parameter[]{new Parameter(receiver.redirect().getOuterClass(), "$p$"), new Parameter(MAP_TYPE, "map")};
                    node = new ConstructorNode(Opcodes.ACC_PUBLIC, params, ClassNode.EMPTY_ARRAY, GENERATED_EMPTY_STATEMENT);
                    node.setDeclaringClass(receiver);
                    node.setSynthetic(true);
                }
            }
        }
        return node;
    }

    /**
     * Returns the inferred argument types for the supplied argument list.
     */
    protected ClassNode[] getArgumentTypes(final ArgumentListExpression argumentList) {
        return argumentList.getExpressions().stream().map(exp -> {
            if (isNullConstant(exp)) return UNKNOWN_PARAMETER_TYPE;
            ClassNode type = getType(exp);
            if (exp instanceof MethodReferenceExpression && type != null) {
                // as a call argument, a method reference (::) is coerced only to a functional
                // interface (unlike assignment to a Closure-typed variable); mark it so overload
                // selection prefers a functional-interface parameter over a Closure parameter,
                // which would otherwise win the distance tie-break and then fail to coerce
                type.putNodeMetaData(StaticTypesMarker.METHOD_REFERENCE_TYPE, Boolean.TRUE);
            }
            return type;
        }).toArray(ClassNode[]::new);
    }

    private Map<VariableExpression, ClassNode> scanVars(final ClosureExpression expr) {
        Map<VariableExpression, ClassNode> varTypes = new HashMap<>();

        GroovyCodeVisitor visitor = new VariableExpressionTypeMemoizer(varTypes, true);
        if (expr.isParameterSpecified()) {
            for (Parameter p : expr.getParameters()) {
                if (p.hasInitialExpression()) {
                    p.getInitialExpression().visit(visitor);
                }
            }
        }
        expr.getCode().visit(visitor);

        return varTypes;
    }

    /** {@inheritDoc} */
    @Override
    public void visitClosureExpression(final ClosureExpression expression) {
        Map<VariableExpression, ClassNode> varTypes = scanVars(expression);
        Map<VariableExpression, List<ClassNode>> oldTracker = pushAssignmentTracking();
        SharedVariableCollector collector = new SharedVariableCollector(getSourceUnit());
        expression.visit(collector); // collect every variable expression the closure references
        Set<VariableExpression> closureSharedVariables = collector.getClosureSharedExpressions();

        Map<VariableExpression, Map<StaticTypesMarker, Object>> variableMetadata = new HashMap<>();
        if (!closureSharedVariables.isEmpty()) {
            // GROOVY-6921: do getType in order to update closure shared variables
            // whose types are inferred thanks to closure parameter type inference
            for (VariableExpression vexp : closureSharedVariables) getType(vexp);
            saveVariableExpressionMetadata(closureSharedVariables, variableMetadata);
        }

        // perform visit
        typeCheckingContext.pushEnclosingClosureExpression(expression);
        DelegationMetadata dmd = getDelegationMetadata(expression);
        if (dmd != null) {
            typeCheckingContext.delegationMetadata = newDelegationMetadata(dmd.getType(), dmd.getStrategy());
        } else {
            typeCheckingContext.delegationMetadata = newDelegationMetadata(typeCheckingContext.getEnclosingClassNode(), Closure.OWNER_FIRST);
        }
        expression.getCode().visit(this);
        typeCheckingContext.delegationMetadata = typeCheckingContext.delegationMetadata.getParent();

        returnAdder.visitMethod(new MethodNode("dummy", 0, OBJECT_TYPE, Parameter.EMPTY_ARRAY, ClassNode.EMPTY_ARRAY, expression.getCode()));
        TypeCheckingContext.EnclosingClosure enclosingClosure = typeCheckingContext.popEnclosingClosure();
        if (!enclosingClosure.getReturnTypes().isEmpty()) { // populated by ReturnAdder
            ClassNode returnType = lowestUpperBound(enclosingClosure.getReturnTypes());
            storeInferredReturnType(expression, wrapTypeIfNecessary(returnType));
            storeType(expression, wrapClosureType(returnType));
        }

        // check types of closure shared variables for change
        if (isSecondPassNeededForControlStructure(varTypes, oldTracker)) {
            visitClosureExpression(expression);
        }

        // restore original metadata
        restoreVariableExpressionMetadata(variableMetadata);
        for (Parameter parameter : getParametersSafe(expression)) {
            typeCheckingContext.controlStructureVariables.remove(parameter);
            // GROOVY-10072: visit param default argument expression if present
            visitInitialExpression(parameter.getInitialExpression(), varX(parameter), parameter);
        }
    }

    /** {@inheritDoc} */
    @Override
    public void visitMethodPointerExpression(final MethodPointerExpression expression) {
        super.visitMethodPointerExpression(expression);
        Expression nameExpr = expression.getMethodName();
        if (nameExpr instanceof ConstantExpression
                && isStringType(getType(nameExpr))) {
            String nameText = nameExpr.getText();

            if ("new".equals(nameText)) {
                ClassNode type = getType(expression.getExpression());
                if (isClassClassNodeWrappingConcreteType(type)){
                    type = type.getGenericsTypes()[0].getType();
                    storeType(expression,wrapClosureType(type));
                    // GROOVY-11385: check if create possible
                    if (type.isAbstract() && !type.isArray())
                        addStaticTypeError("Cannot instantiate the type " +
                                    prettyPrintTypeName(type), expression);
                    // GROOVY-10930: check constructor reference
                    ClassNode[] signature = expression.getNodeMetaData(CLOSURE_ARGUMENTS);
                    if (signature != null) { Expression[] mocks = Arrays.stream(signature)
                            .map(t -> typedValueExpression(t)).toArray(Expression[]::new);
                        Expression dummy = ctorX(type, args(mocks));
                        dummy.setSourcePosition(expression);
                        dummy.visit(this);
                    }
                }
                return;
            }

            List<Receiver<String>> receivers = new ArrayList<>();
            addReceivers(receivers, makeOwnerList(expression.getExpression()), false);

            ClassNode receiverType = null;
            List<MethodNode> candidates = EMPTY_METHODNODE_LIST;
            for (Receiver<String> currentReceiver : receivers) {
                receiverType = wrapTypeIfNecessary(currentReceiver.getType());

                candidates = findMethodsWithGenerated(receiverType, nameText);
                // GROOVY-10741: check for reference to a property node's method
                MethodNode generated = findPropertyMethod(receiverType, nameText);
                if (generated != null && candidates.stream().noneMatch(m -> m.getName().equals(generated.getName()))) {
                    candidates.add(generated);
                }
                candidates.addAll(findDGMMethodsForClassNode(getSourceUnit().getClassLoader(), receiverType, nameText));

                if (candidates.size() > 1) {
                    candidates = filterMethodCandidates(candidates, expression.getExpression(), expression.getNodeMetaData(CLOSURE_ARGUMENTS));
                }
                if (!candidates.isEmpty()) {
                    break;
                }
            }

            if (candidates.isEmpty()) {
                candidates = extension.handleMissingMethod(
                    getType(expression.getExpression()), nameText, null, null, null);
            } else if (candidates.size() > 1) {
                candidates = extension.handleAmbiguousMethods(candidates, expression);
            }

            if (!candidates.isEmpty()) {
                ClassNode[] arguments = expression.getNodeMetaData(CLOSURE_ARGUMENTS);
                if (asBoolean(arguments) && missesGenericsTypes(receiverType) // GROOVY-10984
                                         && expression.getExpression() instanceof ClassExpression) {
                    receiverType = GenericsUtils.parameterizeType(arguments[0], receiverType); // GROOVY-11241
                }

                ClassNode ownerType = receiverType;
                candidates.stream()
                        .peek(candidate -> checkOrMarkPrivateAccess(expression, candidate)) // GROOVY-11365
                        .map (candidate -> {
                            ClassNode returnType = candidate.getReturnType();
                            if (!isStaticInContext(candidate) // GROOVY-11683
                                    && GenericsUtils.hasUnresolvedGenerics(returnType)) {
                                Map<GenericsTypeName, GenericsType> spec = new HashMap<>(); // GROOVY-11364
                                extractGenericsConnections(spec, ownerType, candidate.getDeclaringClass());
                                returnType = applyGenericsContext(spec, returnType);
                            }
                            return returnType;
                        })
                        .reduce(WideningCategories::lowestUpperBound).ifPresent(returnType -> {
                            ClassNode closureType = wrapClosureType(returnType);
                            storeType(expression, closureType);
                            // GROOVY-10858: check method return type
                            ClassNode targetType = expression.getNodeMetaData(PARAMETER_TYPE);
                            if (!returnType.isGenericsPlaceHolder() && isSAMType(targetType)) {
                                targetType = GenericsUtils.parameterizeSAM(targetType).getV2();
                                if (!isPrimitiveVoid(targetType) && !checkCompatibleAssignmentTypes(targetType, returnType, null, false)) // TODO: ext.handleIncompatibleReturnType
                                    addStaticTypeError("Invalid return type: " + prettyPrintType(returnType) + " is not convertible to " + prettyPrintType(targetType), expression);
                            }
                        });
                expression.putNodeMetaData(MethodNode.class, candidates);

                if (asBoolean(arguments)) {
                    ClassNode[] parameters = collateMethodReferenceParameterTypes(expression, candidates.get(0));
                    for (int i = 0; i < arguments.length; i += 1) {
                        ClassNode at = arguments[i];
                        ClassNode pt = parameters[Math.min(i, parameters.length - 1)];
                        if (!pt.equals(at) && (at.isInterface() ? pt.implementsInterface(at) : pt.isDerivedFrom(at)))
                            arguments[i] = pt; // GROOVY-10734, GROOVY-10807, GROPOVY-11026: expected type is refined
                    }
                }
            } else if (!(expression instanceof MethodReferenceExpression)
                    || this.getClass() == StaticTypeCheckingVisitor.class) {
                ClassNode type = wrapTypeIfNecessary(getType(expression.getExpression()));
                if (isClassClassNodeWrappingConcreteType(type)) type = type.getGenericsTypes()[0].getType();
                addStaticTypeError("Cannot find matching method " + prettyPrintTypeName(type) + "#" + nameText + ". Please check if the declared type is correct and if the method exists.", nameExpr);
            }
        }
    }

    private static Expression typedValueExpression(final ClassNode type) {
        if (isObjectType(type) && !type.isGenericsPlaceHolder()) {
            return nullX(); // GROOVY-10971: unknown argument type
        }
        return castX(type, defaultValueX(type));
    }

    private static ClassNode wrapClosureType(final ClassNode returnType) {
        return GenericsUtils.makeClassSafe0(CLOSURE_TYPE, wrapTypeIfNecessary(returnType).asGenericsType());
    }

    private static MethodNode findPropertyMethod(final ClassNode type, final String name) {
        for (ClassNode cn = type; cn != null; cn = cn.getSuperClass()) {
            for (PropertyNode pn : cn.getProperties()) {
                if (name.equals(pn.getGetterNameOrDefault())) {
                    MethodNode node = new MethodNode(name, Opcodes.ACC_PUBLIC | (pn.isStatic() ? Opcodes.ACC_STATIC : 0), pn.getType(), Parameter.EMPTY_ARRAY, ClassNode.EMPTY_ARRAY, null);
                    node.setDeclaringClass(pn.getDeclaringClass());
                    node.setSynthetic(true);
                    return node;
                } else if (name.equals(pn.getSetterNameOrDefault()) && !pn.isFinal()) {
                    MethodNode node = new MethodNode(name, Opcodes.ACC_PUBLIC | (pn.isStatic() ? Opcodes.ACC_STATIC : 0), VOID_TYPE, new Parameter[]{new Parameter(pn.getType(), pn.getName())}, ClassNode.EMPTY_ARRAY, null);
                    node.setDeclaringClass(pn.getDeclaringClass());
                    node.setSynthetic(true);
                    return node;
                }
            }
        }
        return null;
    }

    private List<MethodNode> filterMethodCandidates(final List<MethodNode> candidates, final Expression objectOrType, /*@Nullable*/ ClassNode[] signature) {
        List<MethodNode> result = filterMethodsByVisibility(candidates, typeCheckingContext.getEnclosingClassNode());
        // assignment or parameter target type may help reduce the list
        if (result.size() > 1) {
            ClassNode type = getType(objectOrType);
            if (!isClassClassNodeWrappingConcreteType(type)) {
                result = chooseBestMethod(type, result, signature);
            } else {
                type = type.getGenericsTypes()[0].getType(); // Class<Type> --> Type
                Map<Boolean, List<MethodNode>> staticAndNonStatic = result.stream().collect(Collectors.partitioningBy(this::isStaticInContext));

                result = new ArrayList<>(result.size());
                result.addAll(chooseBestMethod(type, staticAndNonStatic.get(Boolean.TRUE), signature));
                if (result.isEmpty() && !staticAndNonStatic.get(Boolean.FALSE).isEmpty()) { // GROOVY-11009
                    if (asBoolean(signature)) signature= Arrays.copyOfRange(signature, 1, signature.length);
                    result.addAll(chooseBestMethod(type, staticAndNonStatic.get(Boolean.FALSE), signature));
                }
            }
        }
        return result;
    }

    /**
     * Returns delegation metadata attached to the supplied closure expression.
     */
    protected DelegationMetadata getDelegationMetadata(final ClosureExpression expression) {
        return expression.getNodeMetaData(DELEGATION_METADATA);
    }

    private DelegationMetadata newDelegationMetadata(final ClassNode delegateType, final int resolveStrategy) {
        return new DelegationMetadata(delegateType, resolveStrategy, typeCheckingContext.delegationMetadata);
    }

    /**
     * Restores variable-expression metadata saved before a closure visit.
     */
    protected void restoreVariableExpressionMetadata(final Map<VariableExpression, Map<StaticTypesMarker, Object>> typesBeforeVisit) {
        if (typesBeforeVisit != null) {
            typesBeforeVisit.forEach((var, map) -> {
                for (StaticTypesMarker marker : StaticTypesMarker.values()) {
                    // GROOVY-9344, GROOVY-9516: keep type
                    if (marker == INFERRED_TYPE) continue;

                    Object value = map.get(marker);
                    if (value == null) {
                        var.removeNodeMetaData(marker);
                    } else {
                        var.putNodeMetaData(marker, value);
                    }
                }
                var.removeNodeMetaData(DECLARATION_INFERRED_TYPE); // GROOVY-8946
            });
        }
    }

    /**
     * Saves variable-expression metadata that must survive closure re-visits.
     */
    protected void saveVariableExpressionMetadata(final Set<VariableExpression> closureSharedExpressions, final Map<VariableExpression, Map<StaticTypesMarker, Object>> typesBeforeVisit) {
        for (VariableExpression ve : closureSharedExpressions) {
            Variable v;
            while ((v = ve.getAccessedVariable()) != ve && v instanceof VariableExpression) {
                ve = (VariableExpression) v;
            }

            Map<StaticTypesMarker, Object> metadata = new EnumMap<>(StaticTypesMarker.class);
            for (StaticTypesMarker marker : StaticTypesMarker.values()) {
                Object value = ve.getNodeMetaData(marker);
                if (value != null) {
                    metadata.put(marker, value);
                }
            }
            typesBeforeVisit.put(ve, metadata);
        }
    }

    /** {@inheritDoc} */
    @Override
    public void visitConstructor(final ConstructorNode node) {
        if (shouldSkipMethodNode(node)) {
            return;
        }
        super.visitConstructor(node);
    }

    /** {@inheritDoc} */
    @Override
    public void visitMethod(final MethodNode node) {
        if (shouldSkipMethodNode(node)) {
            return;
        }
        if (!extension.beforeVisitMethod(node)) {
            ErrorCollector collector = node.getNodeMetaData(ERROR_COLLECTOR);
            if (collector != null) {
                typeCheckingContext.getErrorCollector().addCollectorContents(collector);
            } else {
                startMethodInference(node, typeCheckingContext.getErrorCollector());
            }
            node.removeNodeMetaData(ERROR_COLLECTOR);
        }
        extension.afterVisitMethod(node);
    }

    /**
     * Starts type inference for the supplied method using the supplied error collector.
     */
    protected void startMethodInference(final MethodNode node, final ErrorCollector collector) {
        // second, we must ensure that this method MUST be statically checked
        // for example, in a mixed mode where only some methods are statically checked
        // we must not visit a method which used dynamic dispatch.
        // We do not check for an annotation because some other AST transformations
        // may use this visitor without the annotation being explicitly set
        if ((typeCheckingContext.methodsToBeVisited.isEmpty() || typeCheckingContext.methodsToBeVisited.contains(node))
                && typeCheckingContext.alreadyVisitedMethods.add(node)) { // prevent re-visiting method (infinite loop)
            typeCheckingContext.pushErrorCollector(collector);
            boolean osc = typeCheckingContext.isInStaticContext;
            try {
                // GROOVY-7890: non-static trait method is static in helper type
                typeCheckingContext.isInStaticContext = isNonStaticHelperMethod(node) ? false : node.isStatic();

                super.visitMethod(node);
            } finally {
                typeCheckingContext.isInStaticContext = osc;
            }
            typeCheckingContext.popErrorCollector();
            node.putNodeMetaData(ERROR_COLLECTOR, collector);
        }
    }

    /** {@inheritDoc} */
    @Override
    protected void visitConstructorOrMethod(final MethodNode node, final boolean isConstructor) {
        typeCheckingContext.pushEnclosingMethod(node);
        // GROOVY-12166: statement-level instanceof narrowing (return, assert)
        // records into the enclosing temporary-type-info frame; scope it to
        // this member so narrowing keyed by a shared node (e.g. a FieldNode)
        // cannot leak into members visited later
        typeCheckingContext.pushTemporaryTypeInfo();
        final ClassNode returnType = node.getReturnType(); // GROOVY-10660: implicit return case
        if (!isConstructor && (isClosureWithType(returnType) || isFunctionalInterface(returnType))) {
            new ReturnAdder(returnStmt -> applyTargetType(returnType, returnStmt.getExpression())).visitMethod(node);
        }
        readClosureParameterAnnotation(node); // GROOVY-6603
        ClassTagSupport.validateParameters(this, node); // GROOVY-12115
        doWithTypeCheckingExtensions(node, it -> super.visitConstructorOrMethod(it, isConstructor));
        if (node.hasDefaultValue()) {
            visitDefaultParameterArguments(node.getParameters());
        }
        if (!isConstructor) {
            returnAdder.visitMethod(node); // GROOVY-7753: we cannot count these auto-generated return statements, see `typeCheckingContext.pushEnclosingReturnStatement`
        } else if (!((ConstructorNode) node).firstStatementIsSpecialConstructorCall()) {
            ClassNode superClass = node.getDeclaringClass().getSuperClass(); // GROOVY-11274: check implicit "super()"
            boolean isInnerClass = superClass.getOuterClass() != null && !Modifier.isStatic(superClass.getModifiers());
            Expression superCall = ctorSuperX(args(isInnerClass ? List.of(castX(superClass.getOuterClass(), nullX())) : List.of()));
            if (node.getCode() != null) superCall.setSourcePosition(node.getCode());
            superCall.visit(this);
        }
        typeCheckingContext.popTemporaryTypeInfo();
        typeCheckingContext.popEnclosingMethod();
    }

    private void readClosureParameterAnnotation(final MethodNode node) {
        for (Parameter parameter : node.getParameters()) {
            for (AnnotationNode annotation : parameter.getAnnotations()) {
                if (annotation.getClassNode().equals(CLOSUREPARAMS_CLASSNODE)) {
                    // GROOVY-6603: propagate closure parameter types
                    Expression value = annotation.getMember("value");
                    Expression options = annotation.getMember("options");
                    List<ClassNode[]> signatures = getSignaturesFromHint(node, value, options, annotation);
                    if (signatures.size() == 1) { // TODO: handle multiple signatures
                        parameter.putNodeMetaData(CLOSURE_ARGUMENTS, Arrays.stream(signatures.get(0)).map(t -> new Parameter(t,"")).toArray(Parameter[]::new));
                    }
                }
            }
        }
    }

    private void visitDefaultParameterArguments(final Parameter[] parameters) {
        for (Parameter parameter : parameters) {
            if (!parameter.hasInitialExpression()) continue;
            // GROOVY-10094: visit param default argument expression
            visitInitialExpression(parameter.getInitialExpression(), varX(parameter), parameter);
            // GROOVY-10104: remove direct target setting to prevent errors
            parameter.getInitialExpression().visit(new CodeVisitorSupport() {
                @Override
                public void visitMethodCallExpression(final MethodCallExpression mce) {
                    super.visitMethodCallExpression(mce);
                    mce.setMethodTarget(null);
                }
            });
        }
    }

    /** {@inheritDoc} */
    @Override
    protected void visitObjectInitializerStatements(final ClassNode node) {
        // GROOVY-5450: create fake constructor node so final field analysis can allow write within non-static initializer block(s)
        ConstructorNode init = new ConstructorNode(0, null, null, new BlockStatement(node.getObjectInitializerStatements(), null));
        typeCheckingContext.pushEnclosingMethod(init);
        typeCheckingContext.pushTemporaryTypeInfo(); // GROOVY-12166: member scope
        super.visitObjectInitializerStatements(node);
        typeCheckingContext.popTemporaryTypeInfo();
        typeCheckingContext.popEnclosingMethod();
    }

    /**
     * Adds {@link TypeChecked.TypeCheckingInfo} metadata for an inferred method return type.
     */
    protected void addTypeCheckingInfoAnnotation(final MethodNode node) {
        // TypeChecked$TypeCheckingInfo cannot be applied on constructors
        if (node.isConstructor()) return;

        // if a returned inferred type is available and no @TypeCheckingInfo is on node, then add an
        // annotation to the method node
        ClassNode rtype = getInferredReturnType(node);
        if (rtype != null && node.getAnnotations(TYPECHECKING_INFO_NODE).isEmpty()) {
            AnnotationNode anno = new AnnotationNode(TYPECHECKING_INFO_NODE);
            anno.setMember("version", CURRENT_SIGNATURE_PROTOCOL);
            SignatureCodec codec = SignatureCodecFactory.getCodec(CURRENT_SIGNATURE_PROTOCOL_VERSION, getTransformLoader());
            String genericsSignature = codec.encode(rtype);
            if (genericsSignature != null) {
                ConstantExpression signature = new ConstantExpression(genericsSignature);
                signature.setType(STRING_TYPE);
                anno.setMember("inferredType", signature);
                node.addAnnotation(anno);
            }
        }
    }

    /** {@inheritDoc} */
    @Override
    public void visitStaticMethodCallExpression(final StaticMethodCallExpression call) {
        String name = call.getMethod();
        if (name == null) {
            addStaticTypeError("cannot resolve dynamic method name at compile time.", call);
            return;
        }
        ClassNode type = call.getOwnerType();
        if (type.isEnum() && name.equals("$INIT")) { // GROOVY-10845: $INIT(Object[]) delegates to constructor
            Expression target = typeCheckingContext.getEnclosingBinaryExpression().getLeftExpression();
            ConstructorCallExpression cce = new ConstructorCallExpression(type, call.getArguments());
            cce.setSourcePosition(((FieldExpression) target).getField());
            visitConstructorCallExpression(cce);

            MethodNode init = type.getDeclaredMethods("$INIT").get(0);
            call.putNodeMetaData(INFERRED_TYPE, init.getReturnType());
            call.putNodeMetaData(DIRECT_METHOD_CALL_TARGET, init);
            return;
        }
        if (extension.beforeMethodCall(call)) {
            extension.afterMethodCall(call);
            return;
        }
        try {
            ArgumentListExpression argumentList = InvocationWriter.makeArgumentList(call.getArguments());

            boolean functorsVisited = false; // visit *after* method selection
            visitMethodCallArguments(type, argumentList, functorsVisited, null);

            {
                ClassNode[] args = getArgumentTypes(argumentList);
                List<MethodNode> opts = findMethod(type, name, args);
                if (opts.isEmpty()) {
                    opts = extension.handleMissingMethod(type, name, argumentList, args, call);
                }
                if (opts.isEmpty()) {
                    addNoMatchingMethodError(type, name, args, call);
                } else {
                    opts = disambiguateMethods(opts, type, args, call);
                    if (opts.size() != 1) {
                        addAmbiguousErrorMessage(opts, name, args, call);
                    } else {
                        MethodNode targetMethod = opts.get(0);
                        storeTargetMethod(call, targetMethod); // trigger onMethodSelection
                        if (call.getNodeMetaData(DIRECT_METHOD_CALL_TARGET) == targetMethod) {
                            // GROOVY-8909, GROOVY-8961, GROOVY-9734, GROOVY-9844, GROOVY-9915, et al.
                            resolvePlaceholdersFromImplicitTypeHints(args, argumentList, targetMethod.getParameters());
                            typeCheckMethodsWithGenericsOrFail(type, args, targetMethod, call);

                            ClassNode returnType = getType(targetMethod);
                            if (isUsingGenericsOrIsArrayUsingGenerics(returnType)) {
                                functorsVisited = true; // visit functional argument(s) with selected method
                                visitMethodCallArguments(type, argumentList, functorsVisited, targetMethod);
                                ClassNode rt = inferReturnTypeGenerics(type, targetMethod, argumentList);
                                if (rt != null && implementsInterfaceOrIsSubclassOf(rt, returnType)) {
                                    returnType = rt;
                                }
                            }
                            storeType(call, returnType);
                        }
                    }
                }
            }

            // type-checking extension may have changed or cleared target method
            MethodNode target = call.getNodeMetaData(DIRECT_METHOD_CALL_TARGET);
            if (!functorsVisited) {
                visitMethodCallArguments(type, argumentList, true, target);
            }
            if (target != null) { Parameter[] params = target.getParameters();
                checkClosureMetadata(argumentList.getExpressions(), params);
                checkForbiddenSpreadArgument(argumentList, params);
            } else {
                checkForbiddenSpreadArgument(argumentList);
            }
        } finally {
            extension.afterMethodCall(call);
        }
    }

    /**
     * @deprecated this method is unused, replaced with {@link DelegatesTo} inference.
     */
    @Deprecated(forRemoval = true, since = "2.5.0")
    protected void checkClosureParameters(final Expression callArguments, final ClassNode receiver) {
        if (callArguments instanceof ArgumentListExpression argList) {
            ClosureExpression closure = (ClosureExpression) argList.getExpression(0);
            Parameter[] parameters = closure.getParameters();
            if (parameters.length > 1) {
                addStaticTypeError("Unexpected number of parameters for a with call", argList);
            } else if (parameters.length == 1) {
                Parameter param = parameters[0];
                if (!param.isDynamicTyped() && !isAssignableTo(receiver, param.getType().redirect())) {
                    addStaticTypeError("Expected parameter type: " + prettyPrintType(receiver) + " but was: " + prettyPrintType(param.getType()), param);
                }
            }
            closure.putNodeMetaData(DELEGATION_METADATA, newDelegationMetadata(receiver, Closure.DELEGATE_FIRST));
        }
    }

    /**
     * Visits a method call target, to infer the type. Don't report errors right
     * away, that will be done by a later visitMethod call.
     */
    protected void silentlyVisitMethodNode(final MethodNode directMethodCallCandidate) {
        // visit is authorized because the classnode belongs to the same source unit
        ErrorCollector collector = new ErrorCollector(typeCheckingContext.getErrorCollector().getConfiguration());
        startMethodInference(directMethodCallCandidate, collector);
    }

    /**
     * Visits method-call arguments and defers functional arguments until a target is known.
     */
    protected void visitMethodCallArguments(final ClassNode receiver, final ArgumentListExpression arguments, final boolean visitFunctors, final MethodNode selectedMethod) {
        Parameter[] parameters;
        List<Expression> expressions = new ArrayList<>();
        if (selectedMethod instanceof ExtensionMethodNode) {
            parameters = ((ExtensionMethodNode) selectedMethod).getExtensionMethodNode().getParameters();
            expressions.add(varX("$self", receiver));
        } else {
            parameters = selectedMethod != null ? selectedMethod.getParameters() : Parameter.EMPTY_ARRAY;
        }
        expressions.addAll(arguments.getExpressions());

        int nExpressions = expressions.size();
        int nthParameter = parameters.length - 1;
        for (int i = 0; i < nExpressions; i += 1) {
            Expression expression = expressions.get(i);
            if (visitFunctors == (expression instanceof ClosureExpression || expression instanceof MethodPointerExpression)) {
                if (visitFunctors && nthParameter != -1) { // GROOVY-10636: vargs call
                    Parameter target = parameters[Math.min(i, nthParameter)];
                    ClassNode targetType = target.getType();
                    if (targetType.isArray() && i >= nthParameter)
                        targetType = targetType.getComponentType();
                    boolean coerceToMethod = isSAMType(targetType);
                    if (coerceToMethod) { // resolve the target parameter's type
                        Map<GenericsTypeName, GenericsType> context = extractPlaceHoldersVisibleToDeclaration(receiver, selectedMethod, arguments);
                        targetType = applyGenericsContext(context, targetType);
                        expression.putNodeMetaData(PARAMETER_TYPE, targetType);
                    }
                    if (expression instanceof ClosureExpression) {
                        checkClosureWithDelegatesTo(receiver, selectedMethod, args(expressions), parameters, expression, target);
                        if (i > 0 || !(selectedMethod instanceof ExtensionMethodNode)) {
                            inferClosureParameterTypes(receiver, arguments, (ClosureExpression) expression, target, selectedMethod);
                        }
                        if (coerceToMethod && targetType.isInterface()) { // @FunctionalInterface
                            storeInferredReturnType(expression, GenericsUtils.parameterizeSAM(targetType).getV2());
                        } else if (isClosureWithType(targetType)) {
                            storeInferredReturnType(expression, getCombinedBoundType(targetType.getGenericsTypes()[0]));
                        }
                    } else if (expression instanceof MethodReferenceExpression) {
                        if (coerceToMethod && targetType.isInterface()) { // @FunctionalInterface
                            LambdaExpression lambda = constructLambdaExpressionForMethodReference(
                                                        targetType, (MethodReferenceExpression) expression);
                            inferClosureParameterTypes(receiver, arguments, lambda, target, selectedMethod);
                            expression.putNodeMetaData(PARAMETER_TYPE, lambda.getNodeMetaData(PARAMETER_TYPE));
                            expression.putNodeMetaData(CLOSURE_ARGUMENTS, lambda.getNodeMetaData(CLOSURE_ARGUMENTS));
                        } else {
                            addStaticTypeError("Argument is a method reference, but parameter type '" + prettyPrintTypeName(targetType) + "' is not a functional interface", expression);
                        }
                    }
                }
                expression.visit(this);
                expression.removeNodeMetaData(DELEGATION_METADATA);
            }
            if (i == 0 && parameters.length > 0 && expression instanceof MapExpression) {
                checkNamedParamsAnnotation(parameters[0], (MapExpression) expression);
            }
        }
    }

    private static LambdaExpression constructLambdaExpressionForMethodReference(final ClassNode functionalInterface, final MethodReferenceExpression methodReference) {
        Parameter[] parameters = findSAM(functionalInterface).getParameters();
        int nParameters = parameters.length;
        if (nParameters > 0) {
            parameters = new Parameter[nParameters];
            for (int i = 0; i < nParameters; i += 1)
                parameters[i] = new Parameter(dynamicType(), "p" + i);
        }
        return new LambdaExpression(parameters, GENERATED_EMPTY_STATEMENT);
    }

    private void checkNamedParamsAnnotation(final Parameter param, final MapExpression args) {
        if (!isOrImplements(param.getType(), MAP_TYPE)) return;
        List<MapEntryExpression> entryExpressions = args.getMapEntryExpressions();
        Map<Object, MapEntryExpression> entries = new LinkedHashMap<>();
        for (MapEntryExpression entry : entryExpressions) {
            Object key = entry.getKeyExpression();
            if (key instanceof ConstantExpression) {
                key = ((ConstantExpression) key).getValue();
            }
            entries.put(key, entry);
        }
        List<String> collectedNames = new ArrayList<>();
        List<AnnotationNode> annotations = param.getAnnotations(NAMED_PARAMS_CLASSNODE);
        if (annotations != null && !annotations.isEmpty()) {
            AnnotationNode an = null;
            for (AnnotationNode next : annotations) {
                if (next.getClassNode().getName().equals(NamedParams.class.getName())) {
                    an = next;
                }
            }
            if (an != null) {
                Expression value = an.getMember("value");
                if (value instanceof AnnotationConstantExpression) {
                    processNamedParam((AnnotationConstantExpression) value, entries, args, collectedNames);
                } else if (value instanceof ListExpression le) {
                    for (Expression next : le.getExpressions()) {
                        if (next instanceof AnnotationConstantExpression) {
                            processNamedParam((AnnotationConstantExpression) next, entries, args, collectedNames);
                        }
                    }
                }
            }
        }
        annotations = param.getAnnotations(NAMED_PARAM_CLASSNODE);
        if (annotations != null && !annotations.isEmpty()) {
            for (AnnotationNode next : annotations) {
                if (next.getClassNode().getName().equals(NamedParam.class.getName())) {
                    processNamedParam(next, entries, args, collectedNames);
                }
            }
        }
        if (!collectedNames.isEmpty()) {
            for (Map.Entry<Object, MapEntryExpression> entry : entries.entrySet()) {
                Object name = entry.getKey();
                if (!collectedNames.contains(name)) {
                    addStaticTypeError("unexpected named arg: " + name, entry.getValue());
                }
            }
        }
    }

    private void processNamedParam(final AnnotationConstantExpression value, final Map<Object, MapEntryExpression> entries, final MapExpression args, final List<String> collectedNames) {
        AnnotationNode namedParam = (AnnotationNode) value.getValue();
        if (!namedParam.getClassNode().getName().equals(NamedParam.class.getName())) return;
        processNamedParam(namedParam, entries, args, collectedNames);
    }

    private void processNamedParam(final AnnotationNode namedParam, final Map<Object, MapEntryExpression> entries, final MapExpression args, final List<String> collectedNames) {
        String name = null;
        boolean required = false;
        ClassNode expectedType = null;
        ConstantExpression constX = (ConstantExpression) namedParam.getMember("value");
        if (constX != null) {
            name = (String) constX.getValue();
            collectedNames.add(name);
        }
        constX = (ConstantExpression) namedParam.getMember("required");
        if (constX != null) {
            required = (Boolean) constX.getValue();
        }
        ClassExpression typeX = (ClassExpression) namedParam.getMember("type");
        if (typeX != null) {
            expectedType = typeX.getType();
        }
        if (!entries.containsKey(name)) {
            if (required) {
                addStaticTypeError("required named param '" + name + "' not found.", args);
            }
        } else if (expectedType != null) {
            MapEntryExpression entry = entries.get(name);
            ClassNode argumentType = getDeclaredOrInferredType(entry.getValueExpression());
            if (!isAssignableTo(argumentType, expectedType)) {
                addStaticTypeError("argument for named param '" + name + "' has type '" + prettyPrintType(argumentType) + "' but expected '" + prettyPrintType(expectedType) + "'.", entry);
            }
        }
    }

    /**
     * Performs type inference on closure argument types whenever code like this
     * is found: <code>foo.collect { it.toUpperCase() }</code>.
     * <p>
     * In this case the type checker tries to find if the {@code collect} method
     * has its {@link Closure} argument annotated with {@link ClosureParams}. If
     * so, then additional type inference can be performed and the type of
     * {@code it} may be inferred.
     *
     * @param receiver
     * @param arguments
     * @param expression closure or lambda expression for which the argument types should be inferred
     * @param target     parameter which may provide {@link ClosureParams} annotation or SAM type
     * @param method     method that declares {@code target}
     */
    protected void inferClosureParameterTypes(final ClassNode receiver, final Expression arguments, final ClosureExpression expression, final Parameter target, final MethodNode method) {
        List<AnnotationNode> annotations = target.getAnnotations(CLOSUREPARAMS_CLASSNODE);
        if (annotations != null && !annotations.isEmpty()) {
            for (AnnotationNode annotation : annotations) {
                Expression value = annotation.getMember("value");
                Expression options = annotation.getMember("options");
                Expression conflictResolver = annotation.getMember("conflictResolutionStrategy");
                processClosureParams(receiver, arguments, expression, method, value, conflictResolver, options);
            }
        } else if (isSAMType(target.getOriginType())) { // SAM-type coercion
            boolean isConstructor = method.isConstructor();
            boolean hasTypeArguments = asBoolean(receiver.getGenericsTypes());
            Map<GenericsTypeName, GenericsType> context = isConstructor && !hasTypeArguments ? new HashMap<>() // GROOVY-10699
                                                        : extractPlaceHoldersVisibleToDeclaration(receiver, method, arguments);
            GenericsType[] typeParameters = isConstructor ? method.getDeclaringClass().getGenericsTypes() : applyGenericsContext(context, method.getGenericsTypes());

            if (typeParameters != null) {
                boolean typeParametersResolved = false;
                // first check for explicit type arguments
                if (isConstructor) {
                    Expression emc = typeCheckingContext.getEnclosingMethodCall();
                    if (emc instanceof ConstructorCallExpression cce) {
                        GenericsType[] typeArguments = cce.getGenericsTypes();
                        if (typeArguments != null) {
                            GenericsType[] methodTypeParameters = method.getGenericsTypes();
                            if (methodTypeParameters != null && methodTypeParameters.length == typeArguments.length) {
                                typeParametersResolved = true;
                                for (int i = 0; i < methodTypeParameters.length; i += 1) {
                                    context.put(new GenericsTypeName(methodTypeParameters[i].getName()), typeArguments[i]);
                                }
                            }
                        }
                    }
                    if (!typeParametersResolved) {
                        typeParametersResolved = hasTypeArguments;
                    }
                } else {
                    Expression emc = typeCheckingContext.getEnclosingMethodCall();
                    if (emc instanceof MethodCallExpression mce) {
                        if (mce.getArguments() == arguments) {
                            GenericsType[] typeArguments = mce.getGenericsTypes();
                            if (typeArguments != null) {
                                int n = typeParameters.length;
                                if (n == typeArguments.length) {
                                    typeParametersResolved = true;
                                    for (int i = 0; i < n; i += 1) {
                                        context.put(new GenericsTypeName(typeParameters[i].getName()), typeArguments[i]);
                                    }
                                }
                            }
                        }
                    }
                }
                if (!typeParametersResolved) {
                    // check for implicit type arguments
                    int i = -1; Parameter[] p = method.getParameters();
                    for (Expression argument : (ArgumentListExpression) arguments) { i += 1;
                        if (isNullConstant(argument)) continue;

                        ClassNode pType = p[Math.min(i, p.length - 1)].getType();
                        Map<GenericsTypeName, GenericsType> gc = new HashMap<>();
                        extractGenericsConnections(gc, wrapTypeIfNecessary(getType(argument)), pType);
                        // GROOVY-10436: extract generics connections from closure parameter declaration(s)
                        if (argument == expression || (argument instanceof ClosureExpression && isSAMType(pType))) {
                            Parameter[] q = getParametersSafe((ClosureExpression) argument);
                            ClassNode[] r = extractTypesFromParameters(q); // maybe typed
                            ClassNode[] s = GenericsUtils.parameterizeSAM(pType).getV1();
                            for (int j = 0; j < r.length && j < s.length; j += 1)
                                if (!q[j].isDynamicTyped()) extractGenericsConnections(gc, r[j], s[j]);
                        }

                        gc.forEach((key, gt) -> {
                            for (GenericsType tp : typeParameters) {
                                if (tp.getName().equals(key.getName())) {
                                    context.putIfAbsent(key, gt); // TODO: merge
                                    break;
                                }
                            }
                        });
                    }

                    for (GenericsType tp : typeParameters) {
                        GenericsTypeName name = new GenericsTypeName(tp.getName());
                        context.computeIfAbsent(name, x -> fullyResolve(tp, context));
                    }
                }
            }

            ClassNode[] paramTypes = expression.getNodeMetaData(CLOSURE_ARGUMENTS);
            if (paramTypes == null) {
                ClassNode targetType = target.getType();
                if (targetType != null && targetType.isGenericsPlaceHolder())
                    targetType = getCombinedBoundType(targetType.asGenericsType());
                targetType = applyGenericsContext(context, targetType); // fill "T"
                inferParameterAndReturnTypesOfClosureOnRHS(targetType, expression);
            }
        }
    }

    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[] parameters = hasImplicitParameter(expression) ? new Parameter[]{new Parameter(dynamicType(),"it")} : getParametersSafe(expression);

        List<ClassNode[]> closureSignatures = new LinkedList<>(getSignaturesFromHint(selectedMethod, hintClass, options, expression));
        List<ClassNode[]> candidates = new LinkedList<>();
        for (var it = closureSignatures.listIterator(); it.hasNext();) { ClassNode[] signature = it.next();
            resolveGenericsFromTypeHint(receiver, arguments, selectedMethod, signature);
            if (parameters.length == signature.length) {
                candidates.add(signature);
            }
            if ((parameters.length != 1 || !isObjectType(parameters[0].getOriginType()))
                    && signature.length == 1 && isOrImplements(signature[0], LIST_TYPE)) {
                // list element(s) spread across closure parameter(s) : ClosureMetaClass
                int itemCount = TUPLE_TYPES.indexOf(signature[0]);
                if (itemCount >= 0) { // GROOVY-11090: Tuple[0-16]
                    if (itemCount != parameters.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[parameters.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 " + parameters.length, expression);
        }

        if (candidates.size() > 1) {
            closureSignatures = new ArrayList<>(candidates);
            for (Iterator<ClassNode[]> candIt = candidates.iterator(); candIt.hasNext(); ) {
                ClassNode[] inferredTypes = candIt.next();
                for (int i = 0; i < inferredTypes.length; i += 1) {
                    ClassNode inferredType = inferredTypes[i], parameterType = parameters[i].getOriginType();
                    if (parameterType.getGenericsTypes() != null) parameterType = GenericsUtils.nonGeneric(parameterType);
                    if (!typeCheckMethodArgumentWithGenerics(parameterType, inferredType, false)) { candIt.remove(); break; }
                }
            }
            if (candidates.size() > 1 && resolverClass instanceof ClassExpression) {
                candidates = resolveWithResolver(candidates, receiver, arguments, expression, selectedMethod, resolverClass, options);
            }
            if (candidates.isEmpty()) {
                String actual = toMethodParametersString("", extractTypesFromParameters(parameters));
                String expect = closureSignatures.stream().map(sig -> toMethodParametersString("",sig)).collect(Collectors.joining(" or "));
                addStaticTypeError("Incorrect parameter type(s). Expected " + expect + " but found " + actual, expression); // at least one fails
            } else if (candidates.size() > 1) {
                addError("Ambiguous prototypes for closure. More than one target method matches. Please use explicit argument types.", expression);
            }
        }

        if (candidates.size() == 1) {
            ClassNode[] inferredTypes = candidates.get(0);
            if (inferredTypes.length == 1 && hasImplicitParameter(expression)) {
                expression.putNodeMetaData(CLOSURE_ARGUMENTS, inferredTypes);
            } else {
                for (int i = 0; i < inferredTypes.length; i += 1) {
                    if (isDynamicTyped(inferredTypes[i])) continue; // GROOVY-6939
                    checkParamType(parameters[i], inferredTypes[i], false, false);
                    typeCheckingContext.controlStructureVariables.put(parameters[i], inferredTypes[i]);
                }
            }
        }
    }

    private List<ClassNode[]> getSignaturesFromHint(final MethodNode method, final Expression hintType, final Expression options, final ASTNode usage) {
        String hintTypeName = hintType.getText();
        try {
            @SuppressWarnings("unchecked")
            // load hint class using compiler's ClassLoader
            Class<? extends ClosureSignatureHint> hintClass = (Class<? extends ClosureSignatureHint>) getTransformLoader().loadClass(hintTypeName);
            ClosureSignatureHint hint = hintClass.getDeclaredConstructor().newInstance();
            return hint.getClosureSignatures(
                    method instanceof ExtensionMethodNode ? ((ExtensionMethodNode) method).getExtensionMethodNode() : method,
                    typeCheckingContext.getSource(),
                    typeCheckingContext.getCompilationUnit(),
                    convertToStringArray(options),
                    usage);
        } catch (ReflectiveOperationException e) {
            throw new GroovyBugError(e);
        }
    }

    private List<ClassNode[]> resolveWithResolver(final List<ClassNode[]> candidates, final ClassNode receiver, final Expression arguments, final ClosureExpression expression, final MethodNode method, final Expression resolverType, final Expression options) {
        String resolverTypeName = resolverType.getText();
        try {
            @SuppressWarnings("unchecked")
            // load conflict resolver class using compiler's ClassLoader
            Class<? extends ClosureSignatureConflictResolver> resolverClass = (Class<? extends ClosureSignatureConflictResolver>) getTransformLoader().loadClass(resolverTypeName);
            ClosureSignatureConflictResolver resolver = resolverClass.getDeclaredConstructor().newInstance();
            return resolver.resolve(
                    candidates,
                    receiver,
                    arguments,
                    expression,
                    method instanceof ExtensionMethodNode ? ((ExtensionMethodNode) method).getExtensionMethodNode() : method,
                    typeCheckingContext.getSource(),
                    typeCheckingContext.getCompilationUnit(),
                    convertToStringArray(options));
        } catch (ReflectiveOperationException e) {
            throw new GroovyBugError(e);
        }
    }

    private static String[] convertToStringArray(final Expression options) {
        if (options == null) {
            return ResolveVisitor.EMPTY_STRING_ARRAY;
        }
        if (options instanceof ConstantExpression) {
            return new String[]{options.getText()};
        }
        if (options instanceof ListExpression) {
            return ((ListExpression) options).getExpressions().stream().map(Expression::getText).toArray(String[]::new);
        }
        throw new IllegalArgumentException("Unexpected options for @ClosureParams:" + options);
    }

    private ClassLoader getTransformLoader() {
        return Optional.ofNullable(typeCheckingContext.getCompilationUnit()).map(CompilationUnit::getTransformLoader).orElseGet(() -> getSourceUnit().getClassLoader());
    }

    /**
     * Computes the inferred types of the closure parameters using the following trick:
     * <ol>
     * <li> creates a dummy MethodNode for which the return type is a class node
     *      for which the generic types are the types returned by the hint
     * <li> calls {@link #inferReturnTypeGenerics}
     * <li> returns inferred types from the result
     * </ol>
     * In practice it could be done differently but it has the main advantage of
     * reusing existing code, hence reducing the amount of code to debug in case
     * of failure.
     */
    private void resolveGenericsFromTypeHint(final ClassNode receiver, final Expression arguments, final MethodNode selectedMethod, final ClassNode[] signature) {
        ClassNode returnType = new ClassNode("ClForInference$" + UNIQUE_LONG.incrementAndGet(), 0, null).getPlainNodeReference();
        returnType.setGenericsTypes(Arrays.stream(signature).map(ClassNode::asGenericsType).toArray(GenericsType[]::new));

        MethodNode methodNode = selectedMethod instanceof ExtensionMethodNode ? ((ExtensionMethodNode) selectedMethod).getExtensionMethodNode() : selectedMethod;
        methodNode = new MethodNode("$$", methodNode.getModifiers(), returnType, methodNode.getParameters(), methodNode.getExceptions(), null);
        methodNode.setDeclaringClass(selectedMethod.getDeclaringClass());
        methodNode.setGenericsTypes(selectedMethod.getGenericsTypes());
        if (selectedMethod instanceof ExtensionMethodNode) {
            methodNode = new ExtensionMethodNode(
                    methodNode,
                    methodNode.getName(),
                    methodNode.getModifiers(),
                    returnType,
                    selectedMethod.getParameters(),
                    selectedMethod.getExceptions(),
                    null,
                    ((ExtensionMethodNode) selectedMethod).isStaticExtension()
            );
            methodNode.setDeclaringClass(selectedMethod.getDeclaringClass());
            methodNode.setGenericsTypes(selectedMethod.getGenericsTypes());
        }

        GenericsType[] typeArguments = null;
        Expression emc = typeCheckingContext.getEnclosingMethodCall(); // GROOVY-7789, GROOVY-11168
        if (emc instanceof MethodCallExpression call) {
            if (arguments == call.getArguments() || InvocationWriter.makeArgumentList(arguments).getExpressions().stream().anyMatch(arg ->
                    arg instanceof ClosureExpression && DefaultGroovyMethods.contains(InvocationWriter.makeArgumentList(call.getArguments()), arg)))
                typeArguments = call.getGenericsTypes();
        } else if (emc instanceof ConstructorCallExpression call) {
            if (arguments == call.getArguments() || InvocationWriter.makeArgumentList(arguments).getExpressions().stream().anyMatch(arg ->
                    arg instanceof ClosureExpression && DefaultGroovyMethods.contains(InvocationWriter.makeArgumentList(call.getArguments()), arg)))
                typeArguments = call.getGenericsTypes();
        }

        returnType = inferReturnTypeGenerics(receiver, methodNode, arguments, typeArguments);
        GenericsType[] returnTypeGenerics = returnType.getGenericsTypes();
        for (int i = 0, n = returnTypeGenerics.length; i < n; i += 1) {
            GenericsType gt = returnTypeGenerics[i];
            if (gt.isPlaceholder()) { // GROOVY-9968, GROOVY-10528, et al.
                signature[i] = gt.getUpperBounds() != null ? gt.getUpperBounds()[0] : gt.getType().redirect();
            } else {
                signature[i] = getCombinedBoundType(gt);
            }
        }
    }

    private void checkClosureWithDelegatesTo(final ClassNode receiver, final MethodNode mn, final ArgumentListExpression arguments, final Parameter[] params, final Expression expression, final Parameter param) {
        List<AnnotationNode> annotations = param.getAnnotations(DELEGATES_TO);
        if (annotations != null && !annotations.isEmpty()) {
            for (AnnotationNode annotation : annotations) {
                // in theory, there can only be one annotation of that type
                Expression value = annotation.getMember("value");
                Expression strategy = annotation.getMember("strategy");
                Expression genericTypeIndex = annotation.getMember("genericTypeIndex");
                Expression type = annotation.getMember("type");
                Integer stInt = Closure.OWNER_FIRST;
                if (strategy != null) {
                    stInt = (Integer) evaluateExpression(castX(Integer_TYPE, strategy), getSourceUnit().getConfiguration(), getSourceUnit().getClassLoader());
                }
                if (value instanceof ClassExpression && !value.getType().equals(DELEGATES_TO_TARGET)) {
                    if (genericTypeIndex != null) {
                        addStaticTypeError("Cannot use @DelegatesTo(genericTypeIndex=" + genericTypeIndex.getText()
                                + ") without @DelegatesTo.Target because generic argument types are not available at runtime", value);
                    }
                    // temporarily store the delegation strategy and the delegate type
                    expression.putNodeMetaData(DELEGATION_METADATA, newDelegationMetadata(value.getType(), stInt));
                } else if (type != null && !"".equals(type.getText()) && type instanceof ConstantExpression) {
                    String typeString = type.getText();
                    ClassNode[] resolved = GenericsUtils.parseClassNodesFromString(
                            typeString,
                            getSourceUnit(),
                            typeCheckingContext.getCompilationUnit(),
                            mn,
                            type
                    );
                    if (resolved != null) {
                        if (resolved.length == 1) {
                            resolveGenericsFromTypeHint(receiver, arguments, mn, resolved);
                            expression.putNodeMetaData(DELEGATION_METADATA, newDelegationMetadata(resolved[0], stInt));
                        } else {
                            addStaticTypeError("Incorrect type hint found in method " + mn, type);
                        }
                    }
                } else {
                    List<Expression> expressions = arguments.getExpressions();
                    int expressionsSize = expressions.size();
                    Expression parameter = annotation.getMember("target");
                    String parameterName = parameter instanceof ConstantExpression ? parameter.getText() : "";
                    // todo: handle vargs!
                    for (int j = 0, paramsLength = params.length; j < paramsLength; j++) {
                        Parameter methodParam = params[j];
                        List<AnnotationNode> targets = methodParam.getAnnotations(DELEGATES_TO_TARGET);
                        if (targets != null && targets.size() == 1) {
                            AnnotationNode targetAnnotation = targets.get(0); // @DelegatesTo.Target Obj foo
                            Expression idMember = targetAnnotation.getMember("value");
                            String id = idMember instanceof ConstantExpression ? idMember.getText() : "";
                            if (id.equals(parameterName)) {
                                if (j < expressionsSize) {
                                    Expression actualArgument = expressions.get(j);
                                    ClassNode actualType = getType(actualArgument);
                                    if (genericTypeIndex instanceof ConstantExpression) {
                                        int gti = Integer.parseInt(genericTypeIndex.getText());
                                        ClassNode paramType = methodParam.getType(); // type annotated with @DelegatesTo.Target
                                        GenericsType[] genericsTypes = paramType.getGenericsTypes();
                                        if (genericsTypes == null) {
                                            addStaticTypeError("Cannot use @DelegatesTo(genericTypeIndex=" + genericTypeIndex.getText()
                                                    + ") with a type that doesn't use generics", methodParam);
                                        } else if (gti < 0 || gti >= genericsTypes.length) {
                                            addStaticTypeError("Index of generic type @DelegatesTo(genericTypeIndex=" + genericTypeIndex.getText()
                                                    + ") " + (gti < 0 ? "lower" : "greater") + " than those of the selected type", methodParam);
                                        } else {
                                            ClassNode pType = GenericsUtils.parameterizeType(actualType, paramType);
                                            GenericsType[] pTypeGenerics = pType.getGenericsTypes();
                                            if (pTypeGenerics != null && pTypeGenerics.length > gti) {
                                                actualType = pTypeGenerics[gti].getType();
                                            } else {
                                                addStaticTypeError("Unable to map actual type [" + prettyPrintType(actualType) + "] onto " + prettyPrintType(paramType), methodParam);
                                            }
                                        }
                                    }
                                    expression.putNodeMetaData(DELEGATION_METADATA, newDelegationMetadata(actualType, stInt));
                                    break;
                                }
                            }
                        }
                    }
                    if (expression.getNodeMetaData(DELEGATION_METADATA) == null) {
                        addError("Not enough arguments found for a @DelegatesTo method call. Please check that you either use an explicit class or @DelegatesTo.Target with a correct id", annotation);
                    }
                }
            }
        }
    }

    /**
     * Adds candidate receivers derived from owner chains.
     */
    protected void addReceivers(final List<Receiver<String>> receivers, final Collection<Receiver<String>> owners, final boolean implicitThis) {
        if (!implicitThis || typeCheckingContext.delegationMetadata == null) {
            receivers.addAll(owners);
        } else {
            addReceivers(receivers, owners, typeCheckingContext.delegationMetadata, "");
        }
    }

    private static void addReceivers(final List<Receiver<String>> receivers, final Collection<Receiver<String>> owners, final DelegationMetadata dmd, final String path) {
        int strategy = dmd.getStrategy();
        switch (strategy) {
          case Closure.DELEGATE_ONLY:
          case Closure.DELEGATE_FIRST:
            addDelegateReceiver(receivers, dmd.getType(), path + "delegate");
            if (strategy == Closure.DELEGATE_FIRST) {
                if (dmd.getParent() == null) {
                    receivers.addAll(owners);
                } else {
                  //receivers.add(new Receiver<String>(CLOSURE_TYPE, path + "owner"));
                    addReceivers(receivers, owners, dmd.getParent(), path + "owner.");
                }
            }
            break;
          case Closure.OWNER_ONLY:
          case Closure.OWNER_FIRST:
            if (dmd.getParent() == null) {
                receivers.addAll(owners);
            } else {
              //receivers.add(new Receiver<String>(CLOSURE_TYPE, path + "owner"));
                addReceivers(receivers, owners, dmd.getParent(), path + "owner.");
            }
            if (strategy == Closure.OWNER_FIRST) {
                addDelegateReceiver(receivers, dmd.getType(), path + "delegate");
            }
            break;
        }
    }

    private static void addDelegateReceiver(final List<Receiver<String>> receivers, final ClassNode delegate, final String path) {
        if (isClassClassNodeWrappingConcreteType(delegate)) { // GROOVY-11393: Type from Class<Type>
            receivers.add(new Receiver<>(delegate.getGenericsTypes()[0].getType(), false, path));
        }
        if (receivers.stream().map(Receiver::getType).noneMatch(delegate::equals)) {
            receivers.add(new Receiver<>(delegate, path));
        }
    }

    /** {@inheritDoc} */
    @Override
    public void visitMethodCallExpression(final MethodCallExpression call) {
        String name = call.getMethodAsString();
        if (name == null) {
            addStaticTypeError("Cannot resolve dynamic method name at compile time", call.getMethod());
            return;
        }
        if (extension.beforeMethodCall(call)) {
            extension.afterMethodCall(call);
            return;
        }
        typeCheckingContext.pushEnclosingMethodCall(call);
        Expression objectExpression = call.getObjectExpression();

        objectExpression.visit(this);
        call.getMethod().visit(this);

        ClassNode receiver = getType(objectExpression);
        if (objectExpression instanceof ConstructorCallExpression) { // GROOVY-10228
            inferDiamondType((ConstructorCallExpression) objectExpression, receiver.getPlainNodeReference());
        }
        if (call.isSpreadSafe()) {
            ClassNode componentType = inferComponentType(receiver, null);
            if (componentType == null) {
                addStaticTypeError("Spread-dot operator can only be used on iterable types", objectExpression);
            } else {
                MethodCallExpression subcall = callX(varX("item", componentType), name, call.getArguments());
                subcall.setLineNumber(call.getLineNumber()); subcall.setColumnNumber(call.getColumnNumber());
                subcall.setImplicitThis(call.isImplicitThis());
                visitMethodCallExpression(subcall);
                // inferred type should be a list of what sub-call returns
                storeType(call, extension.buildListType(getType(subcall)));
                storeTargetMethod(call, subcall.getNodeMetaData(DIRECT_METHOD_CALL_TARGET));
            }
            typeCheckingContext.popEnclosingMethodCall();
            return;
        }

        Expression callArguments = call.getArguments();
        ArgumentListExpression argumentList = InvocationWriter.makeArgumentList(callArguments);

        // visit functional arguments *after* target method selection
        visitMethodCallArguments(receiver, argumentList, false, null);

        try {
            boolean isThisObjectExpression = isThisExpression(objectExpression);
            ClassNode[] args = getArgumentTypes(argumentList);
            boolean functorsVisited = false;
            if (!isThisObjectExpression && receiver.equals(CLOSURE_TYPE)
                    && (name.equals("call") || name.equals("doCall"))) {
                if (objectExpression instanceof VariableExpression) {
                    Variable variable = findTargetVariable((VariableExpression) objectExpression);
                    if (variable instanceof ASTNode) {
                        Parameter[] parameters = ((ASTNode) variable).getNodeMetaData(CLOSURE_ARGUMENTS);
                        if (parameters != null) {
                            checkForbiddenSpreadArgument(argumentList, parameters);
                            typeCheckClosureCall(callArguments, args, parameters);
                        }
                        ClassNode type = getType((ASTNode) variable);
                        if (type.equals(CLOSURE_TYPE)) { // GROOVY-10098, et al.
                            GenericsType[] genericsTypes = type.getGenericsTypes();
                            if (genericsTypes != null && genericsTypes.length == 1
                                    && genericsTypes[0].getLowerBound() == null) {
                                type = genericsTypes[0].getType();
                            } else {
                                type = OBJECT_TYPE;
                            }
                        }
                        if (type != null) {
                            storeType(call, type);
                        }
                    }
                } else if (objectExpression instanceof ClosureExpression) {
                    // we can get actual parameters directly
                    Parameter[] parameters = ((ClosureExpression) objectExpression).getParameters();
                    if (parameters != null) {
                        typeCheckClosureCall(callArguments, args, parameters);
                    }
                    ClassNode type = getInferredReturnType(objectExpression);
                    if (type != null) {
                        storeType(call, type);
                    }
                }

                List<Expression> list = argumentList.getExpressions();
                int nArgs = list.stream().noneMatch(e -> e instanceof SpreadExpression) ? list.size() : Integer.MAX_VALUE;
                storeTargetMethod(call, nArgs == 0 ? CLOSURE_CALL_NO_ARG : nArgs == 1 ? CLOSURE_CALL_ONE_ARG : CLOSURE_CALL_VARGS);
            } else {
                List<MethodNode> mn = null;
                Receiver<String> chosenReceiver = null;
                // GROOVY-11386:
                if (isThisObjectExpression && call.isImplicitThis()
                    && typeCheckingContext.getEnclosingClosure() != null
                    && !name.equals("call") && !name.equals("doCall")) { // GROOVY-9662
                    mn = CLOSURE_TYPE.getMethods(name);
                    if (!mn.isEmpty()) {
                        receiver = CLOSURE_TYPE.getPlainNodeReference();
                        objectExpression.removeNodeMetaData(INFERRED_TYPE);
                    }
                }
                if (mn == null || mn.isEmpty()) {
                    // method call receivers are:
                    //   - closure delegate(s) or owner(s) and trait self type(s)
                    //   - the actual receiver as found in the method call expression
                    //   - any of the potential receivers found in the instanceof temporary table
                    // in that order
                    List<Receiver<String>> receivers = new ArrayList<>();
                    addReceivers(receivers, makeOwnerList(objectExpression), call.isImplicitThis());

                    MethodNode first = null;
                    for (Receiver<String> currentReceiver : receivers) {
                        mn = findMethod(currentReceiver.getType().getPlainNodeReference(), name, args);
                        if (!mn.isEmpty()) {
                            first = mn.get(0); // capture for error reporting
                            chosenReceiver = currentReceiver;
                            // GROOVY-10819, GROOVY-10820, GROOVY-11393, et al.:
                            // for a static receiver, only static methods are compatible
                            mn = allowStaticAccessToMember(mn, !currentReceiver.isObject());
                            if (!mn.isEmpty()) {
                                break;
                            }
                        }
                    }
                    // GROOVY-12115: @ClassTag - under static checking, synthesize compiler-supplied
                    // Class<X> token argument(s) from the receiver's type argument(s) at their declared
                    // positions, then retry selection. When nothing matched this is additive (supplying
                    // an otherwise-mandatory token, e.g. for asChecked); when a token-less overload
                    // already matched, injecting *preempts* it (e.g. the lenient withDefault superseded
                    // by the key-checked one) - gated by the API author's declared intent (preempt=true),
                    // contained to the incumbent's owner, and vetoable via the configured selectors.
                    // Matching is a pure computation; the call is only rewritten once the retried
                    // selection binds a method on the matched receiver, so a failed match leaves the
                    // call - and its error reporting - exactly as written.
                    if (mn.isEmpty() || ClassTagSupport.isPreemptionPossible(this)) {
                        ClassTagSupport.ClassTagMatch tagged = mn.isEmpty()
                                ? ClassTagSupport.matchAdditive(this, receivers, name, args)
                                : ClassTagSupport.matchPreemption(this, chosenReceiver, mn, name, args);
                        if (tagged != null) {
                            ClassNode[] taggedArgs = tagged.expandArgumentTypes(args);
                            List<MethodNode> taggedMn = findMethod(tagged.receiver.getType().getPlainNodeReference(), name, taggedArgs);
                            if (!taggedMn.isEmpty()) {
                                MethodNode taggedFirst = taggedMn.get(0);
                                taggedMn = allowStaticAccessToMember(taggedMn, !tagged.receiver.isObject());
                                if (!taggedMn.isEmpty()) {
                                    tagged.rewriteArguments(argumentList); // commit the synthesised tokens
                                    args = taggedArgs;
                                    mn = taggedMn;
                                    first = taggedFirst;
                                    chosenReceiver = tagged.receiver;
                                }
                            }
                        }
                    }
                    if (mn.isEmpty() && !name.equals("call")) {
                        // GROOVY-5705, GROOVY-5881, GROOVY-6324, GROOVY-11366: closure property
                        var property = propX(objectExpression, call.getMethod(), call.isSafe());
                        property.setImplicitThis(call.isImplicitThis());
                        if (existsProperty(property, true)
                                && getType(property).equals(CLOSURE_TYPE)) {
                            chosenReceiver = Receiver.make(getType(property));
                            call.putNodeMetaData("callable property", property);
                            List<Expression> list = argumentList.getExpressions();
                            int nArgs = list.stream().noneMatch(e -> e instanceof SpreadExpression) ? list.size() : -1;
                            mn = List.of(nArgs == 0 ? CLOSURE_CALL_NO_ARG : nArgs == 1 ? CLOSURE_CALL_ONE_ARG : CLOSURE_CALL_VARGS);
                        }
                    }
                    if (mn.isEmpty()) {
                        mn = extension.handleMissingMethod(receiver, name, argumentList, args, call);
                        if (mn == null || mn.isEmpty()) {
                            if (first != null) mn = List.of(first); // instance method
                            else addNoMatchingMethodError(receiver, name, args, call);
                        }
                    }
                }
                if (!mn.isEmpty()) {
                    if (areCategoryMethodCalls(mn, name, args)) addCategoryMethodCallError(call);

                    {
                        ClassNode obj = chosenReceiver != null ? chosenReceiver.getType() : null;
                        if (mn.size() > 1 && obj instanceof UnionTypeClassNode) { // GROOVY-8965: support duck-typing using dynamic resolution
                            ClassNode returnType = mn.stream().map(MethodNode::getReturnType).reduce(WideningCategories::lowestUpperBound).get();
                            call.putNodeMetaData(DYNAMIC_RESOLUTION, returnType);

                            MethodNode tmp = new MethodNode(name, 0, returnType, Parameter.EMPTY_ARRAY, ClassNode.EMPTY_ARRAY, null);
                            tmp.setDeclaringClass(obj); // for storeTargetMethod
                            mn = Collections.singletonList(tmp);
                        } else {
                            mn = disambiguateMethods(mn, obj, args, call);
                        }
                    }

out:                if (mn.size() != 1) {
                        addAmbiguousErrorMessage(mn, name, args, call);
                    } else {
                        MethodNode targetMethod = mn.get(0);
                        // note second pass to differentiate from extension that sets type
                        boolean mergeType = (call.getNodeMetaData(INFERRED_TYPE) != null);
                        storeTargetMethod(call, targetMethod); // trigger onMethodSelection
                        if (call.getNodeMetaData(DIRECT_METHOD_CALL_TARGET) != targetMethod) {
                            break out; // type-checking script intervention
                        }
                        ClassNode declaringClass = targetMethod.getDeclaringClass();
                        if (chosenReceiver == null) {
                            chosenReceiver = Receiver.make(declaringClass.getPlainNodeReference());
                        }
                        if (!targetMethod.isStatic() && !(isClassType(declaringClass) || isObjectType(declaringClass)) // GROOVY-10939: Class or Object
                                && !chosenReceiver.isObject() && !Boolean.TRUE.equals(call.getNodeMetaData(DYNAMIC_RESOLUTION))) {
                            addStaticTypeError("Non-static method " + prettyPrintTypeName(declaringClass) + "#" + targetMethod.getName() + " cannot be called from static context", call);
                        } else if ((chosenReceiver.getType().isInterface() || targetMethod.isAbstract()) && isSuperExpression(objectExpression)) { // GROOVY-8299, GROOVY-10341
                            String target = toMethodParametersString(targetMethod.getName(), extractTypesFromParameters(targetMethod.getParameters()));
                            if (targetMethod.isDefault() || Traits.hasDefaultImplementation(targetMethod)) { // GROOVY-10494
                                if (objectExpression instanceof VariableExpression)
                                  addStaticTypeError("Default method " + target + " requires qualified super", call);
                            } else {
                                addStaticTypeError("Abstract method " + target + " cannot be called directly", call);
                            }
                        }

                        visitMethodCallArguments(chosenReceiver.getType(), argumentList, true, targetMethod); functorsVisited = true;

                        ClassNode returnType = getType(targetMethod);
                        // GROOVY-5470, GROOVY-6091, GROOVY-9604, GROOVY-11399:
                        if (isThisObjectExpression && call.isImplicitThis() && typeCheckingContext.getEnclosingClosure() != null
                                && (targetMethod == GET_DELEGATE || targetMethod == GET_OWNER || targetMethod == GET_THISOBJECT)) {
                            switch (name) {
                              case "getDelegate":
                                var dm = getDelegationMetadata(typeCheckingContext.getEnclosingClosure().getClosureExpression());
                                if (dm != null) {
                                    returnType = dm.getType();
                                    break;
                                }
                                // falls through
                              case "getOwner":
                                if (typeCheckingContext.getEnclosingClosureStack().size() > 1) {
                                    returnType = CLOSURE_TYPE.getPlainNodeReference();
                                    break;
                                }
                                // falls through
                              case "getThisObject":
                                returnType = makeThis();
                            }
                        } else if (returnType.isGenericsPlaceHolder() || isUsingGenericsOrIsArrayUsingGenerics(returnType)) {
                            ClassNode irtg = inferReturnTypeGenerics(chosenReceiver.getType(), targetMethod, callArguments, call.getGenericsTypes());
                            if (irtg != null && (returnType.isGenericsPlaceHolder() || implementsInterfaceOrIsSubclassOf(irtg, returnType)))
                                returnType = irtg;
                        }
                        // GROOVY-7106, GROOVY-7274, GROOVY-8909, GROOVY-8961, GROOVY-9734, GROOVY-9844, GROOVY-9915, et al.
                        Parameter[] parameters = targetMethod.getParameters();
                        if (chosenReceiver.getType().getGenericsTypes() != null && !targetMethod.isStatic() && !(targetMethod instanceof ExtensionMethodNode)) {
                            Map<GenericsTypeName, GenericsType> context = extractPlaceHoldersVisibleToDeclaration(chosenReceiver.getType(), targetMethod, argumentList);
                            parameters = Arrays.stream(parameters).map(p -> new Parameter(applyGenericsContext(context, p.getType()), p.getName())).toArray(Parameter[]::new);
                        }
                        resolvePlaceholdersFromImplicitTypeHints(args, argumentList, parameters);

                        if (typeCheckMethodsWithGenericsOrFail(chosenReceiver.getType(), args, targetMethod, call)) {
                            String data = chosenReceiver.getData();
                            if (data != null) {
                                // the method which has been chosen is supposed to be a call on delegate or owner
                                // so we store the information so that the static compiler may reuse it
                                call.putNodeMetaData(IMPLICIT_RECEIVER, data);
                            }
                            receiver = chosenReceiver.getType();
                            if (mergeType || call.getNodeMetaData(INFERRED_TYPE) == null)
                                storeType(call, adjustWithTraits(targetMethod, receiver, args, returnType));

                            if (objectExpression instanceof VariableExpression && ((VariableExpression) objectExpression).isClosureSharedVariable()) {
                                // if the object expression is a closure shared variable, we will have to perform a second pass
                                typeCheckingContext.secondPassExpressions.add(new SecondPassExpression<>(call, args));
                            }
                        } else {
                            call.removeNodeMetaData(DIRECT_METHOD_CALL_TARGET);
                        }
                    }
                }
            }
            // adjust typing for explicit math methods which have special handling; operator variants handled elsewhere
            if (args.length == 1 && isNumberType(args[0]) && isNumberType(receiver) && NUMBER_OPS.containsKey(name)) {
                ClassNode resultType = getMathResultType(NUMBER_OPS.get(name), receiver, args[0], name);
                if (resultType != null) {
                    storeType(call, resultType);
                }
            }

            // type-checking extension may have changed or cleared target method
            MethodNode target = call.getNodeMetaData(DIRECT_METHOD_CALL_TARGET);
            if (!functorsVisited) {
                visitMethodCallArguments(receiver, argumentList, true, target);
            }
            if (target != null) { Parameter[] params = target.getParameters();
                checkClosureMetadata(argumentList.getExpressions(), params);
                checkForbiddenSpreadArgument(argumentList, params);
            } else {
                checkForbiddenSpreadArgument(argumentList);
            }
        } finally {
            typeCheckingContext.popEnclosingMethodCall();
            extension.afterMethodCall(call);
        }
    }

    private void checkClosureMetadata(final List<Expression> arguments, final Parameter[] parameters) {
        // TODO: Check additional delegation metadata like type (see checkClosureWithDelegatesTo).
        for (int i = 0, n = Math.min(arguments.size(), parameters.length); i < n; i += 1) {
            Expression argument = arguments.get(i);
            ClassNode aType = getType(argument), pType = parameters[i].getType();
            // GROOVY-7996: check delegation metadata of closure parameter used as method call argument
            if (CLOSURE_TYPE.equals(aType) && CLOSURE_TYPE.equals(pType) && argument instanceof VariableExpression && ((VariableExpression) argument).getAccessedVariable() instanceof Parameter) {
                int incomingStrategy = getResolveStrategy((Parameter) ((VariableExpression) argument).getAccessedVariable());
                int outgoingStrategy = getResolveStrategy(parameters[i]);
                if (incomingStrategy != outgoingStrategy) {
                    addStaticTypeError("Closure parameter with resolve strategy " + getResolveStrategyName(incomingStrategy) + " passed to method with resolve strategy " + getResolveStrategyName(outgoingStrategy), argument);
                }
            }
        }
    }

    private int getResolveStrategy(final Parameter parameter) {
        List<AnnotationNode> annotations = parameter.getAnnotations(DELEGATES_TO);
        if (annotations != null && !annotations.isEmpty()) {
            Expression strategy = annotations.get(0).getMember("strategy");
            if (strategy != null) {
                return (Integer) evaluateExpression(castX(Integer_TYPE, strategy), getSourceUnit().getConfiguration(), getSourceUnit().getClassLoader());
            }
        }
        return Closure.OWNER_FIRST;
    }

    /**
     * A special method handling the "withTraits" call for which the type checker
     * knows more than what the type signature is able to tell. If "withTraits"
     * is detected, then a new class node is created representing the receiver
     * type interfaces and the trait interface(s).
     *
     * @param directMethodCallCandidate a method selected by the type checker
     * @param receiver                  the receiver of the method call
     * @param argumentTypes             the argument types of the method call
     * @param returnType                the return type as inferred by the type checker
     * @return proxy return type if the selected method is {@link org.codehaus.groovy.runtime.DefaultGroovyMethods#withTraits(Object, Class[]) withTraits}
     */
    private static ClassNode adjustWithTraits(final MethodNode directMethodCallCandidate, final ClassNode receiver, final ClassNode[] argumentTypes, final ClassNode returnType) {
        if ("withTraits".equals(directMethodCallCandidate.getName()) && isDefaultExtension(directMethodCallCandidate)) {
            List<ClassNode> interfaces = new ArrayList<>(Arrays.asList(receiver.getInterfaces()));
            for (ClassNode argumentType : argumentTypes) {
                if (isClassClassNodeWrappingConcreteType(argumentType)) {
                    argumentType = argumentType.getGenericsTypes()[0].getType();
                }
                if (argumentType.isInterface()) {
                    interfaces.add(argumentType);
                }
            }
            return new WideningCategories.LowestUpperBoundClassNode("ProxyOf$" + receiver.getNameWithoutPackage(), OBJECT_TYPE, interfaces.toArray(ClassNode.EMPTY_ARRAY));
        }
        return returnType;
    }

    /**
     * In the case of a <em>Object.with { ... }</em> call, this method is supposed to retrieve
     * the inferred closure return type.
     *
     * @param callArguments the argument list from the <em>Object#with(Closure)</em> call, i.e. a single closure expression
     * @return the inferred closure return type or <em>null</em>
     */
    protected ClassNode getInferredReturnTypeFromWithClosureArgument(final Expression callArguments) {
        if (!(callArguments instanceof ArgumentListExpression argList)) return null;

        ClosureExpression closure = (ClosureExpression) argList.getExpression(0);

        visitClosureExpression(closure);

        return getInferredReturnType(closure);
    }

    /**
     * Given an object expression (a message receiver expression), generate list
     * of possible types.
     *
     * @param objectExpression the message receiver
     * @return types and qualifiers of the receiver
     */
    protected List<Receiver<String>> makeOwnerList(final Expression objectExpression) {
        ClassNode receiver = getType(objectExpression);
        List<Receiver<String>> owners = new ArrayList<>();
        if (typeCheckingContext.delegationMetadata != null
                && /*isNested:*/typeCheckingContext.delegationMetadata.getParent() != null
                && objectExpression instanceof VariableExpression v && v.getName().equals("owner")) {
            owners.add(new Receiver<>(receiver, "owner")); // if nested, Closure is the first owner
            List<Receiver<String>> thisType = new ArrayList<>(2); // and the enclosing class is the
            addDelegateReceiver(thisType, makeThis(), null);      // end of the closure owner chain
            addReceivers(owners, thisType, typeCheckingContext.delegationMetadata.getParent(), "owner.");
        } else {
            List<ClassNode> temporaryTypes = getTemporaryTypesForExpression(objectExpression);
            temporaryTypes.removeIf(t -> receiver.isDerivedFrom(t) || receiver.implementsInterface(t));
            if (!temporaryTypes.isEmpty()) { // GROOVY-8965, GROOVY-10180, GROOVY-10668, et al.
                owners.add(Receiver.make(newIntersectionTypeClassNode(temporaryTypes)));
            }
            if (isClassClassNodeWrappingConcreteType(receiver)) {
                ClassNode staticType = receiver.getGenericsTypes()[0].getType();
                owners.add(new Receiver<>(staticType, false, null)); // Type from Class<Type>
                addTraitType(staticType, owners); // T in Class<T$Trait$Helper>
                owners.add(Receiver.make(receiver)); // Class<Type>
            } else {
                addBoundType(receiver, owners);
                addSelfTypes(receiver, owners);
                addTraitType(receiver, owners);
                if (isSuperExpression(objectExpression)) { // GROOVY-9909: super.defaultMethod()
                    for (ClassNode in : typeCheckingContext.getEnclosingClassNode().getInterfaces()) {
                        if (!receiver.implementsInterface(in)) owners.add(Receiver.make(in));
                    }
                }
            }
        }
        return owners;
    }

    private static void addBoundType(final ClassNode receiver, final List<Receiver<String>> owners) {
        if (!receiver.isGenericsPlaceHolder() || receiver.getGenericsTypes() == null) {
            owners.add(Receiver.make(receiver));
            return;
        }

        GenericsType gt = receiver.getGenericsTypes()[0];
        if (gt.getLowerBound() == null && gt.getUpperBounds() != null) {
            for (ClassNode cn : gt.getUpperBounds()) { // T extends C & I
                addBoundType(cn, owners);
                addSelfTypes(cn, owners);
            }
        } else {
            ClassNode cn = gt.getType().redirect(); // GROOVY-10846
            owners.add(Receiver.make(cn)); // T or T super Type
        }
    }

    private static void addSelfTypes(final ClassNode receiver, final List<Receiver<String>> owners) {
        for (ClassNode selfType : Traits.collectSelfTypes(receiver, new LinkedHashSet<>())) {
            owners.add(Receiver.make(selfType));
        }
    }

    private static void addTraitType(final ClassNode receiver, final List<Receiver<String>> owners) {
        if (Traits.isTrait(receiver.getOuterClass()) && receiver.getName().endsWith("$Helper")) {
            ClassNode traitType = receiver.getOuterClass();
            owners.add(Receiver.make(traitType));
            addSelfTypes(traitType, owners);
        }
    }

    private   void checkForbiddenSpreadArgument(final ArgumentListExpression arguments, final Parameter[] parameters) {
        if (!isVargs(parameters)) {
            checkForbiddenSpreadArgument(arguments);
        } else { // GROOVY-10597: allow spread for the ... param
            List<Expression> list = arguments.getExpressions();
            list = list.subList(0, Math.min(parameters.length - 1, list.size())); // GROOVY-11448
            checkForbiddenSpreadArgument(args(list));
        }
    }

    /**
     * Rejects spread arguments where no target signature is available.
     */
    protected void checkForbiddenSpreadArgument(final ArgumentListExpression arguments) {
        for (Expression argument : arguments) {
            if (argument instanceof SpreadExpression) {
                addStaticTypeError("The spread operator cannot be used as argument of method or closure calls with static type checking because the number of arguments cannot be determined at compile time", argument);
            }
        }
    }

    /**
     * Stores the selected target method on the call expression.
     */
    protected void storeTargetMethod(final Expression call, final MethodNode target) {
        if (target == null) {
            call.removeNodeMetaData(DIRECT_METHOD_CALL_TARGET); return;
        }
        call.putNodeMetaData(DIRECT_METHOD_CALL_TARGET,target);

        checkInterfaceStaticCall(call, target);
        checkOrMarkPrivateAccess(call, target);
        checkSuperCallFromClosure(call, target);
        extension.onMethodSelection(call, target);
    }

    private void checkInterfaceStaticCall(final Expression call, final MethodNode target) {
        if (target instanceof ExtensionMethodNode) return;
        ClassNode declaringClass = target.getDeclaringClass();
        if (declaringClass.isInterface() && target.isStatic()) {
            Expression objectExpression = getObjectExpression(call);
            if (objectExpression != null) { ClassNode type = getType(objectExpression);
                if (!isClassClassNodeWrappingConcreteType(type) || !type.getGenericsTypes()[0].getType().equals(declaringClass)) {
                    addStaticTypeError("static method of interface " + prettyPrintTypeName(declaringClass) + " can only be accessed with class qualifier", call);
                }
            }
        }
    }

    private void checkSuperCallFromClosure(final Expression call, final MethodNode target) {
        if (isSuperExpression(getObjectExpression(call)) && typeCheckingContext.getEnclosingClosure() != null) {
            ClassNode current = typeCheckingContext.getEnclosingClassNode();
            current.getNodeMetaData(SUPER_MOP_METHOD_REQUIRED, x -> new LinkedList<>()).add(target);
            call.putNodeMetaData(SUPER_MOP_METHOD_REQUIRED, current);
        }
    }

    private static Expression getObjectExpression(final Expression expression) {
        if (expression instanceof MethodCallExpression) {
            return ((MethodCallExpression) expression).getObjectExpression();
        }
        if (expression instanceof PropertyExpression) {
            return ((PropertyExpression) expression).getObjectExpression();
        }
        /*if (expression instanceof MethodPointerExpression) {
            return ((MethodPointerExpression) expression).getExpression();
        }*/
        return null;
    }

    /**
     * Validates closure calls against the signatures implied by the closure parameters.
     */
    protected void typeCheckClosureCall(final Expression arguments, final ClassNode[] argumentTypes, final Parameter[] parameters) {
        int nArguments = argumentTypes.length;
        List<ClassNode[]> signatures = new LinkedList<>();
        signatures.add(extractTypesFromParameters(parameters));

        var n = Arrays.stream(parameters).filter(Parameter::hasInitialExpression).count();
        for (int i = 1; i <= n; i += 1) { // drop parameters with value from right to left
            ClassNode[] signature = new ClassNode[parameters.length - i];
            int j = 1, index = 0;
            for (Parameter parameter : parameters) {
                if (j > n - i && parameter.hasInitialExpression()) {
                    // skip parameter with default argument
                } else {
                    signature[index++] = parameter.getType();
                }
                if (parameter.hasInitialExpression()) j += 1;
            }
            signatures.add(signature);
        }

        for (var it = signatures.listIterator(); it.hasNext(); ) { var signature = it.next();
            if (asBoolean(signature) && last(signature).isArray()) {
                int vaIndex = signature.length - 1;
                if (vaIndex == nArguments) { // empty array
                    it.add(Arrays.copyOf(signature, nArguments));
                } else if (vaIndex < nArguments) { // spread arg(s)?
                    var subType = last(signature).getComponentType();
                    signature = Arrays.copyOf(signature, nArguments);
                    Arrays.fill(signature, vaIndex, nArguments, subType);
                    it.add(signature);
                }
            }
        }

        ClassNode[] firstMatch = null;

trying: for (ClassNode[] signature : signatures) {
            if (nArguments == signature.length) {
                if (firstMatch == null)
                    firstMatch = signature;
                for (int i = 0; i < nArguments; i += 1) {
                    if (!isAssignableTo(argumentTypes[i], signature[i])) {
                        continue trying;
                    }
                }
                return; // arguments match
            }
        }

        String actual = formatArgumentList(argumentTypes);
        String expect = formatArgumentList(firstMatch != null ? firstMatch : signatures.get(0));
        addStaticTypeError("Cannot call closure that accepts " + expect + " with " + actual, arguments);
    }

    /** {@inheritDoc} */
    @Override
    public void visitIfElse(final IfStatement ifElse) {
        Map<VariableExpression, List<ClassNode>> oldTracker = pushAssignmentTracking();
        try {
            Statement thenPath = ifElse.getIfBlock(), elsePath = ifElse.getElseBlock();

            // create a new scope for instanceof testing
            typeCheckingContext.pushTemporaryTypeInfo();
            visitStatement(ifElse);
            ifElse.getBooleanExpression().visit(this);

            var tti = Map.copyOf(typeCheckingContext.temporaryIfBranchTypeInformation.peek()); // GROOVY-11864

            thenPath.visit(this);

            typeCheckingContext.temporaryIfBranchTypeInformation.pop();
            // GROOVY-6099: isolate assignment tracking
            restoreTypeBeforeConditional();

            // GROOVY-6429: reverse instanceof tracking
            typeCheckingContext.pushTemporaryTypeInfo();
            // GROOVY-11983: only invert when the boolean's falsity pins down each instanceof
            boolean canInvert = canInvertNarrowingForElseBranch(ifElse.getBooleanExpression());
            if (canInvert) tti.forEach(this::putNotInstanceOfTypeInfo);

            elsePath.visit(this);

            typeCheckingContext.popTemporaryTypeInfo();
            // GROOVY-8523: propagate tracking to outer scope; keep simple for now
            if (canInvert && elsePath.isEmpty() && !GeneralUtils.maybeFallsThrough(thenPath)) {
                tti.forEach(this::putNotInstanceOfTypeInfo);
            }
        } finally {
            popAssignmentTracking(oldTracker);
        }
    }

    /** {@inheritDoc} */
    @Override
    public void visitSwitch(final SwitchStatement statement) {
        typeCheckingContext.pushEnclosingSwitchStatement(statement);
        try {
            Map<VariableExpression, List<ClassNode>> oldTracker = pushAssignmentTracking();
            try {
                super.visitSwitch(statement);
            } finally {
                popAssignmentTracking(oldTracker);
            }
        } finally {
            typeCheckingContext.popTemporaryTypeInfo();
            typeCheckingContext.popEnclosingSwitchStatement();
        }
    }

    /**
     * Type-checks a switch expression: visits the selector and each arm, selects
     * an {@code isCase} target for non-intrinsic arms, unifies the yielded types
     * (JEP 361 poly expression), and reports a non-exhaustive switch when the
     * selector type is statically known.
     *
     * @since 6.0.0
     */
    @Override
    public void visitSwitchExpression(final SwitchExpression expression) {
        typeCheckingContext.pushEnclosingSwitchExpression(expression);
        try {
            Map<VariableExpression, List<ClassNode>> oldTracker = pushAssignmentTracking();
            try {
                super.visitSwitchExpression(expression);
            } finally {
                popAssignmentTracking(oldTracker);
            }

            List<ClassNode> yieldTypes = typeCheckingContext.getEnclosingSwitchExpressionYieldTypes();
            ClassNode resultType;
            if (yieldTypes.isEmpty()) {
                resultType = OBJECT_TYPE;
            } else {
                resultType = yieldTypes.get(0);
                for (int i = 1; i < yieldTypes.size(); i += 1) {
                    resultType = lowestUpperBound(resultType, yieldTypes.get(i));
                }
                resultType = wrapTypeIfNecessary(checkForTargetType(expression, resultType));
            }
            storeType(expression, resultType);
            expression.setType(resultType);

            typeCheckSwitchExpressionIsCase(expression);
            checkSwitchExpressionDuplicateLabels(expression);
            checkSwitchExpressionExhaustiveness(expression);
        } finally {
            typeCheckingContext.popTemporaryTypeInfo();
            typeCheckingContext.popEnclosingSwitchExpression();
        }
    }

    /**
     * Resolves {@code isCase} for every non-null label by running method
     * selection on a dummy call. Type-checking extensions, instance methods,
     * DGM and other extensions all see that call. A selected target is stored
     * on the {@link CaseStatement} for {@code writeDirectMethodCall}; no target
     * is a compilation error. Primitive int constant switches skip this: their
     * stack type cannot erase, so tableswitch is guaranteed. Wrapper, String
     * and enum selectors can erase to {@code Object} (list {@code getAt}, etc.).
     */
    private void typeCheckSwitchExpressionIsCase(final SwitchExpression expression) {
        Expression selector = expression.getExpression();
        ClassNode selectorType = getType(selector);
        if (isIntegralType(selectorType)
                && isOptimizedIntSwitch(selectorType, expression.getCaseStatements())) {
            return;
        }
        for (CaseStatement caseStatement : expression.getCaseStatements()) {
            Expression caseValue = caseStatement.getExpression();
            if (isNullConstant(caseValue)) {
                continue;
            }
            ClassNode caseType = getType(caseValue);
            VariableExpression dummyReceiver = varX("#case", caseType);
            dummyReceiver.setSourcePosition(caseValue);
            VariableExpression dummySelector = varX("#selector", getWrapper(selectorType));
            dummySelector.setSourcePosition(selector);
            MethodCallExpression call = callX(dummyReceiver, "isCase", args(dummySelector));
            call.setImplicitThis(false);
            call.setSourcePosition(caseValue);
            visitMethodCallExpression(call);
            MethodNode target = call.getNodeMetaData(DIRECT_METHOD_CALL_TARGET);
            if (target == null) {
                MethodNode enclosing = typeCheckingContext.getEnclosingMethod();
                if (enclosing == null || !isSkipMode(enclosing)) {
                    addNoMatchingMethodError(caseType, "isCase", new ClassNode[]{getWrapper(selectorType)}, caseValue);
                }
                continue;
            }
            caseStatement.putNodeMetaData(DIRECT_METHOD_CALL_TARGET, target);
            Object privateAccess = call.getNodeMetaData(PV_METHODS_ACCESS);
            if (privateAccess != null) {
                caseStatement.putNodeMetaData(PV_METHODS_ACCESS, privateAccess);
            }
        }
    }

    /**
     * Reports a repeated constant case label in a switch expression. Sequential
     * {@code isCase} semantics make the second arm dead code, and the optimized
     * {@code tableswitch}/{@code lookupswitch} forms cannot represent it at all,
     * so it is rejected here, uniformly for type-checked and statically-compiled
     * code (GROOVY-12289). Labels compared are the same ones the optimizers key
     * on: int-family, String and enum constants; anything else (GStrings, calls,
     * regex or collection labels) cannot be proven duplicated statically and is
     * left to sequential first-match-wins dispatch.
     *
     * @since 6.0.0
     */
    private void checkSwitchExpressionDuplicateLabels(final SwitchExpression expression) {
        ClassNode enumType = unwrapEnumType(getType(expression.getExpression()));
        Set<Object> seen = new HashSet<>();
        for (CaseStatement caseStatement : expression.getCaseStatements()) {
            Expression label = caseStatement.getExpression();
            Object key = null;
            if (enumType != null && enumType.isEnum()) {
                String name = enumConstantName(label, enumType);
                // keyed by type and name so an enum constant never collides with
                // a string label of the same spelling (a distinct, legal label)
                if (name != null) key = Map.entry(enumType, name);
            }
            if (key == null) key = intConstant(label);
            if (key == null) key = stringConstant(label);
            if (key != null && !seen.add(key)) {
                addStaticTypeError("Duplicate case label: " + label.getText(), label);
            }
        }
    }

    /**
     * {@inheritDoc}
     */
    @Override
    protected void afterSwitchConditionExpressionVisited(final SwitchExpression expression) {
        typeCheckingContext.pushTemporaryTypeInfo();
        Expression conditionExpression = expression.getExpression();
        conditionExpression.putNodeMetaData(TYPE, getType(conditionExpression));
    }

    /**
     * {@inheritDoc}
     */
    @Override
    protected void afterSwitchCaseStatementsVisited(final SwitchExpression expression) {
        if (!expression.getDefaultStatement().isEmpty()) {
            Expression selectable = expression.getExpression();
            optInstanceOfTypeInfo(selectable, selectable.getNodeMetaData(TYPE));
        }
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public void visitYieldStatement(final YieldStatement statement) {
        super.visitYieldStatement(statement);
        if (typeCheckingContext.getEnclosingSwitchExpression() != null) {
            typeCheckingContext.getEnclosingSwitchExpressionYieldTypes().add(getType(statement.getExpression()));
        }
    }

    private Expression selectableOfEnclosingSwitch() {
        return typeCheckingContext.getEnclosingSwitchSelector();
    }

    private void checkSwitchExpressionExhaustiveness(final SwitchExpression expression) {
        if (expression.getDefaultStatement() != null && !expression.getDefaultStatement().isEmpty()) {
            return;
        }
        Expression selector = expression.getExpression();
        ClassNode selectorType = getType(selector);
        if (selectorType != null && selectorType.isEnum() && coversAllEnumConstants(expression, selectorType)) {
            return;
        }
        addError("the switch expression does not cover all possible input values", expression);
    }

    private boolean coversAllEnumConstants(final SwitchExpression expression, final ClassNode enumType) {
        Set<String> remaining = new LinkedHashSet<>();
        for (FieldNode field : enumType.redirect().getFields()) {
            if (field.isEnum()) {
                remaining.add(field.getName());
            }
        }
        if (remaining.isEmpty() && enumType.isResolved()) {
            Object[] constants = enumType.getTypeClass().getEnumConstants();
            if (constants != null) {
                for (Object constant : constants) {
                    remaining.add(((Enum<?>) constant).name());
                }
            }
        }
        if (remaining.isEmpty()) {
            return false;
        }
        for (CaseStatement caseStatement : expression.getCaseStatements()) {
            Expression caseExpr = caseStatement.getExpression();
            if (caseExpr instanceof VariableExpression variable) {
                remaining.remove(variable.getName());
            } else if (caseExpr instanceof PropertyExpression property
                    && property.getObjectExpression() instanceof ClassExpression owner
                    && owner.getType().equals(enumType)
                    && property.getProperty() instanceof ConstantExpression name) {
                remaining.remove(name.getText());
            }
        }
        return remaining.isEmpty();
    }

    /** {@inheritDoc} */
    @Override
    protected void afterSwitchConditionExpressionVisited(final SwitchStatement statement) {
        typeCheckingContext.pushTemporaryTypeInfo();
        Expression conditionExpression = statement.getExpression();
        conditionExpression.putNodeMetaData(TYPE, getType(conditionExpression));
    }

    /** {@inheritDoc} */
    @Override
    protected void afterSwitchCaseStatementsVisited(final SwitchStatement statement) {
        // GROOVY-8411: if any "case Type:" then "default:" contributes alternate type
        if (!statement.getDefaultStatement().isEmpty()) {
            Expression selectable = statement.getExpression();
            optInstanceOfTypeInfo(selectable, selectable.getNodeMetaData(TYPE));
        }
    }

    /** {@inheritDoc} */
    @Override
    public void visitCaseStatement(final CaseStatement statement) {
        Expression selectable = selectableOfEnclosingSwitch();
        if (selectable == null) {
            super.visitCaseStatement(statement);
            return;
        }
        Expression expression = statement.getExpression();
        if (expression instanceof ClassExpression) { // GROOVY-8411: refine the switch type
            if (!optInstanceOfTypeInfo(selectable, expression.getType())) {
                pushInstanceOfTypeInfo(selectable, expression);
            }
        } else if (expression instanceof ClosureExpression) { // GROOVY-9854: propagate the switch type
            ClassNode inf = selectable.getNodeMetaData(TYPE);
            expression.putNodeMetaData(CLOSURE_ARGUMENTS, new ClassNode[]{inf});
            Parameter[] params = ((ClosureExpression) expression).getParameters();
            if (params != null && params.length == 1) {
                boolean lambda = (expression instanceof LambdaExpression);
                checkParamType(params[0], wrapTypeIfNecessary(inf), false, lambda);
            } else if (params == null || params.length > 1) {
                int paramCount = (params != null ? params.length : 0);
                addError("Incorrect number of parameters. Expected 1 but found " + paramCount, expression);
            }
        }
        super.visitCaseStatement(statement);
        if (!maybeFallsThrough(statement.getCode())) {
            typeCheckingContext.temporaryIfBranchTypeInformation
                .peek().remove(extractTemporaryTypeInfoKey(selectable));
            restoreTypeBeforeConditional(); // isolate assignment branch
        }
    }

    private static boolean maybeFallsThrough(Statement statement) {
        if (statement.isEmpty()) return true;
        if (statement instanceof BlockStatement)
            statement = DefaultGroovyMethods.last(((BlockStatement) statement).getStatements());
        // end break, continue, return or throw
        if (statement instanceof BreakStatement
                || statement instanceof ContinueStatement
                || !GeneralUtils.maybeFallsThrough(statement)) {
            return false;
        }
        return true;
    }

    private void recordAssignment(final VariableExpression lhsExpr, final ClassNode rhsType) {
        typeCheckingContext.ifElseForWhileAssignmentTracker.computeIfAbsent(lhsExpr, lhs -> {
            ClassNode lhsType = lhs.getNodeMetaData(INFERRED_TYPE);
            List<ClassNode> types = new ArrayList<>(2);
            types.add(lhsType);
            return types;
        }).add(rhsType);
    }

    private void restoreTypeBeforeConditional() {
        typeCheckingContext.ifElseForWhileAssignmentTracker.forEach((var, types) -> {
            var.putNodeMetaData(INFERRED_TYPE, types.get(0));
        });
    }

    /**
     * Pushes a fresh assignment tracker and returns the previous tracker.
     */
    protected Map<VariableExpression, List<ClassNode>> pushAssignmentTracking() {
        Map<VariableExpression, List<ClassNode>> oldTracker = typeCheckingContext.ifElseForWhileAssignmentTracker;
        typeCheckingContext.ifElseForWhileAssignmentTracker = new HashMap<>();
        return oldTracker;
    }

    /**
     * Pops the current assignment tracker and applies any merged types.
     */
    protected Map<VariableExpression, ClassNode> popAssignmentTracking(final Map<VariableExpression, List<ClassNode>> oldTracker) {
        Map<VariableExpression, ClassNode> assignments = new HashMap<>();
        typeCheckingContext.ifElseForWhileAssignmentTracker.forEach((var, types) -> {
            types.stream().filter(t -> t != null && t != UNKNOWN_PARAMETER_TYPE) // GROOVY-6099, GROOVY-10294
                    .reduce(WideningCategories::lowestUpperBound).ifPresent(type -> {
                assignments.put(var, type);
                storeType(var, type);
            });
        });
        typeCheckingContext.ifElseForWhileAssignmentTracker = oldTracker;
        // GROOVY-9786, GROOVY-11450: nested conditional assignments
        if (oldTracker != null) {
            assignments.forEach(this::recordAssignment);
        }
        return assignments;
    }

    /** {@inheritDoc} */
    @Override
    public void visitArrayExpression(final ArrayExpression expression) {
        super.visitArrayExpression(expression);
        ClassNode elementType;
        List<Expression> expressions;
        if (expression.hasInitializer()) {
            elementType = expression.getElementType();
            expressions = expression.getExpressions();
        } else {
            elementType = int_TYPE;
            expressions = expression.getSizeExpression();
        }
        for (Expression elementExpr : expressions) {
            if (!checkCompatibleAssignmentTypes(elementType, getType(elementExpr), elementExpr, false)) {
                addStaticTypeError("Cannot convert from " + prettyPrintType(getType(elementExpr)) + " to " + prettyPrintType(elementType), elementExpr);
            }
        }
    }

    /** {@inheritDoc} */
    @Override
    public void visitCastExpression(final CastExpression expression) {
        ClassNode target = expression.getType();
        Expression source = expression.getExpression();
        if (target instanceof IntersectionTypeClassNode) { // GROOVY-11998
            validateAndApplyIntersectionCast((IntersectionTypeClassNode) target, expression, source);
        } else {
            applyTargetType(target, source); // GROOVY-9997
        }

        source.visit(this);

        if (!expression.isCoerce() && !checkCast(target, source)) {
            addStaticTypeError("Inconvertible types: cannot cast " + prettyPrintType(getType(source)) + " to " + prettyPrintType(target), expression);
        }
    }

    /**
     * Validates JLS §4.9 well-formedness of an intersection cast target,
     * and for lambda / method-reference / closure operands picks the
     * SAM-bearing component as the primary functional target so that
     * parameter inference (and downstream lambda factory generation)
     * can proceed. Additional components are stored as
     * {@link StaticTypesMarker#LAMBDA_MARKERS} on both the cast and
     * source for use by the bytecode writers.
     */
    private void validateAndApplyIntersectionCast(final IntersectionTypeClassNode target,
                                                  final CastExpression expression,
                                                  final Expression source) {
        ClassNode[] components = target.getComponents();
        int classCount = 0;
        boolean classNotFirst = false;
        for (int i = 0, n = components.length; i < n; i += 1) {
            ClassNode c = components[i];
            if (isPrimitiveType(c)) {
                addStaticTypeError("Intersection type components must be reference types: " + prettyPrintType(c), expression);
            }
            if (!c.isInterface()) {
                classCount += 1;
                if (i != 0) classNotFirst = true;
                if (Modifier.isFinal(c.getModifiers())) {
                    addStaticTypeError("Intersection type may not include the final class " + prettyPrintType(c), expression);
                }
            }
        }
        if (classCount > 1) {
            addStaticTypeError("Intersection type may include at most one class component: " + prettyPrintType(target), expression);
        }
        if (classNotFirst) {
            addStaticTypeError("Class component of intersection type must come first: " + prettyPrintType(target), expression);
        }

        boolean isFunctionalSource = source instanceof ClosureExpression
                                  || source instanceof MethodReferenceExpression;
        if (!isFunctionalSource) return; // non-functional cast: just decompose check below

        ClassNode primary = null;
        List<ClassNode> markers = new ArrayList<>(components.length);
        for (ClassNode c : components) {
            if (!c.isInterface()) { markers.add(c); continue; }
            MethodNode sam = findSAM(c);
            if (sam == null) {
                markers.add(c); // marker interface
            } else if (primary == null) {
                primary = c;
            } else {
                addStaticTypeError("Intersection type for lambda/closure has multiple functional interface components: "
                        + prettyPrintType(primary) + " and " + prettyPrintType(c), expression);
                markers.add(c);
            }
        }
        if (primary == null) {
            addStaticTypeError("Intersection type for lambda/closure target has no functional interface component: " + prettyPrintType(target), expression);
            return;
        }
        applyTargetType(primary, source);
        expression.putNodeMetaData(PRIMARY_FUNCTIONAL_TYPE, primary);
        source.putNodeMetaData(PRIMARY_FUNCTIONAL_TYPE, primary);
        if (!markers.isEmpty()) {
            source.putNodeMetaData(LAMBDA_MARKERS, markers);
            expression.putNodeMetaData(LAMBDA_MARKERS, markers);
        }
        if (source instanceof LambdaExpression) {
            for (ClassNode m : markers) {
                if (m.equals(ClassHelper.SERIALIZABLE_TYPE)
                        || m.implementsInterface(ClassHelper.SERIALIZABLE_TYPE)) {
                    ((LambdaExpression) source).setSerializable(true);
                    break;
                }
            }
        }
    }

    /**
     * Checks whether the source expression can be cast to the supplied target type.
     */
    protected boolean checkCast(final ClassNode targetType, final Expression source) {
        if (targetType instanceof IntersectionTypeClassNode it) { // GROOVY-11998
            for (ClassNode component : it.getComponents()) {
                if (!checkCast(component, source)) return false;
            }
            return true;
        }
        if (isNullConstant(source)) {
            return !isPrimitiveType(targetType) || isPrimitiveBoolean(targetType); // GROOVY-6577
        }
        ClassNode sourceType = getType(source);
        if (targetType.isArray() && sourceType.isArray()) {
            ClassNode targetItemType = targetType.getComponentType();
            ClassNode sourceItemType = sourceType.getComponentType();
            return (isPrimitiveType(targetItemType) && !isPrimitiveBoolean(targetItemType))
                    ? isPrimitiveType(sourceItemType) // GROOVY-11371: primitive array only
                    : checkCast(targetItemType, varX("_", sourceItemType));
        } else if (isPrimitiveChar(targetType) && isStringType(sourceType)
                && source instanceof ConstantExpression && source.getText().length() == 1) {
            // ex: (char) 'c'
        } else if (isWrapperCharacter(targetType) && isStringType(sourceType)
                && source instanceof ConstantExpression && source.getText().length() == 1) {
            // ex: (Character) 'c'
        } else if (isNumberCategory(getWrapper(targetType)) && (isNumberCategory(getWrapper(sourceType)) || isPrimitiveChar(sourceType))) {
            // ex: (long) 0
        } else if (isPrimitiveChar(targetType) && isPrimitiveType(sourceType) && isNumberType(sourceType)) {
            // ex: (char) 0
        } else if (!Modifier.isFinal(sourceType.getModifiers()) && targetType.isInterface()) {
            // ex: TODO
        } else if (!Modifier.isFinal(targetType.getModifiers()) && sourceType.isInterface()) {
            // ex: TODO
        } else if (!isAssignableTo(targetType, sourceType) && !implementsInterfaceOrIsSubclassOf(sourceType, targetType)) {
            return false;
        }
        return true;
    }

    /** {@inheritDoc} */
    @Override
    public void visitTernaryExpression(final TernaryExpression expression) {
        Map<VariableExpression, List<ClassNode>> oldTracker = pushAssignmentTracking();
        typeCheckingContext.pushTemporaryTypeInfo(); // capture instanceof restriction
        if (!(expression instanceof ElvisOperatorExpression)) {
            expression.getBooleanExpression().visit(this);
        }
        Expression trueExpression = expression.getTrueExpression();
        ClassNode typeOfTrue = findCurrentInstanceOfClass(trueExpression, null);
        typeOfTrue = Optional.ofNullable(typeOfTrue).orElse(visitValueExpression(trueExpression));
        Map<Object, List<ClassNode>> tti = typeCheckingContext.temporaryIfBranchTypeInformation.pop();

        typeCheckingContext.pushTemporaryTypeInfo();
        // GROOVY-8412 / GROOVY-11983: only invert when sound (no LOGICAL_AND in condition)
        if (!(expression instanceof ElvisOperatorExpression)
                && canInvertNarrowingForElseBranch(expression.getBooleanExpression())) {
            tti.forEach(this::putNotInstanceOfTypeInfo);
        }
        Expression falseExpression = expression.getFalseExpression();
        ClassNode typeOfFalse = visitValueExpression(falseExpression);
        typeCheckingContext.popTemporaryTypeInfo();

        ClassNode resultType;
        if (isNullConstant(trueExpression) && isNullConstant(falseExpression)) { // GROOVY-5523
            resultType = checkForTargetType(trueExpression, UNKNOWN_PARAMETER_TYPE);
        } else if (isNullConstant(trueExpression) || (isEmptyCollection(trueExpression)
                && isOrImplements(typeOfTrue, typeOfFalse))) { // [] : List/Collection/Iterable
            resultType = wrapTypeIfNecessary(checkForTargetType(falseExpression, typeOfFalse));
        } else if (isNullConstant(falseExpression) || (isEmptyCollection(falseExpression)
                && isOrImplements(typeOfFalse, typeOfTrue))) { // List/Collection/Iterable : []
            resultType = wrapTypeIfNecessary(checkForTargetType(trueExpression, typeOfTrue));
        } else {
            typeOfFalse = checkForTargetType(falseExpression, typeOfFalse);
            typeOfTrue = checkForTargetType(trueExpression, typeOfTrue);
            resultType = lowestUpperBound(typeOfTrue, typeOfFalse);
        }
        storeType(expression, resultType);
        popAssignmentTracking(oldTracker);
    }

    /**
     * @param expr true or false branch of ternary expression
     * @return the inferred type of {@code expr}
     */
    private ClassNode visitValueExpression(final Expression expr) {
        if (expr instanceof ClosureExpression) {
            applyTargetType(checkForTargetType(expr, null), expr);
        }
        expr.visit(this);
        return getType(expr);
    }

    /**
     * @param expr true or false branch of ternary expression
     * @param type the inferred type of {@code expr}
     */
    private ClassNode checkForTargetType(final Expression expr, final ClassNode type) {
        ClassNode targetType = null;
        MethodNode enclosingMethod = typeCheckingContext.getEnclosingMethod();
        MethodCall enclosingMethodCall = (MethodCall)typeCheckingContext.getEnclosingMethodCall();
        BinaryExpression enclosingExpression = typeCheckingContext.getEnclosingBinaryExpression();
        if (enclosingExpression != null
                && isAssignment(enclosingExpression.getOperation().getType())
                && isTypeSource(expr, enclosingExpression.getRightExpression())) {
            targetType = getDeclaredOrInferredType(enclosingExpression.getLeftExpression());
        } else if (enclosingMethodCall != null
                && InvocationWriter.makeArgumentList(enclosingMethodCall.getArguments())
                        .getExpressions().stream().anyMatch(arg -> isTypeSource(expr, arg))) {
            // TODO: locate method target parameter
        } else if (enclosingMethod != null
                && !enclosingMethod.isAbstract()
                && !enclosingMethod.isVoidMethod()
                && isTypeSource(expr, enclosingMethod)) {
             targetType = enclosingMethod.getReturnType();
        }

        if (expr instanceof ClosureExpression) { // GROOVY-10271, GROOVY-10272
            return isSAMType(targetType) ? targetType : type;
        }

        if (targetType == null)
            targetType = type.getPlainNodeReference();
        if (type == UNKNOWN_PARAMETER_TYPE) return targetType;

        if (expr instanceof ConstructorCallExpression) { // GROOVY-9972, GROOVY-9983, GROOVY-10114
            inferDiamondType((ConstructorCallExpression) expr, targetType);
        }

        return adjustForTargetType(type, targetType);
    }

    private static ClassNode adjustForTargetType(final ClassNode resultType, final ClassNode targetType) {
        if (targetType.isUsingGenerics()
                && missesGenericsTypes(resultType)
                // GROOVY-10324, GROOVY-10342, et al.
                && !resultType.isGenericsPlaceHolder()) {
            // unchecked assignment
            // List<Type> list = new LinkedList()
            // Iterable<Type> iter = new LinkedList()
            // Collection<Type> col1 = Collections.emptyList()
            // Collection<Type> col2 = Collections.emptyList() ?: []
            // Collection<Type> view = ConcurrentHashMap.newKeySet()

            // the inferred type of the binary expression is the type of the RHS
            // "completed" with generics type information available from the LHS
            if (targetType.equals(resultType)) {
                // GROOVY-6126, GROOVY-6558, GROOVY-6564, et al.
                if (!targetType.isGenericsPlaceHolder()) return targetType;
            } else {
                // GROOVY-5640, GROOVY-9033, GROOVY-10220, GROOVY-10235, GROOVY-10688, GROOVY-11192, et al.
                Map<GenericsTypeName, GenericsType> gt = new HashMap<>();
                ClassNode sc = resultType;
                for (;;) {
                    sc = getNextSuperClass(sc,targetType);
                    if (!gt.isEmpty()) {
                        // propagate resultType's generics
                        sc = applyGenericsContext(gt, sc);
                    }
                    if (sc == null || sc.equals(targetType)) {
                        gt.clear();
                        break;
                    }
                    // map of sc's type vars to resultType's type vars
                    extractGenericsConnections(gt, sc, sc.redirect());
                }
                extractGenericsConnections(gt, resultType, resultType.redirect());
                extractGenericsConnections(gt, targetType, sc); // maps rt's tv(s)

                return applyGenericsContext(gt, resultType.redirect());
            }
        }

        return resultType;
    }

    private static boolean isTypeSource(final Expression expr, final Expression right) {
        if (right instanceof TernaryExpression) {
            return isTypeSource(expr, ((TernaryExpression) right).getTrueExpression())
                || isTypeSource(expr, ((TernaryExpression) right).getFalseExpression());
        }
        return expr == right;
    }

    private static boolean isTypeSource(final Expression expr, final MethodNode mNode) {
        boolean[] returned = new boolean[1];

        mNode.getCode().visit(new CodeVisitorSupport() {
            @Override
            public void visitReturnStatement(final ReturnStatement returnStatement) {
                if (isTypeSource(expr, returnStatement.getExpression())) {
                    returned[0] = true;
                }
            }
            @Override
            public void visitClosureExpression(final ClosureExpression expression) {
            }
        });

        if (!returned[0]) {
            new ReturnAdder(returnStatement -> {
                if (isTypeSource(expr, returnStatement.getExpression())) {
                    returned[0] = true;
                }
            }).visitMethod(mNode);
        }

        return returned[0];
    }

    private static boolean isEmptyCollection(final Expression expr) {
        return isEmptyList(expr) || isEmptyMap(expr);
    }

    private static boolean isEmptyList(final Expression expr) {
        return expr instanceof ListExpression && ((ListExpression) expr).getExpressions().isEmpty();
    }

    private static boolean isEmptyMap(final Expression expr) {
        return expr instanceof MapExpression && ((MapExpression) expr).getMapEntryExpressions().isEmpty();
    }

    /** {@inheritDoc} */
    @Override
    public void visitTryCatchFinally(final TryCatchStatement statement) {
        Map<Parameter, ClassNode> vars = typeCheckingContext.controlStructureVariables;
        Map<VariableExpression, List<ClassNode>> oldTracker = pushAssignmentTracking();
        try {
            visitStatement(statement);
            statement.getResourceStatements().forEach(rsrc -> rsrc.visit(this));
            statement.getTryStatement().visit(this);
            for (CatchStatement catchStatement : statement.getCatchStatements()) {
                vars.put(catchStatement.getVariable(), catchStatement.getExceptionType());
                try {
                    restoreTypeBeforeConditional();
                    catchStatement.visit(this);
                } finally {
                    vars.remove(catchStatement.getVariable());
                }
            }
        } finally {
            popAssignmentTracking(oldTracker);
        }
        statement.getFinallyStatement().visit(this);
    }

    /**
     * Stores the inferred type for the supplied expression.
     */
    protected void storeType(final Expression exp, ClassNode cn) {
        if (cn == UNKNOWN_PARAMETER_TYPE) { // null for assignment or parameter
            // instead of storing an "unknown" type, reset the type information
            // by determining the declaration type of the expression
            cn = getOriginalDeclarationType(exp);
            // GROOVY-10356, GROOVY-10623: "def"
            if (isDynamicTyped(cn)) return;
        }
        if (cn != null && isPrimitiveType(cn)) {
            if (exp instanceof VariableExpression && ((VariableExpression) exp).isClosureSharedVariable()) {
                cn = getWrapper(cn);
            } else if (exp instanceof MethodCallExpression && ((MethodCallExpression) exp).isSafe()) {
                cn = getWrapper(cn);
            } else if (exp instanceof PropertyExpression && ((PropertyExpression) exp).isSafe()) {
                cn = getWrapper(cn);
            }
        }

        cn = withRareTypeFromOrigin(exp, cn);

        ClassNode oldValue = (ClassNode) exp.putNodeMetaData(INFERRED_TYPE, cn);
        if (oldValue != null) {
            // this may happen when a variable declaration type is wider than the subsequent assignment values
            // for example :
            // def o = 1 // first, an int
            // o = 'String' // then a string
            // o = new Object() // and eventually an object !
            // in that case, the INFERRED_TYPE corresponds to the current inferred type, while
            // DECLARATION_INFERRED_TYPE is the type which should be used for the initial type declaration
            ClassNode oldDIT = exp.getNodeMetaData(DECLARATION_INFERRED_TYPE);
            if (oldDIT != null) {
                exp.putNodeMetaData(DECLARATION_INFERRED_TYPE, cn == null ? oldDIT : lowestUpperBound(oldDIT, cn));
            } else {
                exp.putNodeMetaData(DECLARATION_INFERRED_TYPE, cn == null ? null : lowestUpperBound(oldValue, cn));
            }
        }

        if (exp instanceof VariableExpression var) {
            Variable accessedVariable = var.getAccessedVariable();
            if (accessedVariable instanceof VariableExpression) {
                if (accessedVariable != var)
                    storeType((VariableExpression) accessedVariable, cn);
            } else if (accessedVariable instanceof Parameter) {
                ((Parameter) accessedVariable).putNodeMetaData(INFERRED_TYPE, cn);
            }
            if (cn != null && var.isClosureSharedVariable()) {
                List<ClassNode> assignedTypes = typeCheckingContext.closureSharedVariablesAssignmentTypes.computeIfAbsent(var, k -> new LinkedList<>());
                assignedTypes.add(cn);
            }
            if (!var.isThisExpression() && !var.isSuperExpression() && !typeCheckingContext.temporaryIfBranchTypeInformation.isEmpty()) {
                // GROOVY-5226, GROOVY-11290: assignment voids instanceof
                pushInstanceOfTypeInfo(var, classX(VOID_TYPE));
            }
        }
    }

    /**
     * Rare types ({@code Outer<T>.Inner}) store the enclosing parameterization
     * on the origin type. Inference often produces a redirect of {@code Inner}
     * without that field; copy it at the {@link #storeType} write site so
     * {@link #getType} does not have to reconstruct it.
     */
    private static ClassNode withRareTypeFromOrigin(final Expression exp, final ClassNode cn) {
        if (cn == null || cn.getOuterClassType() != null) {
            return cn;
        }
        ClassNode origin = null;
        if (exp instanceof VariableExpression vexp) {
            origin = vexp.getOriginType();
        }
        if (origin == null || origin.getOuterClassType() == null) {
            return cn;
        }
        ClassNode copy = cn.getPlainNodeReference();
        copy.setGenericsTypes(origin.getGenericsTypes());
        copy.setOuterClassType(origin.getOuterClassType());
        return copy;
    }

    /**
     * Computes the result type of a binary operation.
     */
    protected ClassNode getResultType(ClassNode left, final int op, final ClassNode right, final BinaryExpression expr) {
        ClassNode leftRedirect = left.redirect();
        ClassNode rightRedirect = right.redirect();

        Expression leftExpression = expr.getLeftExpression();
        Expression rightExpression = expr.getRightExpression();

        if (op == EQUAL || op == ELVIS_EQUAL) {
            if (leftExpression instanceof VariableExpression) {
                ClassNode initialType = getOriginalDeclarationType(leftExpression);
                if (isDynamicTyped(initialType)) { // GROOVY-11353, GROOVY-11375
                    ClassNode inferredType = leftExpression.getNodeMetaData(INFERRED_TYPE);
                    if (inferredType != null ? !isPrimitiveType(inferredType) : Boolean.TRUE.equals(initialType.getNodeMetaData("non-primitive type"))) {
                        initialType = OBJECT_TYPE;
                    }
                }

                if (isPrimitiveType(rightRedirect) && (initialType.isDerivedFrom(Number_TYPE) || (isObjectType(initialType) && !isDynamicTyped(initialType)))) {
                    return getWrapper(right);
                }

                if (isPrimitiveType(initialType) && (isNumberType(initialType) ? rightRedirect.isDerivedFrom(Number_TYPE) : rightRedirect == getWrapper(initialType))) { // GROOVY-10359, GROOVY-6574
                    return getUnwrapper(right);
                }

                // as anything can be assigned to a String, Class or [Bb]oolean, return the left type instead
                if (isWildcardLeftHandSide(initialType) && !isObjectType(initialType)) {
                    return initialType;
                }
            }

            if (!isObjectType(leftRedirect)) {
                if (rightExpression instanceof ListExpression) {
                    if (LIST_TYPE.equals(leftRedirect)
                            || ITERABLE_TYPE.equals(leftRedirect)
                            || Collection_TYPE.equals(leftRedirect)
                            || ArrayList_TYPE.isDerivedFrom(leftRedirect)) { // GROOVY-6912
                        return getLiteralResultType(left, right, ArrayList_TYPE); // GROOVY-7128
                    }
                    if (SET_TYPE.equals(leftRedirect)
                            || LinkedHashSet_TYPE.isDerivedFrom(leftRedirect)) { // GROOVY-6912
                        return getLiteralResultType(left, right, LinkedHashSet_TYPE); // GROOVY-7128
                    }
                } else if (rightExpression instanceof MapExpression) {
                    if (MAP_TYPE.equals(leftRedirect)
                            || LinkedHashMap_TYPE.isDerivedFrom(leftRedirect)) {
                        return getLiteralResultType(left, right, LinkedHashMap_TYPE); // GROOVY-7128, GROOVY-9844
                    }
                } else if (rightExpression instanceof ClosureExpression
                        || rightExpression instanceof MethodPointerExpression) {
                    if (isSAMType(leftRedirect)) {
                        return left; // coercion
                    }
                }
            }

            return right;
        }

        if (isBoolIntrinsicOp(op)) {
            return boolean_TYPE;
        }

        if (op == FIND_REGEX) {
            return Matcher_TYPE;
        }

        if (isArrayOp(op)) {
            if (isOrImplements(left, MAP_TYPE) && (isStringType(right) || isGStringOrGStringStringLUB(right))) { // GROOVY-5700, GROOVY-6668, GROOVY-8212, GROOVY-8788
                PropertyExpression prop = propX(leftExpression, rightExpression); // m['xx'] -> m.xx
                return existsProperty(prop, !typeCheckingContext.isTargetOfEnclosingAssignment(expr))
                            ? getType(prop) : getTypeForMapPropertyExpression(left, prop);
            }
            Expression copy = binX(leftExpression, expr.getOperation(), rightExpression);
            copy.setSourcePosition(expr); // do not propagate BINARY_EXP_TARGET, etc.
            MethodNode method = findMethodOrFail(copy, left, "getAt", rightRedirect);
            if (method != null && !isNumberCategory(getWrapper(rightRedirect))) {
                return inferReturnTypeGenerics(left, method, rightExpression);
            }
            return inferComponentType(left, right);
        }

        String operationName = getOperationName(op);
        if (operationName == null) throw new GroovyBugError(
                "Unknown result type for binary operator " + op);
        // the left operand is determining the result of the operation
        // for primitives and their wrapper we use a fixed table here:
        ClassNode mathResultType = getMathResultType(op, leftRedirect, rightRedirect, operationName);
        if (mathResultType != null) {
            return mathResultType;
        }
        // GROOVY-9006: compare to null for types that overload equals
        if ("equals".equals(operationName) && (left == UNKNOWN_PARAMETER_TYPE
                                            || right == UNKNOWN_PARAMETER_TYPE)) {
            return boolean_TYPE;
        }
        // GROOVY-5890: do not mix Class<Type> with Type
        if (leftExpression instanceof ClassExpression) {
            left = CLASS_Type.getPlainNodeReference();
        }
        MethodNode method = findMethodOrFail(expr, left, operationName, right);
        if (method != null) {
            if (op == COMPARE_NOT_IN && isDefaultExtension(method)) {
                // GROOVY-10915: a user-defined isCase still drives `in` (GROOVY-9848 only
                // changes Map/CharSequence), so fall back to dynamic dispatch when present.
                MethodNode isCase = findMethodOrFail(expr, left, "isCase", right);
                if (isCase != null && !isDefaultExtension(isCase)) return null; // require dynamic dispatch
            }

            storeTargetMethod(expr, method);
            typeCheckMethodsWithGenericsOrFail(left, new ClassNode[]{right}, method, expr);

            if (isAssignment(op)) return left;
            if (!"compareTo".equals(operationName))
                return inferReturnTypeGenerics(left, method, args(rightExpression));
        }

        if (isCompareToBoolean(op)) return boolean_TYPE;
        if (op == COMPARE_TO) return int_TYPE;
        return null;
    }

    /**
     * For "{@code List<Type> x = [...]}" or "{@code Set<Type> y = [...]}", etc.
     * the literal may be composed of subtypes of {@code Type}. In these cases,
     * {@code ArrayList<Type>} is an appropriate result type for the expression.
     */
    private static ClassNode getLiteralResultType(final ClassNode targetType, final ClassNode sourceType, final ClassNode baseType) {
        ClassNode resultType = sourceType.equals(baseType) ? sourceType
                : GenericsUtils.parameterizeType(sourceType, baseType.getPlainNodeReference());

        if (targetType.getGenericsTypes() != null
                && !GenericsUtils.buildWildcardType(targetType).isCompatibleWith(resultType)) {
            BiPredicate<GenericsType, GenericsType> isEqualOrSuper = (target, source) -> {
                if (target.isCompatibleWith(source.getType())) {
                    return true;
                }
                if (!target.isPlaceholder() && !target.isWildcard()) {
                    return GenericsUtils.buildWildcardType(getCombinedBoundType(target)).isCompatibleWith(source.getType());
                }
                return false;
            };

            GenericsType[] lgt = targetType.getGenericsTypes(), rgt = resultType.getGenericsTypes();
            if (IntStream.range(0, lgt.length).allMatch(i -> isEqualOrSuper.test(lgt[i], rgt[i]))) {
                resultType = GenericsUtils.parameterizeType(targetType, baseType.getPlainNodeReference());
            }
        }

        return resultType;
    }

    private static ClassNode getMathResultType(final int op, final ClassNode leftRedirect, final ClassNode rightRedirect, final String operationName) {
        if (isNumberType(leftRedirect) && isNumberType(rightRedirect)) {
            if (isOperationInGroup(op)) {
                if (isIntCategory(leftRedirect) && isIntCategory(rightRedirect)) return int_TYPE;
                if (isLongCategory(leftRedirect) && isLongCategory(rightRedirect)) return long_TYPE;
                if (isFloat(leftRedirect) && isFloat(rightRedirect)) return float_TYPE;
                if (isDouble(leftRedirect) && isDouble(rightRedirect)) return double_TYPE;
            } else if (isPowerOperator(op)) {
                return Number_TYPE;
            } else if (isBitOperator(op) || op == INTDIV || op == INTDIV_EQUAL) {
                if (isIntCategory(getUnwrapper(leftRedirect)) && isIntCategory(getUnwrapper(rightRedirect)))
                    return int_TYPE;
                if (isLongCategory(getUnwrapper(leftRedirect)) && isLongCategory(getUnwrapper(rightRedirect)))
                    return long_TYPE;
                if (isBigIntCategory(getUnwrapper(leftRedirect)) && isBigIntCategory(getUnwrapper(rightRedirect)))
                    return BigInteger_TYPE;
            } else if (isCompareToBoolean(op) || op == COMPARE_EQUAL || op == COMPARE_NOT_EQUAL) {
                return boolean_TYPE;
            }
        } else if (isPrimitiveChar(leftRedirect) && isPrimitiveChar(rightRedirect)) {
            if (isCompareToBoolean(op) || op == COMPARE_EQUAL || op == COMPARE_NOT_EQUAL) {
                return boolean_TYPE;
            }
        }

        if (isShiftOperation(operationName) && isNumberCategory(leftRedirect) && isLongCategory(getUnwrapper(rightRedirect))) {
            return leftRedirect;
        }

        if (isNumberCategory(getWrapper(leftRedirect)) && isNumberCategory(getWrapper(rightRedirect))) {
            switch (op) {
              case DIVIDE:
              case DIVIDE_EQUAL:
                // divisions may produce different results depending on operand types
                if (isFloatingCategory(leftRedirect) || isFloatingCategory(rightRedirect)) {
                    if (!isPrimitiveType(leftRedirect) || !isPrimitiveType(rightRedirect)) {
                        return Double_TYPE;
                    }
                    return double_TYPE;
                }
                if (DIVIDE == op) {
                    return BigDecimal_TYPE;
                }
                // falls through
              case MOD:
              case MOD_EQUAL:
              case REMAINDER:
              case REMAINDER_EQUAL:
                return leftRedirect;
            }

            if (isOperationInGroup(op)) {
                return getGroupOperationResultType(leftRedirect, rightRedirect);
            }
        }

        return null;
    }

    /**
     * Returns the result type for grouped numeric operations such as addition.
     */
    protected static ClassNode getGroupOperationResultType(final ClassNode a, final ClassNode b) {
        if (isBigIntCategory(a) && isBigIntCategory(b)) return BigInteger_TYPE;
        if (isBigDecCategory(a) && isBigDecCategory(b)) return BigDecimal_TYPE;
        if (isBigDecimalType(a) || isBigDecimalType(b)) return BigDecimal_TYPE;
        if (isBigIntegerType(a) || isBigIntegerType(b)) {
            if (isBigIntCategory(a) && isBigIntCategory(b)) return BigInteger_TYPE;
            return BigDecimal_TYPE;
        }
        if (isPrimitiveDouble(a) || isPrimitiveDouble(b)) return double_TYPE;
        if (isWrapperDouble(a) || isWrapperDouble(b)) return Double_TYPE;
        if (isPrimitiveFloat(a) || isPrimitiveFloat(b)) return float_TYPE;
        if (isWrapperFloat(a) || isWrapperFloat(b)) return Float_TYPE;
        if (isPrimitiveLong(a) || isPrimitiveLong(b)) return long_TYPE;
        if (isWrapperLong(a) || isWrapperLong(b)) return Long_TYPE;
        if (isPrimitiveInt(a) || isPrimitiveInt(b)) return int_TYPE;
        if (isWrapperInteger(a) || isWrapperInteger(b)) return Integer_TYPE;
        if (isPrimitiveShort(a) || isPrimitiveShort(b)) return short_TYPE;
        if (isWrapperShort(a) || isWrapperShort(b)) return Short_TYPE;
        if (isPrimitiveByte(a) || isPrimitiveByte(b)) return byte_TYPE;
        if (isWrapperByte(a) || isWrapperByte(b)) return Byte_TYPE;
        if (isPrimitiveChar(a) || isPrimitiveChar(b)) return char_TYPE;
        if (isWrapperCharacter(a) || isWrapperCharacter(b)) return Character_TYPE;
        return Number_TYPE;
    }

    /**
     * GEP-20: probe the static return type of {@code rhs.getAt(IntRange)} on
     * {@code receiverType}, so the rest binder can be typed against the actual
     * slice type rather than a hard-coded {@code List<T>}. Returns the resolved
     * slice type (e.g. {@code String} for a String receiver, {@code BitSet} for
     * a BitSet receiver, the user-declared return type for custom classes), or
     * {@code null} if the receiver has no resolvable {@code getAt(IntRange)}.
     *
     * <p>Mirrors {@link #inferComponentType} but for the IntRange overload.
     * Method resolution runs under a swallowed error collector so a missing
     * overload doesn't surface as a static-type error.</p>
     */
    protected ClassNode inferRangeSliceType(final ClassNode receiverType) {
        if (receiverType == null || receiverType.equals(OBJECT_TYPE)) return null;
        // Stream has a getAt(IntRange) extension that materialises eagerly into a List —
        // antithetical to GEP-20's "no silent materialisation of unbounded sources" rule.
        // Stream RHS routes through Path A (lazy iterator wrap) instead, so exclude it
        // from Path B here regardless of getAt(IntRange) availability.
        if (GeneralUtils.isOrImplements(receiverType, STREAM_TYPE)) return null;
        if (receiverType.isArray()) {
            // Arrays don't carry a getAt(IntRange) method per se — it's a DGM extension
            // (ArrayGroovyMethods.getAt(T[], IntRange)) returning List<T>. Mirror that
            // here so we don't depend on STC catching the extension on array types.
            return GenericsUtils.makeClassSafeWithGenerics(LIST_TYPE,
                    new GenericsType(ClassHelper.getWrapper(receiverType.getComponentType())));
        }
        MethodCallExpression mce = callX(varX("#", receiverType), "getAt",
                varX("range", ClassHelper.make(IntRange.class)));
        mce.setImplicitThis(false);
        typeCheckingContext.pushErrorCollector();
        try {
            visitMethodCallExpression(mce);
        } finally {
            typeCheckingContext.popErrorCollector();
        }
        MethodNode target = mce.getNodeMetaData(DIRECT_METHOD_CALL_TARGET);
        // No matching overload, or resolved to a too-generic catch-all on Object — punt.
        if (target == null || target.getDeclaringClass().equals(OBJECT_TYPE)) return null;
        ClassNode sliceType = getType(mce);
        if (sliceType == null || sliceType.equals(OBJECT_TYPE)) return null;
        return sliceType;
    }

    /**
     * Infers the component type addressed by subscript or iterator access.
     */
    protected ClassNode inferComponentType(final ClassNode receiverType, final ClassNode subscriptType) {
        ClassNode componentType = null;
        if (receiverType.isArray()) { // GROOVY-11335
            componentType = receiverType.getComponentType();
        } else {
            MethodCallExpression mce;
            if (subscriptType != null) { // GROOVY-5521: check for a suitable "getAt(T)" method
                mce = callX(varX("#", receiverType), "getAt", varX("selector", subscriptType));
            } else { // GROOVY-8133: check for an "iterator()" method
                mce = callX(varX("#", receiverType), "iterator");
            }
            mce.setImplicitThis(false); // GROOVY-8943

            typeCheckingContext.pushErrorCollector();
            try {
                visitMethodCallExpression(mce);
            } finally {
                typeCheckingContext.popErrorCollector();
            }

            if (subscriptType != null) {
                componentType = getType(mce);
            } else {
                ClassNode iteratorType = getType(mce);
                if (isOrImplements(iteratorType, Iterator_TYPE) && (iteratorType.getGenericsTypes() != null
                        // ignore the iterator(Object) extension method, since it makes *everything* appear iterable
                        || !mce.<MethodNode>getNodeMetaData(DIRECT_METHOD_CALL_TARGET).getDeclaringClass().equals(OBJECT_TYPE))) {
                    componentType = Optional.ofNullable(iteratorType.getGenericsTypes()).map(gt -> getCombinedBoundType(gt[0])).orElse(OBJECT_TYPE);
                }
            }
        }
        return componentType;
    }

    /**
     * Finds a matching method or reports a static type error when none is available.
     */
    protected MethodNode findMethodOrFail(final Expression expr, final ClassNode receiver, final String name, final ClassNode... args) {
        List<MethodNode> methods = findMethod(receiver, name, args);
        if (methods.isEmpty() && (expr instanceof BinaryExpression be)) {
            MethodCallExpression call = callX(be.getLeftExpression(), name, be.getRightExpression());
            methods = extension.handleMissingMethod(receiver, name, args(be.getLeftExpression()), args, call);
        }
        if (methods.isEmpty()) {
            addNoMatchingMethodError(receiver, name, args, expr);
        } else {
            if (areCategoryMethodCalls(methods, name, args)) {
                addCategoryMethodCallError(expr);
            }
            methods = disambiguateMethods(methods, receiver, args, expr);
            if (methods.size() == 1) {
                return methods.get(0);
            } else {
                addAmbiguousErrorMessage(methods, name, args, expr);
            }
        }
        return null;
    }

    private List<MethodNode> disambiguateMethods(List<MethodNode> methods, final ClassNode receiver, final ClassNode[] arguments, final Expression call) {
        if (methods.size() > 1 && receiver != null && arguments != null) {
            List<MethodNode> filteredWithGenerics = new LinkedList<>();
            for (MethodNode method : methods) {
                if (typeCheckMethodsWithGenerics(receiver, arguments, method)
                        && (method.getModifiers() & Opcodes.ACC_BRIDGE) == 0) {
                    filteredWithGenerics.add(method);
                }
            }
            if (filteredWithGenerics.size() == 1) {
                return filteredWithGenerics;
            }

            methods = extension.handleAmbiguousMethods(methods, call);
        }

        if (methods.size() > 1 && call instanceof MethodCall) {
            String methodName = ((MethodCall) call).getMethodAsString();
            methods = methods.stream().filter(m -> m.getName().equals(methodName)).collect(Collectors.toList());
        }

        return methods;
    }

    /**
     * Formats a method list for diagnostics.
     */
    protected static String prettyPrintMethodList(final List<MethodNode> nodes) {
        StringBuilder sb = new StringBuilder("[");
        for (int i = 0, n = nodes.size(); i < n; i += 1) {
            MethodNode node = nodes.get(i);
            sb.append(prettyPrintType(node.getReturnType()));
            sb.append(" ");
            sb.append(prettyPrintTypeName(node.getDeclaringClass()));
            sb.append("#");
            sb.append(toMethodParametersString(node.getName(), extractTypesFromParameters(node.getParameters())));
            if (i < n - 1) sb.append(", ");
        }
        sb.append("]");
        return sb.toString();
    }

    /**
     * Indicates whether all candidates are category-style extension methods.
     */
    protected boolean areCategoryMethodCalls(final List<MethodNode> foundMethods, final String name, final ClassNode[] args) {
        boolean category = false;
        if ("use".equals(name) && args != null && args.length >= 2 && args[args.length - 1].equals(CLOSURE_TYPE)) {
            category = true;
            for (MethodNode method : foundMethods) {
                if (!isDefaultExtension(method)) {
                    category = false;
                    break;
                }
            }
        }
        return category;
    }

    /**
     * Returns methods defined for the specified receiver and adds "non-existing"
     * methods that will be generated afterwards by the compiler; for example if
     * a method is using default values and the class node is not compiled yet.
     *
     * @param receiver the type to search for methods
     * @param name     the name of the methods to return
     * @return the methods that are defined on the receiver completed with stubs for future methods
     */
    protected List<MethodNode> findMethodsWithGenerated(final ClassNode receiver, final String name) {
        if (receiver.isArray()) {
            if (name.equals("clone")) { // GROOVY-10319: array clone -- <https://docs.oracle.com/javase/specs/jls/se8/html/jls-10.html#jls-10.7>
                MethodNode clone = new MethodNode("clone", Opcodes.ACC_PUBLIC, OBJECT_TYPE, Parameter.EMPTY_ARRAY, ClassNode.EMPTY_ARRAY, null);
                clone.setDeclaringClass(OBJECT_TYPE); // retain Object for declaringClass and returnType
                clone.setNodeMetaData(INFERRED_RETURN_TYPE, receiver);
                return Collections.singletonList(clone);
            } else {
                return OBJECT_TYPE.getMethods(name);
            }
        }

        List<MethodNode> methods = receiver.getMethods(name);

        // GROOVY-5166, GROOVY-9890, GROOVY-10700: non-static interface/trait methods
        Set<ClassNode> done = new HashSet<>();
        for (ClassNode next = receiver; next != null; next = next.getSuperClass()) {
            done.add(next);
            for (ClassNode face : next.getAllInterfaces()) {
                if (done.add(face)) {
                    for (MethodNode mn : face.getDeclaredMethods(name)) {
                        if (mn.isPublic() && !mn.isStatic()) methods.add(mn);
                    }
                }
            }
        }

        if (receiver.isInterface()) {
            methods.addAll(OBJECT_TYPE.getMethods(name));
        }

        if (!receiver.isResolved() && !methods.isEmpty()) {
            methods = withDefaultArgumentMethods(methods);
        }

        return methods;
    }

    /**
     * Finds candidate methods matching the supplied receiver, name, and argument types.
     */
    protected List<MethodNode> findMethod(ClassNode receiver, final String name, final ClassNode... args) {
        if (isPrimitiveType(receiver)) receiver = getWrapper(receiver);

        List<MethodNode> methods;
        if ("<init>".equals(name)) {
            if (receiver.isInterface()) {
                methods = EMPTY_METHODNODE_LIST;
            } else {
                methods = withDefaultArgumentMethods(receiver.getDeclaredConstructors());
                if (methods.isEmpty()) {
                    Parameter[] parameters = receiver.isArray() ? new Parameter[]{new Parameter(int_TYPE, "array_length")} : Parameter.EMPTY_ARRAY;
                    var defaultConstructor = new ConstructorNode(Opcodes.ACC_PUBLIC, parameters, ClassNode.EMPTY_ARRAY, GENERATED_EMPTY_STATEMENT);
                    defaultConstructor.setDeclaringClass(receiver);
                    methods.add(defaultConstructor);
                }
            }
        } else {
            methods = findMethodsWithGenerated(receiver, name);
            if ("call".equals(name) && receiver.isInterface()) {
                MethodNode sam = findSAM(receiver);
                if (sam != null && !"call".equals(sam.getName())) {
                    MethodNode callMethod = new MethodNode("call", sam.getModifiers(), sam.getReturnType(), sam.getParameters(), sam.getExceptions(), sam.getCode());
                    callMethod.setDeclaringClass(sam.getDeclaringClass());
                    callMethod.setSourcePosition(sam);
                    callMethod.setSynthetic(true);
                    methods.add(callMethod);
                }
            }
            if (typeCheckingContext.getEnclosingClassNodes().contains(receiver)) {
                boolean staticOnly = Modifier.isStatic(receiver.getModifiers());
                for (ClassNode outer = receiver; (outer = outer.getOuterClass()) != null;
                        staticOnly = staticOnly || Modifier.isStatic(outer.getModifiers())) {
                    methods.addAll(allowStaticAccessToMember(findMethodsWithGenerated(outer, name), staticOnly));
                }
            }
            if (args == null || args.length == 0) {
                // check for property accessor
                String pname = extractPropertyNameFromMethodName("get", name);
                if (pname == null) {
                    pname = extractPropertyNameFromMethodName("is", name);
                }
                PropertyNode property = null;
                if (pname != null) {
                    property = findProperty(receiver, pname);
                } else {
                    out: // look for property via getGetterName() for non-canonical case
                    for (ClassNode cn = receiver; cn != null; cn = cn.getSuperClass()) {
                        for (PropertyNode pn : cn.getProperties()) {
                            if (name.equals(pn.getGetterName())) {
                                property = pn;
                                break out;
                            }
                        }
                    }
                }
                if (property != null && property.getDeclaringClass().getGetterMethod(name) == null) {
                    MethodNode node = new MethodNode(name, Opcodes.ACC_PUBLIC | (property.isStatic() ? Opcodes.ACC_STATIC : 0),
                            property.getOriginType(), Parameter.EMPTY_ARRAY, ClassNode.EMPTY_ARRAY, GENERATED_EMPTY_STATEMENT);
                    node.setDeclaringClass(property.getDeclaringClass());
                    node.setSynthetic(true);
                    methods.add(node);
                }
            } else if (args.length == 1 && (methods.isEmpty()
                    || methods.stream().allMatch(MethodNode::isAbstract))) { // GROOVY-10922
                // check for property mutator
                String pname = extractPropertyNameFromMethodName("set", name);
                if (pname != null) {
                    PropertyNode property = findProperty(receiver, pname);
                    if (property != null && !property.isFinal()) {
                        ClassNode type = property.getOriginType();
                        // GROOVY-12229: the synthetic mutator accepts whatever the property
                        // assignment form accepts, including implicit conversions like GString-String
                        if (isAssignableTo(wrapTypeIfNecessary(args[0]), wrapTypeIfNecessary(type))) {
                            MethodNode node = new MethodNode(name, Opcodes.ACC_PUBLIC | (property.isStatic() ? Opcodes.ACC_STATIC : 0),
                                    VOID_TYPE, new Parameter[]{new Parameter(type, name)}, ClassNode.EMPTY_ARRAY, GENERATED_EMPTY_STATEMENT);
                            node.setDeclaringClass(property.getDeclaringClass());
                            node.setSynthetic(true);
                            methods.add(node);
                        }
                    }
                }
            }
        }

        if (!name.endsWith("init>")) { // also search for extension methods
            methods.addAll(findDGMMethodsForClassNode(getSourceUnit().getClassLoader(), receiver, name));
        }
        methods = filterMethodsByVisibility(methods, typeCheckingContext.getEnclosingClassNode());
        List<MethodNode> chosen = chooseBestMethod(receiver, methods, args);
        if (!chosen.isEmpty()) return chosen;

        // GROOVY-5566
        if (receiver instanceof InnerClassNode && ((InnerClassNode) receiver).isAnonymous() && methods.size() == 1 && args != null && "<init>".equals(name)) {
            MethodNode constructor = methods.get(0);
            if (constructor.getParameters().length == args.length) {
                return methods;
            }
        }

        if (isClassClassNodeWrappingConcreteType(receiver)) { // GROOVY-6802, GROOVY-6803, GROOVY-9415
            List<MethodNode> result = findMethod(receiver.getGenericsTypes()[0].getType(), name, args);
            result = allowStaticAccessToMember(result, true); // GROOVY-11427
            if (!result.isEmpty()) return result;
        }

        if (isGStringType(receiver)) {
            return findMethod(STRING_TYPE, name, args);
        }

        return EMPTY_METHODNODE_LIST;
    }

    private PropertyNode findProperty(final ClassNode receiver, final String name) {
        for (ClassNode cn = receiver; cn != null; cn = cn.getSuperClass()) {
            PropertyNode property = cn.getProperty(name);
            if (property != null) return property;

            if (!cn.isStaticClass() && cn.getOuterClass() != null
                    && typeCheckingContext.getEnclosingClassNodes().contains(cn)) {
                ClassNode outer = cn.getOuterClass();
                do {
                    property = outer.getProperty(name);
                    if (property != null) return property;
                } while (!outer.isStaticClass() && (outer = outer.getOuterClass()) != null);
            }
        }
        return null;
    }

    /**
     * Given a method name and a prefix, returns the name of the property that should be looked up,
     * following the java beans rules. For example, "getName" would return "name", while
     * "getFullName" would return "fullName".
     * If the prefix is not found, returns null.
     *
     * @param prefix     the method name prefix ("get", "is", "set", ...)
     * @param methodName the method name
     * @return a property name if the prefix is found and the method matches the java beans rules, null otherwise
     */
    public static String extractPropertyNameFromMethodName(final String prefix, final String methodName) {
        if (prefix == null || methodName == null) return null;
        if (methodName.startsWith(prefix) && prefix.length() < methodName.length()) {
            String result = methodName.substring(prefix.length());
            String propertyName = decapitalize(result);
            if (result.equals(capitalize(propertyName))) return propertyName;
        }
        return null;
    }

    /**
     * Returns the inferred type for the supplied AST node.
     */
    protected ClassNode getType(final ASTNode node) {
        ClassNode type = node.getNodeMetaData(INFERRED_TYPE);
        if (type != null) {
            return type;
        }

        if (node instanceof ClassExpression) {
            type = ((ClassExpression) node).getType();
            return GenericsUtils.makeClassSafe0(CLASS_Type, new GenericsType(type));
        }

        if (node instanceof VariableExpression vexp) {
            type = isTraitSelf(vexp);
            if (type != null) return makeSelf(type);
            if (vexp.isThisExpression()) return makeThis();
            if (vexp.isSuperExpression()) return makeSuper();

            Variable variable = vexp.getAccessedVariable();
            if (variable instanceof FieldNode fieldNode) {
                ClassNode fieldType = fieldNode.getOriginType();
                if (!fieldNode.isStatic() && GenericsUtils.hasUnresolvedGenerics(fieldType)) {
                    ClassNode declType = fieldNode.getDeclaringClass(), thisType = typeCheckingContext.getEnclosingClassNode();
                    fieldType = resolveGenericsWithContext(extractPlaceHolders(thisType, declType), fieldType);
                }
                return fieldType;
            }
            if (variable != vexp && variable instanceof VariableExpression) {
                return getType((VariableExpression) variable);
            }
            if (variable instanceof Parameter parameter) {
                if (getTemporaryTypesForExpression(vexp).isEmpty()) { // not instanceof
                    // check if the parameter is part of a control structure (for loop)
                    type = typeCheckingContext.controlStructureVariables.get(parameter);
                    if (type == null) { // else check closure metadata
                        type = getTypeFromClosureArguments(parameter);
                    }
                }
                if (type != null) {
                    storeType(vexp, type);
                    return type;
                }
                return getType(parameter);
            }
            return vexp.getOriginType();
        }

        if (node instanceof Parameter || node instanceof FieldNode || node instanceof PropertyNode) {
            return ((Variable) node).getOriginType();
        }

        if (node instanceof MethodNode) {
            type = ((MethodNode) node).getReturnType();
            return Optional.ofNullable(getInferredReturnType(node)).orElse(type);
        }

        if (node instanceof MethodCall) {
            if (node instanceof ConstructorCallExpression) {
                return ((ConstructorCallExpression) node).getType();
            }
            MethodNode target = node.getNodeMetaData(DIRECT_METHOD_CALL_TARGET);
            if (target != null) {
                return getType(target);
            }
        }

        if (node instanceof ClosureExpression) {
            type = CLOSURE_TYPE.getPlainNodeReference();
            ClassNode returnType = getInferredReturnType(node);
            if (returnType != null) {
                type.setGenericsTypes(new GenericsType[]{
                    new GenericsType(wrapTypeIfNecessary(returnType))
                });
            }
            Parameter[] parameters = ((ClosureExpression) node).getParameters();
            int nParameters = parameters == null ? 0
               : parameters.length == 0 ? -1 : parameters.length;
            type.putNodeMetaData(CLOSURE_ARGUMENTS, nParameters);
            return type;
        }

        if (node instanceof ListExpression) {
            type = inferListExpressionType((ListExpression) node);
            node.putNodeMetaData(INFERRED_TYPE, type);
            return type;
        }

        if (node instanceof MapExpression) {
            type = inferMapExpressionType((MapExpression) node);
            node.putNodeMetaData(INFERRED_TYPE, type);
            return type;
        }

        if (node instanceof RangeExpression re) {
            ClassNode fromType = getType(re.getFrom());
            ClassNode toType = getType(re.getTo());
            if (fromType.equals(toType)) {
                type = wrapTypeIfNecessary(fromType);
            } else {
                type = wrapTypeIfNecessary(lowestUpperBound(fromType, toType));
            }
            return GenericsUtils.makeClassSafe0(RANGE_TYPE, new GenericsType(type));
        }

        if (node instanceof SpreadExpression) {
            type = getType(((SpreadExpression) node).getExpression());
            type = inferComponentType(type, null); // for list literal
            if (type == null) // GROOVY-11572: not an iterable
                type = UNKNOWN_PARAMETER_TYPE;
            return type;
        }

        if (node instanceof UnaryPlusExpression) {
            return getType(((UnaryPlusExpression) node).getExpression());
        }

        if (node instanceof UnaryMinusExpression) {
            return getType(((UnaryMinusExpression) node).getExpression());
        }

        if (node instanceof BitwiseNegationExpression) {
            return getType(((BitwiseNegationExpression) node).getExpression());
        }

        return ((Expression) node).getType();
    }

    private ClassNode getTypeFromClosureArguments(final Parameter parameter) {
        for (TypeCheckingContext.EnclosingClosure enclosingClosure : typeCheckingContext.getEnclosingClosureStack()) {
            ClosureExpression closureExpression = enclosingClosure.getClosureExpression();
            ClassNode[] closureParamTypes = closureExpression.getNodeMetaData(CLOSURE_ARGUMENTS);
            if (closureParamTypes != null) {
                Parameter[] parameters = closureExpression.getParameters();
                if (parameters != null) {
                    final int n = parameters.length;
                    String parameterName = parameter.getName();
                    if (n == 0 && parameterName.equals("it")) {
                        return closureParamTypes.length > 0 ? closureParamTypes[0] : null;
                    }
                    for (int i = 0; i < n; i += 1) {
                        if (parameterName.equals(parameters[i].getName())) {
                            return closureParamTypes.length > i ? closureParamTypes[i] : null;
                        }
                    }
                }
            }
        }
        return null;
    }

    private static ClassNode makeSelf(final ClassNode trait) {
        Set<ClassNode> selfTypes = Traits.collectSelfTypes(trait, new LinkedHashSet<>());
        if (!selfTypes.isEmpty()) {
            selfTypes.add(trait);
            return newIntersectionTypeClassNode(selfTypes);
        }
        return trait;
    }

    private ClassNode makeSuper() {
        return makeType(typeCheckingContext.getEnclosingClassNode().getUnresolvedSuperClass(), typeCheckingContext.isInStaticContext);
    }

    private ClassNode makeThis() {
        return makeType(typeCheckingContext.getEnclosingClassNode(), typeCheckingContext.isInStaticContext);
    }

    /**
     * Wrap type in Class&lt;&gt; if usingClass==true.
     */
    private static ClassNode makeType(final ClassNode cn, final boolean usingClass) {
        if (usingClass) {
            ClassNode clazzType = CLASS_Type.getPlainNodeReference();
            clazzType.setGenericsTypes(new GenericsType[]{new GenericsType(cn)});
            return clazzType;
        } else {
            return cn;
        }
    }

    /**
     * Stores the inferred return type of a closure or method.  We are using a
     * separate key to store inferred return type because the inferred type of
     * a closure is {@link Closure}, which is different from the inferred type
     * of the code of the closure.
     *
     * @param node a {@link ClosureExpression} or {@link MethodNode}
     * @param type the inferred return type of the code
     * @return The old value of the inferred type.
     */
    protected ClassNode storeInferredReturnType(final ASTNode node, final ClassNode type) {
        if (node instanceof ClosureExpression) {
            if (node.getNodeMetaData(INFERRED_TYPE) != null) {
                return getInferredReturnType(node); // GROOVY-11079
            } else {
                return (ClassNode) node.putNodeMetaData(INFERRED_RETURN_TYPE, type);
            }
        }
        throw new IllegalArgumentException("Storing inferred return type is only allowed on closures but found " + node.getClass());
    }

    /**
     * Returns the inferred return type of a closure or method, if stored on the
     * AST node. This method doesn't perform any type inference by itself.
     *
     * @param node a {@link ClosureExpression} or {@link MethodNode}
     * @return The expected return type.
     */
    protected ClassNode getInferredReturnType(final ASTNode node) {
        return node.getNodeMetaData(INFERRED_RETURN_TYPE);
    }

    /**
     * Indicates whether the expression is the {@code null} constant.
     */
    protected static boolean isNullConstant(final Expression expression) {
        return expression instanceof ConstantExpression && ((ConstantExpression) expression).isNullExpression();
    }

    /**
     * Indicates whether the expression represents {@code this}.
     */
    protected static boolean isThisExpression(final Expression expression) {
        return expression instanceof VariableExpression && ((VariableExpression) expression).isThisExpression();
    }

    /**
     * Indicates whether the expression represents {@code super}.
     */
    protected static boolean isSuperExpression(final Expression expression) {
        if (expression instanceof PropertyExpression) { // GROOVY-11256
            return "super".equals(((PropertyExpression) expression).getPropertyAsString())
                && ((PropertyExpression) expression).getObjectExpression() instanceof ClassExpression;
        }
        return expression instanceof VariableExpression && ((VariableExpression) expression).isSuperExpression();
    }

    /**
     * Infers the static type of a list literal.
     */
    protected ClassNode inferListExpressionType(final ListExpression list) {
        ClassNode listType = list.getType();

        GenericsType[] genericsTypes = listType.getGenericsTypes();
        if (!asBoolean(genericsTypes)
                || (genericsTypes.length == 1 && genericsTypes[0].isPlaceholder())) {
            // maybe infer the element type
            List<ClassNode> expressionTypes = list.getExpressions().stream()
                .filter(e -> !isNullConstant(e)).map(this::getType).collect(Collectors.toList());
            if (!expressionTypes.isEmpty()) {
                ClassNode subType = lowestUpperBound(expressionTypes);
                genericsTypes = new GenericsType[]{new GenericsType(wrapTypeIfNecessary(subType))};
            } else { // GROOVY-11028
                GenericsType[] typeVars = ArrayList_TYPE.getGenericsTypes();
                Map<GenericsTypeName, GenericsType> spec = extractGenericsConnectionsFromArguments(
                    typeVars, Parameter.EMPTY_ARRAY, ArgumentListExpression.EMPTY_ARGUMENTS, null);
                genericsTypes = applyGenericsContext(spec, typeVars);
            }
            listType = ArrayList_TYPE.getPlainNodeReference();
            listType.setGenericsTypes(genericsTypes);
        }

        return listType;
    }

    /**
     * Infers the static type of a map literal.
     */
    protected ClassNode inferMapExpressionType(final MapExpression map) {
        ClassNode mapType = map.getType();

        GenericsType[] genericsTypes = mapType.getGenericsTypes();
        if (!asBoolean(genericsTypes)
                || (genericsTypes.length == 2 && genericsTypes[0].isPlaceholder() && genericsTypes[1].isPlaceholder())) {
            // maybe infer the entry type
            List<MapEntryExpression> entryExpressions = map.getMapEntryExpressions();
            int nExpressions = entryExpressions.size();
            if (nExpressions != 0) {
                ClassNode keyType;
                ClassNode valueType;
                List<ClassNode> keyTypes = new ArrayList<>(nExpressions);
                List<ClassNode> valueTypes = new ArrayList<>(nExpressions);
                for (MapEntryExpression entryExpression : entryExpressions) {
                    valueType = getType(entryExpression.getValueExpression());
                    if (!(entryExpression.getKeyExpression() instanceof SpreadMapExpression)) {
                        keyType = getType(entryExpression.getKeyExpression());
                    } else { // GROOVY-7247
                        valueType = GenericsUtils.parameterizeType(valueType, MAP_TYPE);
                        keyType = getCombinedBoundType(valueType.getGenericsTypes()[0]);
                        valueType = getCombinedBoundType(valueType.getGenericsTypes()[1]);
                    }
                    keyTypes.add(keyType);
                    valueTypes.add(valueType); // TODO: skip null value
                }
                keyType = lowestUpperBound(keyTypes);
                valueType = lowestUpperBound(valueTypes);
                genericsTypes = new GenericsType[]{new GenericsType(wrapTypeIfNecessary(keyType)), new GenericsType(wrapTypeIfNecessary(valueType))};
            } else { // GROOVY-11028
                GenericsType[] typeVars = LinkedHashMap_TYPE.getGenericsTypes();
                Map<GenericsTypeName, GenericsType> spec = extractGenericsConnectionsFromArguments(
                    typeVars, Parameter.EMPTY_ARRAY, ArgumentListExpression.EMPTY_ARGUMENTS, null);
                genericsTypes = applyGenericsContext(spec, typeVars);
            }
            mapType = LinkedHashMap_TYPE.getPlainNodeReference();
            mapType.setGenericsTypes(genericsTypes);
        }

        return mapType;
    }

    /**
     * If a method call returns a parameterized type, then perform additional
     * inference on the return type, so that the type gets actual type arguments.
     * For example, the method {@code Arrays.asList(T...)} is parameterized with
     * {@code T}, which can be deduced type arguments or call arguments.
     *
     * @param method            the method node
     * @param arguments         the method call arguments
     * @param receiver          the object expression type
     */
    protected ClassNode inferReturnTypeGenerics(final ClassNode receiver, final MethodNode method, final Expression arguments) {
        return inferReturnTypeGenerics(receiver, method, arguments, null);
    }

    /**
     * If a method call returns a parameterized type, then perform additional
     * inference on the return type, so that the type gets actual type arguments.
     * For example, the method {@code Arrays.asList(T...)} is parameterized with
     * {@code T}, which can be deduced type arguments or call arguments.
     *
     * @param method            the method node
     * @param arguments         the method call arguments
     * @param receiver          the object expression type
     * @param explicitTypeHints type arguments (optional), for example {@code Collections.<String>emptyList()}
     */
    protected ClassNode inferReturnTypeGenerics(final ClassNode receiver, final MethodNode method, final Expression arguments, final GenericsType[] explicitTypeHints) {
        ClassNode returnType = method.isConstructor() ? method.getDeclaringClass() : method.getReturnType();
        if (!GenericsUtils.hasUnresolvedGenerics(returnType)) {
            // GROOVY-7538: replace "Type<?>" with "Type<? extends/super X>" for any "Type<T extends/super X>"
            if (getGenericsWithoutArray(returnType) != null) returnType = boundUnboundedWildcards(returnType);

            return returnType;
        }

        if (method instanceof ExtensionMethodNode) {
            ArgumentListExpression args = getExtensionArguments(receiver, method, arguments);
            MethodNode extension = ((ExtensionMethodNode) method).getExtensionMethodNode();
            return inferReturnTypeGenerics(receiver, extension, args, explicitTypeHints);
        }

        Map<GenericsTypeName, GenericsType> context = method.isStatic() || method.isConstructor()
                                            ? null : extractPlaceHoldersVisibleToDeclaration(receiver, method, arguments);
        GenericsType[] methodGenericTypes = method.isConstructor() ? method.getDeclaringClass().getGenericsTypes() : applyGenericsContext(context, method.getGenericsTypes());

        // 1) resolve type parameters of method

        if (methodGenericTypes != null) {
            Map<GenericsTypeName, GenericsType> resolvedPlaceholders = new HashMap<>();
            for (GenericsType gt : methodGenericTypes) resolvedPlaceholders.put(new GenericsTypeName(gt.getName()), gt);
            applyGenericsConnections(extractGenericsConnectionsFromArguments(methodGenericTypes, Arrays.stream(method.getParameters()).map(param ->
                new Parameter(applyGenericsContext(context, param.getType()), param.getName())
            ).toArray(Parameter[]::new), arguments, explicitTypeHints), resolvedPlaceholders);

            returnType = applyGenericsContext(resolvedPlaceholders, returnType);
        }

        // 2) resolve type parameters of method's enclosing context

        if (context != null) {
            returnType = applyGenericsContext(context, returnType);

            if (receiver.getGenericsTypes() == null && receiver.redirect().getGenericsTypes() != null
                    && !receiver.isGenericsPlaceHolder() && GenericsUtils.hasUnresolvedGenerics(returnType)) {
                returnType = returnType.getPlainNodeReference(); // GROOVY-10049: not "Stream<E>" for raw type
            }
        }

        // 3) resolve bounds of type parameters from calling context

        returnType = applyGenericsContext(extractGenericsParameterMapOfThis(typeCheckingContext), returnType);

        return returnType;
    }

    /**
     * Resolves type parameters declared by method from type or call arguments.
     */
    private Map<GenericsTypeName, GenericsType> extractGenericsConnectionsFromArguments(final GenericsType[] methodGenericTypes, final Parameter[] parameters, final Expression arguments, final GenericsType[] explicitTypeHints) {
        Map<GenericsTypeName, GenericsType> resolvedPlaceholders = new HashMap<>();

        if (asBoolean(explicitTypeHints)) { // resolve type parameters from type arguments
            int n = methodGenericTypes.length;
            if (n == explicitTypeHints.length) {
                for (int i = 0; i < n; i += 1) {
                    resolvedPlaceholders.put(new GenericsTypeName(methodGenericTypes[i].getName()), explicitTypeHints[i]);
                }
            }
        } else if (parameters.length > 0) { // resolve type parameters from call arguments
            List<Expression> expressions = InvocationWriter.makeArgumentList(arguments).getExpressions();
            boolean isVargs = isVargs(parameters);
            int nArguments = expressions.size();
            int nParams = parameters.length;

            if (isVargs ? nArguments >= nParams - 1 : nArguments == nParams) {
                for (int i = 0; i < nArguments; i += 1) {
                    Expression argument = expressions.get(i);
                    if (isNullConstant(argument)) continue; // GROOVY-9984: skip
                    ClassNode argumentType = getDeclaredOrInferredType(argument);
                    ClassNode paramType = parameters[Math.min(i, nParams - 1)].getType();

                    if (GenericsUtils.hasUnresolvedGenerics(paramType)) {
                        // if supplying array param with multiple arguments or single non-array argument, infer using element type
                        if (isVargs && (i >= nParams || (i == nParams - 1 && (nArguments > nParams || !argumentType.isArray())))) {
                            paramType = paramType.getComponentType();
                        }

                        Map<GenericsTypeName, GenericsType> connections = new HashMap<>();

                        if ((argument instanceof ClosureExpression || argument instanceof MethodPointerExpression) && isSAMType(paramType)) {
                            // target type information
                            Tuple2<ClassNode[], ClassNode> samParamsAndReturnType = GenericsUtils.parameterizeSAM(paramType);
                            ClassNode[] q = samParamsAndReturnType.getV1();

                            // source type information
                            ClassNode returnType = isClosureWithType(argumentType)
                                    ? getCombinedBoundType(argumentType.getGenericsTypes()[0])
                                        : wrapTypeIfNecessary(getInferredReturnType(argument));
                            ClassNode[] p;
                            if (argument instanceof ClosureExpression closure) {
                                p = extractTypesFromParameters(getParametersSafe(closure));
                            } else { // argument instanceof MethodPointerExpression
                                List<MethodNode> candidates = argument.getNodeMetaData(MethodNode.class);
                                if (candidates != null && !candidates.isEmpty()) {
                                    MethodPointerExpression methodPointer = (MethodPointerExpression) argument;
                                    p = collateMethodReferenceParameterTypes(methodPointer, candidates.get(0));
                                    if (p.length > 0 && GenericsUtils.hasUnresolvedGenerics(returnType)) {
                                        // GROOVY-11241: implicit receiver for "Optional::get" is resolved
                                        if (!candidates.get(0).isStatic() && isClassClassNodeWrappingConcreteType(getType(methodPointer.getExpression()))) {
                                            extractGenericsConnections(connections, q[0], p[0].redirect());
                                        }
                                        for (int j = 0; j < q.length; j += 1) {
                                            // SAM parameters are like arguments in this case
                                            extractGenericsConnections(connections, q[j], p[j]);
                                        }
                                        // convert the method's generics into the SAM's generics
                                        returnType = applyGenericsContext(connections, returnType);
                                        p          = applyGenericsContext(connections, p         );

                                        connections.clear();
                                    }
                                } else {
                                    p = ClassNode.EMPTY_ARRAY;
                                }
                            }

                            // parameters and return type correspond to the SAM's
                            for (int j = 0; j < p.length && j < q.length; j += 1) {
                                if (!isDynamicTyped(p[j]))
                                    // GROOVY-10054, GROOVY-10699, GROOVY-10749, et al.
                                    extractGenericsConnections(connections, wrapTypeIfNecessary(p[j]), q[j]);
                            }
                            extractGenericsConnections(connections, returnType, samParamsAndReturnType.getV2());
                        } else {
                            extractGenericsConnections(connections, applyKnownTypeArgs(argumentType, paramType), paramType);
                        }

                        connections.forEach((gtn, gt) -> resolvedPlaceholders.merge(gtn, gt, (gt1, gt2) -> {
                            // GROOVY-10339, GROOVY-11616: incorporate another type witness
                            return getCombinedGenericsType(gt1, gt2);
                        }));
                    }
                }
            }

            // in case of "<T, U extends Type<T>>", we can learn about "T" from a resolved "U"
            extractGenericsConnectionsForBoundTypes(methodGenericTypes, resolvedPlaceholders);
        }

        for (GenericsType gt : methodGenericTypes) {
            // GROOVY-8409, GROOVY-10067, et al.: provide "no type witness" mapping for param
            resolvedPlaceholders.computeIfAbsent(new GenericsTypeName(gt.getName()), gtn -> {
                ClassNode hash = ClassHelper.makeWithoutCaching("#" + gt.getName());
                hash.setRedirect(getCombinedBoundType(gt));
                hash.setGenericsPlaceHolder(true);

                GenericsType gtx = new GenericsType(hash, applyGenericsContext(resolvedPlaceholders, gt.getUpperBounds()), null);
                gtx.putNodeMetaData(GenericsType.class, gt);
                gtx.setResolved(true);
                return gtx;
            });
        }

        return resolvedPlaceholders;
    }

    /**
     * For an expression like "list.stream().collect(Collectors.toSet())" the
     * return type is dependent on the return type of {@code toSet} which can
     * be partially resolved from the receiver type {@code Stream<SomeType>}.
     * <p>
     * {@code Collector<#T,?,Set<#T>> -> Collector<SomeType,?,Set<SomeType>>}
     *
     * @see #adjustForTargetType(ClassNode,ClassNode)
     */
    private static ClassNode applyKnownTypeArgs(final ClassNode sourceType, final ClassNode targetType) {
        ClassNode resultType = wrapTypeIfNecessary(sourceType);

        if (!targetType.isGenericsPlaceHolder()) {
            // GROOVY-11615: receiver type arguments from target type
            var spec = new HashMap<GenericsTypeName, GenericsType>();
            extractGenericsConnections(spec, targetType, sourceType);

            for (var iter = spec.entrySet().iterator(); iter.hasNext(); ) {
                var entry = iter.next();
                var value = entry.getValue();
                if (!entry.getKey().getName().startsWith("#") // type that requires target information
                        || GenericsUtils.hasUnresolvedGenerics(GenericsUtils.makeClassSafe0(CLASS_Type, value))) {
                    iter.remove();
                } else if (value.getLowerBound() != null) {
                    entry.setValue(new GenericsType(value.getLowerBound()));
                }
            }

            resultType = applyGenericsContext(spec, resultType);
        }

        return resultType;
    }

    /**
     * Given method call like "m(Collections.emptyList())", the type of the call
     * argument is {@code List<T>} without explicit type arguments. Now, knowing
     * the method target of "m", {@code T} could be resolved.
     */
    private void resolvePlaceholdersFromImplicitTypeHints(final ClassNode[] actuals, final ArgumentListExpression argumentList, final Parameter[] parameterArray) {
        int np = parameterArray.length;
        for (int i = 0, n = actuals.length; np > 0 && i < n; i += 1) {
            Parameter p = parameterArray[Math.min(i, np - 1)];
            ClassNode pt = p.getOriginType(), at = actuals[i];
            if (!isUsingGenericsOrIsArrayUsingGenerics(pt)) continue;
            if (i >= (np - 1) && pt.isArray() && !at.isArray()) pt = pt.getComponentType();

            var a = argumentList.getExpression(i);

            if (a instanceof ListExpression) {
                actuals[i] = getLiteralResultType(pt, at, ArrayList_TYPE);
            } else if (a instanceof MapExpression) {
                actuals[i] = getLiteralResultType(pt, at, LinkedHashMap_TYPE);
            } else if (a instanceof ConstructorCallExpression) {
                inferDiamondType((ConstructorCallExpression) a, pt); // GROOVY-8974, GROOVY-9983, GROOVY-10086, GROOVY-10890, et al.
            } else if (a instanceof TernaryExpression && at.getGenericsTypes() != null && at.getGenericsTypes().length == 0) {
                // GROOVY-9983: double diamond scenario -- "m(flag ? new Type<>(...) : new Type<>(...))"
                typeCheckingContext.pushEnclosingBinaryExpression(assignX(varX(p), a, a));
                a.visit(this); // re-visit with target type witness
                typeCheckingContext.popEnclosingBinaryExpression();
                actuals[i] = getType(a);
            }

            // check for method call without type arguments, with a known target
            if (!(a instanceof MethodCall) || (a instanceof MethodCallExpression
                    && ((MethodCallExpression) a).isUsingGenerics())) continue;
            MethodNode aNode = a.getNodeMetaData(DIRECT_METHOD_CALL_TARGET);
            if (aNode == null || aNode.getGenericsTypes() == null) continue;

            // and unknown generics
            if (!GenericsUtils.hasUnresolvedGenerics(at)) continue;

            while (!at.equals(pt) && !isObjectType(at) && !isGenericsPlaceHolderOrArrayOf(at) && !isGenericsPlaceHolderOrArrayOf(pt)) {
                at = applyGenericsContext(GenericsUtils.extractPlaceholders(at), getNextSuperClass(at, pt));
            }

            // try to resolve placeholder(s) in argument type using parameter type

            Map<GenericsTypeName, GenericsType> linked = new HashMap<>();
            Map<GenericsTypeName, GenericsType> source = GenericsUtils.extractPlaceholders(at);
            Map<GenericsTypeName, GenericsType> target = GenericsUtils.extractPlaceholders(pt);

            if (at.isGenericsPlaceHolder()) // GROOVY-10482: call argument via "def <T> T m()"
                target.put(new GenericsTypeName(at.getUnresolvedName()), pt.asGenericsType());

            // connect E:T from source to E:Type from target
            for (GenericsType placeholder : aNode.getGenericsTypes()) {
                for (Map.Entry<GenericsTypeName, GenericsType> e : source.entrySet()) {
                    if (e.getValue().getNodeMetaData(GenericsType.class) == placeholder) {
                        Optional.ofNullable(target.get(e.getKey()))
                            // skip "f(g())" for "f(T<String>)" and "<U extends Number> U g()"
                            .filter(gt -> isAssignableTo(gt.getType(), placeholder.getType()))
                            .ifPresent(gt -> linked.put(new GenericsTypeName(e.getValue().getName()), gt));
                        break;
                    }
                }
            }

            actuals[i] = applyGenericsContext(linked, at);
        }
    }

    private static void extractGenericsConnectionsForBoundTypes(final GenericsType[] spec, final Map<GenericsTypeName, GenericsType> target) {
        if (spec.length < 2) return;
        Map<GenericsTypeName, GenericsType> outer = new HashMap<>();
        for (GenericsType tp : spec) {
            ClassNode[] bounds = tp.getUpperBounds();
            if (bounds == null || bounds.length == 0) continue;

            GenericsTypeName key = new GenericsTypeName(tp.getName());
            GenericsType value = target.get(key); // look for specific resolved type
            if (value == null || value.isPlaceholder() || value.isWildcard()) continue;

            Map<GenericsTypeName, GenericsType> inner = new HashMap<>();
            for (ClassNode bound : bounds) {
                extractGenericsConnections(inner,value.getType(),bound);
            }
            inner.forEach((k, v) -> outer.merge(k, v, StaticTypeCheckingSupport::getCombinedGenericsType)); // GROOVY-5893
        }
        outer.forEach(target::putIfAbsent);
    }

    private static ClassNode[] collateMethodReferenceParameterTypes(final MethodPointerExpression source, final MethodNode target) {
        Parameter[] params;

        if (target instanceof ExtensionMethodNode && !((ExtensionMethodNode) target).isStaticExtension()) {
            params = ((ExtensionMethodNode) target).getExtensionMethodNode().getParameters();
        } else if (!target.isStatic() && source.getExpression() instanceof ClassExpression) {
            ClassNode thisType = source.getExpression().getType();
            // there is an implicit parameter for "String::length"
            int n = target.getParameters().length;
            params = new Parameter[n + 1];
            params[0] = new Parameter(thisType, "");
            System.arraycopy(target.getParameters(), 0, params, 1, n);
        } else {
            params = target.getParameters();
            // GROOVY-10974, GROOVY-10975: propagate type args
            if (!target.isStatic() && target.getDeclaringClass().getGenericsTypes() != null) {
                ClassNode objectExprType = source.getExpression().getNodeMetaData(INFERRED_TYPE);
                if (objectExprType == null && source.getExpression() instanceof VariableExpression) {
                    Variable variable = ((VariableExpression) source.getExpression()).getAccessedVariable();
                    if (variable instanceof ASTNode) objectExprType = ((ASTNode) variable).getNodeMetaData(INFERRED_TYPE);
                }
                if (objectExprType == null) objectExprType = source.getExpression().getType();
                var spec = extractPlaceHolders(objectExprType, target.getDeclaringClass());
                return applyGenericsContext(spec, extractTypesFromParameters(params));
            }
        }

        return extractTypesFromParameters(params);
    }

    private ClassNode getDeclaredOrInferredType(final Expression expression) {
        ClassNode declaredOrInferred;
        // in case of "T t = new ExtendsOrImplementsT()", return T for the expression type
        if (expression instanceof Variable && !((Variable) expression).isDynamicTyped()) {
            declaredOrInferred = getOriginalDeclarationType(expression); // GROOVY-9996
        } else {
            declaredOrInferred = getType(expression);
        }
        // GROOVY-10011: apply instanceof constraints to either option
        return getInferredTypeFromTempInfo(expression, declaredOrInferred);
    }

    private static class ExtensionMethodDeclaringClass {
    }

    private static ArgumentListExpression getExtensionArguments(final ClassNode receiver, final MethodNode method, final Expression arguments) {
        VariableExpression self = varX("$self", receiver); // implicit first argument
        self.putNodeMetaData(ExtensionMethodDeclaringClass.class, method.getDeclaringClass());

        ArgumentListExpression args = new ArgumentListExpression();
        args.addExpression(self);
        if (arguments instanceof TupleExpression) {
            for (Expression argument : (TupleExpression) arguments) {
                args.addExpression(argument);
            }
        } else {
            args.addExpression(arguments);
        }
        return args;
    }

    private static boolean isDefaultExtension(final MethodNode method) {
        return method instanceof ExtensionMethodNode && ((ExtensionMethodNode) method).getExtensionMethodNode().getDeclaringClass().equals(DGM_CLASSNODE);
    }

    private static boolean isGenericsPlaceHolderOrArrayOf(ClassNode cn) {
        while (cn.isArray()) cn = cn.getComponentType();
        return cn.isGenericsPlaceHolder();
    }

    private static Map<GenericsTypeName, GenericsType> extractPlaceHolders(final ClassNode receiver, final ClassNode declaringClass) {
        Map<GenericsTypeName, GenericsType> result = null;
        ClassNode[] todo;
        if (receiver instanceof UnionTypeClassNode) {
            todo = ((UnionTypeClassNode) receiver).getDelegates(); // GROOVY-7275
        } else {
            todo = new ClassNode[] {!isPrimitiveType(declaringClass) ? wrapTypeIfNecessary(receiver) : receiver};
        }
out:    for (ClassNode type : todo) {
            for (ClassNode current = type; current != null; ) {
                Map<GenericsTypeName, GenericsType> placeHolders = new HashMap<>();
                if (current.isGenericsPlaceHolder())
                    // GROOVY-10622: type param bound "T extends Set<Type>"
                    current = current.asGenericsType().getUpperBounds()[0];
                // GROOVY-10055: handle diamond or raw type
                else if (current.getGenericsTypes() != null
                        ? current.getGenericsTypes().length == 0
                        : current.redirect().getGenericsTypes() != null) {
                    for (GenericsType gt : current.redirect().getGenericsTypes()) {
                        ClassNode cn = gt.getUpperBounds() != null ? gt.getUpperBounds()[0] : gt.getType().redirect();
                        placeHolders.put(new GenericsTypeName(gt.getName()), cn.getPlainNodeReference().asGenericsType());
                    }
                }

                boolean currentIsDeclaring = current.equals(declaringClass) || isGenericsPlaceHolderOrArrayOf(declaringClass);
                if (currentIsDeclaring) {
                    extractGenericsConnections(placeHolders, current, declaringClass);
                } else {
                    GenericsUtils.extractPlaceholders(current, placeHolders);
                }

                if (result != null) { // merge maps
                    for (Map.Entry<GenericsTypeName, GenericsType> entry : placeHolders.entrySet()) {
                        GenericsType gt = entry.getValue();
                        if (!gt.isPlaceholder()) continue;
                        GenericsType referenced = result.get(new GenericsTypeName(gt.getName()));
                        if (referenced == null) continue;
                        entry.setValue(referenced);
                    }
                }
                result = placeHolders;

                // we are done if we are now in the declaring class
                if (currentIsDeclaring) break out;

                current = getNextSuperClass(current, declaringClass);
                if (current == null && isClassType(declaringClass)) {
                    // this can happen if the receiver is Class<Foo>, then
                    // the actual receiver is Foo and declaringClass is Class
                    current = declaringClass;
                } else {
                    current = applyGenericsContext(placeHolders, current);
                }
            }
        }
        if (result == null) {
            throw new GroovyBugError("Declaring class " + prettyPrintTypeName(declaringClass) + " was not matched with receiver " + prettyPrintTypeName(receiver) + ". This should not have happened!");
        }
        return result;
    }

    private static Map<GenericsTypeName, GenericsType> extractPlaceHoldersVisibleToDeclaration(final ClassNode receiver, final MethodNode method, final Expression argument) {
        Map<GenericsTypeName, GenericsType> result;
        if (method.isStatic() || (method.isConstructor() && !asBoolean(receiver.getGenericsTypes()))) {
            result = new HashMap<>();
        } else {
            ClassNode declaring = method.getDeclaringClass();
            if (argument instanceof TupleExpression) { // resolve extension method class
                List<Expression> arguments = ((TupleExpression) argument).getExpressions();
                if (!arguments.isEmpty()) {
                    ClassNode cn = arguments.get(0).getNodeMetaData(ExtensionMethodDeclaringClass.class);
                    if (cn != null)
                        declaring = cn;
                }
            }
            result = extractPlaceHolders(receiver, declaring);
            ClassNode oc = receiver.getOuterClassType();
            if (oc == null && argument instanceof VariableExpression vexp && vexp.getOriginType() != null) {
                oc = vexp.getOriginType().getOuterClassType();
            }
            if (oc != null) {
                Map<GenericsTypeName, GenericsType> outerPlaceHolders = extractPlaceHolders(oc, oc.redirect());
                outerPlaceHolders.forEach(result::putIfAbsent); // inner type parameters win over outer
            }
            if (!result.isEmpty()) Optional.ofNullable(method.getGenericsTypes()).ifPresent(methodGenerics ->
                Arrays.stream(methodGenerics).map(gt -> new GenericsTypeName(gt.getName())).forEach(result::remove)); // GROOVY-10322
        }
        return result;
    }

    /**
     * Validates a chosen method against its generic constraints and reports failures.
     */
    protected boolean typeCheckMethodsWithGenericsOrFail(final ClassNode receiver, final ClassNode[] arguments, final MethodNode candidateMethod, final Expression location) {
        if (!typeCheckMethodsWithGenerics(receiver, arguments, candidateMethod)) {
            ClassNode r = receiver;
            ClassNode[] at = arguments;
            MethodNode m = candidateMethod;
            if (candidateMethod instanceof ExtensionMethodNode) {
                m = ((ExtensionMethodNode) candidateMethod).getExtensionMethodNode();
                r = m.getDeclaringClass();
                at = new ClassNode[arguments.length + 1];
                at[0] = receiver; // object expression is first argument
                System.arraycopy(arguments, 0, at, 1, arguments.length);
            } else {
                r = wrapTypeIfNecessary(r); // GROOVY-11383
            }
            Map<GenericsTypeName, GenericsType> spec = extractPlaceHoldersVisibleToDeclaration(r, m, null);
            GenericsType[] gt = applyGenericsContext(spec, m.getGenericsTypes()); // class params in bounds
            GenericsUtils.extractPlaceholders(GenericsUtils.makeClassSafe0(OBJECT_TYPE, gt), spec);

            Parameter[] parameters = m.getParameters();
            ClassNode[] paramTypes = new ClassNode[parameters.length];
            for (int i = 0, n = parameters.length; i < n; i += 1) {
                paramTypes[i] = fullyResolveType(parameters[i].getType(), spec);
                // GROOVY-10010: check for List<String> parameter and ["foo","$bar"] argument
                if (i < at.length && hasGStringStringError(paramTypes[i], at[i], location)) {
                    return false;
                }
            }

            addStaticTypeError("Cannot call " + (gt == null ? "" : GenericsUtils.toGenericTypesString(gt)) +
                    prettyPrintTypeName(r) + "#" + toMethodParametersString(m.getName(), paramTypes) + " with arguments " + formatArgumentList(at), location);
            return false;
        }
        return true;
    }

    /**
     * Formats an argument-type list for diagnostics.
     */
    protected static String formatArgumentList(final ClassNode[] nodes) {
        if (nodes == null || nodes.length == 0) return "[]";

        StringJoiner joiner = new StringJoiner(", ", "[", "]");
        for (ClassNode node : nodes) {
            joiner.add(prettyPrintType(node));
        }
        return joiner.toString();
    }

    private static void putSetterInfo(final Expression exp, final SetterInfo info) {
        exp.putNodeMetaData(SetterInfo.class, info);
    }

    private static SetterInfo removeSetterInfo(final Expression exp) {
        Object nodeMetaData = exp.getNodeMetaData(SetterInfo.class);
        if (nodeMetaData != null) {
            exp.removeNodeMetaData(SetterInfo.class);
            return (SetterInfo) nodeMetaData;
        }
        return null;
    }

    /** {@inheritDoc} */
    @Override
    public void addError(final String msg, final ASTNode node) {
        Long err = ((long) node.getLineNumber()) << 16 + node.getColumnNumber();
        if ((DEBUG_GENERATED_CODE && node.getLineNumber() < 0) || !typeCheckingContext.reportedErrors.contains(err)) {
            SourceUnit source = getSourceUnit();
            ErrorCollector collector = typeCheckingContext.getErrorCollector();
            Message message = Message.create(new SyntaxException(msg + '\n', node), source);
            // GROOVY-12306: only the source unit's own collector enforces the error tolerance.
            // The temporary collectors pushed for speculative checks must never bail out, as
            // their errors are routinely discarded once a candidate is ruled in or out.
            if (collector == source.getErrorCollector()) {
                collector.addError(message);
            } else {
                collector.addErrorAndContinue(message);
            }
            typeCheckingContext.reportedErrors.add(err);
        }
    }

    /**
     * Reports a static type checking error for the supplied node.
     */
    protected void addStaticTypeError(final String msg, final ASTNode node) {
        if (node.getColumnNumber() > 0 && node.getLineNumber() > 0) {
            addError(StaticTypesTransformation.STATIC_ERROR_PREFIX + msg, node);
        } else if (DEBUG_GENERATED_CODE) { // if not debug, ignore errors which are related to unknown source locations (generated code)
            addError(StaticTypesTransformation.STATIC_ERROR_PREFIX + "Error in generated code [" + node.getText() + "] - " + msg, node);
        }
    }

    /**
     * Reports that no matching method or constructor could be found for the call.
     */
    protected void addNoMatchingMethodError(final ClassNode receiver, final String name, ClassNode[] args, final Expression exp) {
        addNoMatchingMethodError(receiver, name, args, (ASTNode)exp);
    }

    /**
     * Reports that no matching method or constructor could be found for the call.
     */
    protected void addNoMatchingMethodError(final ClassNode receiver, final String name, ClassNode[] args, final ASTNode origin) {
        String classifier;
        String descriptor;
        if ("<init>".equals(name)) {
            classifier = "constructor";
            // remove implicit agruments [String, int] from enum constant construction
            if (receiver.isEnum() && args.length >= 2) args = Arrays.copyOfRange(args, 2, args.length);
            descriptor = prettyPrintTypeName(receiver) + toMethodParametersString("", args); // type is name
        } else {
            classifier = "method";
            descriptor = prettyPrintTypeName(wrapTypeIfNecessary(receiver)) + "#" + toMethodParametersString(name, args);
            if (isClassClassNodeWrappingConcreteType(receiver)) { ClassNode t = receiver.getGenericsTypes()[0].getType();
                descriptor += " or static method " + prettyPrintTypeName(t) + "#" + toMethodParametersString(name, args);
            }
        }
        addStaticTypeError("Cannot find matching " + classifier + " " + descriptor + ". Please check if the declared type is correct and if the method exists.", origin);
    }

    /**
     * Reports that method resolution remained ambiguous after filtering.
     */
    protected void addAmbiguousErrorMessage(final List<MethodNode> foundMethods, final String name, final ClassNode[] args, final Expression expr) {
        addStaticTypeError("Reference to method is ambiguous. Cannot choose between " + prettyPrintMethodList(foundMethods), expr);
    }

    /**
     * Reports that categories cannot be used under static type checking.
     */
    protected void addCategoryMethodCallError(final Expression call) {
        addStaticTypeError("Due to their dynamic nature, usage of categories is not possible with static type checking active. Consider using an extension module, which is compatible with static type checking", call);
    }

    /**
     * Reports an incompatible assignment between the supplied types.
     */
    protected void addAssignmentError(final ClassNode leftType, final ClassNode rightType, final Expression expression) {
        addStaticTypeError("Cannot assign value of type " + prettyPrintType(rightType) + " to variable of type " + prettyPrintType(leftType), expression);
    }

    /**
     * Reports an unsupported prefix or postfix operation.
     */
    protected void addUnsupportedPreOrPostfixExpressionError(final Expression expression) {
        if (expression instanceof PostfixExpression) {
            addStaticTypeError("Unsupported postfix operation type [" + ((PostfixExpression) expression).getOperation() + "]", expression);
        } else if (expression instanceof PrefixExpression) {
            addStaticTypeError("Unsupported prefix operation type [" + ((PrefixExpression) expression).getOperation() + "]", expression);
        } else {
            throw new IllegalArgumentException("Method should be called with a PostfixExpression or a PrefixExpression");
        }
    }

    /**
     * Restricts type checking to the supplied method set.
     */
    public void setMethodsToBeVisited(final Set<MethodNode> methodsToBeVisited) {
        this.typeCheckingContext.methodsToBeVisited = methodsToBeVisited;
    }

    /**
     * Replays deferred checks that require information collected during the first pass.
     */
    public void performSecondPass() {
        for (SecondPassExpression<?> wrapper : typeCheckingContext.secondPassExpressions) {
            Expression expression = wrapper.getExpression();
            if (expression instanceof BinaryExpression) {
                Expression left = ((BinaryExpression) expression).getLeftExpression();
                if (left instanceof VariableExpression) {
                    Variable target = findTargetVariable((VariableExpression) left);
                    if (target instanceof VariableExpression) {
                        List<ClassNode> classNodes = typeCheckingContext.closureSharedVariablesAssignmentTypes.get(target);
                        if (classNodes != null && classNodes.size() > 1) {
                            ClassNode type = lowestUpperBound(classNodes);
                            String message = getOperationName(((BinaryExpression) expression).getOperation().getType());
                            if (message != null) {
                                List<MethodNode> methods = findMethod(type, message, getType(((BinaryExpression) expression).getRightExpression()));
                                if (methods.isEmpty()) {
                                    String methSpec = toMethodParametersString(message, getType(((BinaryExpression) expression).getRightExpression()));
                                    String stcError = String.format("The closure shared variable \"%s\" has been assigned with various types and the method %s does not exist in the lowest upper bound of those types: %s", target.getName(), methSpec, prettyPrintTypeName(type));
                                    addStaticTypeError(stcError + ". In general, this style of variable reuse is a bad practice because the compiler cannot determine safely what is the type of the variable at the moment of the call in a multi-threaded context.", expression );
                                }
                            }
                        }
                    }
                }
            } else if (expression instanceof MethodCallExpression call) {
                Expression objectExpression = call.getObjectExpression();
                if (objectExpression instanceof VariableExpression) {
                    // this should always be the case, but adding a test is safer
                    Variable target = findTargetVariable((VariableExpression) objectExpression);
                    if (target instanceof VariableExpression) {
                        List<ClassNode> classNodes = typeCheckingContext.closureSharedVariablesAssignmentTypes.get(target);
                        if (classNodes != null && classNodes.size() > 1) {
                            ClassNode type = lowestUpperBound(classNodes);
                            MethodNode mct = call.getNodeMetaData(DIRECT_METHOD_CALL_TARGET);
                            // we must check that such a method exists for the common type(s)
                            List<MethodNode> methods = findMethod(type, mct.getName(), (ClassNode[]) wrapper.getData());
                            if (methods.size() != 1) {
                                String methSpec = toMethodParametersString(mct.getName(), extractTypesFromParameters(mct.getParameters()));
                                String stcError = String.format("The closure shared variable \"%s\" has been assigned with various types and the method %s does not exist in the lowest upper bound of those types: %s", target.getName(), methSpec, prettyPrintTypeName(type));
                                addStaticTypeError(stcError + ". In general, this style of variable reuse is a bad practice because the compiler cannot determine safely what is the type of the variable at the moment of the call in a multi-threaded context.", expression );
                            }
                        }
                    }
                }
            }
        }
        // give a chance to type checker extensions to throw errors based on information gathered afterwards
        extension.finish();
    }

    /**
     * Extracts the declared types of the supplied parameters.
     */
    protected static ClassNode[] extractTypesFromParameters(final Parameter[] parameters) {
        return Arrays.stream(parameters).map(Parameter::getType).toArray(ClassNode[]::new);
    }

    /**
     * Returns a wrapped type if, and only if, the provided class node is a primitive type.
     * This method differs from {@link ClassHelper#getWrapper(org.codehaus.groovy.ast.ClassNode)} as it will
     * return the same instance if the provided type is not a generic type.
     *
     * @return the wrapped type
     */
    protected static ClassNode wrapTypeIfNecessary(final ClassNode type) {
        return (type != null && isPrimitiveType(type) ? getWrapper(type) : type);
    }

    /**
     * Indicates whether {@code start} is {@code toBeChecked} or nested within it.
     */
    protected static boolean isClassInnerClassOrEqualTo(final ClassNode toBeChecked, final ClassNode start) {
        if (start == toBeChecked) return true;
        ClassNode outer = start.getOuterClass();
        if (outer != null) {
            return isClassInnerClassOrEqualTo(toBeChecked, outer);
        }
        return false;
    }

    private static boolean isNonStaticHelperMethod(final MethodNode method) {
        Parameter[] parameters = method.getParameters(); // check first param is "$self"
        if (parameters.length > 0 && parameters[0].getName().equals(Traits.THIS_OBJECT)) {
            return !method.getName().contains("$init$") && Traits.isTrait(method.getDeclaringClass().getOuterClass());
        }
        return false;
    }

    private static BinaryExpression assignX(final Expression lhs, final Expression rhs, final ASTNode pos) {
        BinaryExpression exp = (BinaryExpression) GeneralUtils.assignX(lhs, rhs);
        exp.setSourcePosition(pos);
        exp.setSynthetic(true);
        return exp;
    }

    //--------------------------------------------------------------------------
    // temporaryIfBranchTypeInformation support; migrate to TypeCheckingContext?

    /**
     * Stores information about types when <i>[objectExpression instanceof typeExpression]</i> is visited.
     *
     * @param objectExpression the expression to be checked against instanceof
     * @param typeExpression   the expression which represents the target type
     */
    protected void pushInstanceOfTypeInfo(final Expression objectExpression, final Expression typeExpression) {
        var tti = typeCheckingContext.peekTemporaryTypeInfo(extractTemporaryTypeInfoKey(objectExpression));
        ClassNode type = typeExpression.getType();
        tti.removeIf(type::isDerivedFrom); // x instanceof Number && x instanceof Integer; Number is redundant
        tti.add(type);
    }

    private void putNotInstanceOfTypeInfo(final Object key, final Collection<ClassNode> types) {
        Object notKey = key instanceof Object[] arr ? arr[1] : new Object[]{"!instanceof", key};
        var tti = typeCheckingContext.peekTemporaryTypeInfo(notKey);
        tti.addAll(types); // stash negative type(s)
    }

    private boolean optInstanceOfTypeInfo(final Expression expression, final ClassNode type) {
        var tti = typeCheckingContext.temporaryIfBranchTypeInformation.peek().get(extractTemporaryTypeInfoKey(expression));
        if (tti != null) { assert !tti.isEmpty();
            tti.add(type);
            ClassNode ut = newUnionTypeClassNode(tti);
            tti.clear();
            tti.add(ut);
            return true;
        }
        return false;
    }

    /**
     * GROOVY-11983: Whether the temporary type info captured while visiting {@code expr}
     * may be soundly inverted for the else branch of {@code if (expr) ... else ...} (or
     * the false branch of a ternary, or the surrounding scope after an early-returning
     * {@code if (expr) return}). Inversion is only sound when {@code !expr} pins down
     * the negation of every individual instanceof / !instanceof check inside it.
     * <p>
     * Only OR-like operators ({@code ||}, and {@code |} on booleans) preserve that
     * property — {@code !(L op R)} is equivalent to {@code !L && !R}, so each operand's
     * negation is pinned down. AND-like and XOR-like operators ({@code &&}, {@code &},
     * {@code ^}) do not: {@code !(L && R)} only requires <em>at least one</em> operand
     * to be false, so a {@code !instanceof} hidden inside one of them would otherwise
     * be unwrapped into a positive smart-cast in the else branch, producing an unsound
     * {@code checkcast} and a runtime ClassCastException.
     * <p>
     * A leading {@code NotExpression} is always invertible regardless of its operand:
     * {@code visitNotExpression} sign-flips the operand's tti on the way out, and the
     * else-branch inversion flips a second time, leaving entries that match the operand
     * being true — exactly the condition under which the else fires.
     */
    private static boolean canInvertNarrowingForElseBranch(final Expression expr) {
        // NotExpression extends BooleanExpression — must check before the BooleanExpression branch
        if (expr instanceof NotExpression) return true;
        if (expr instanceof BooleanExpression be) {
            return canInvertNarrowingForElseBranch(be.getExpression());
        }
        if (expr instanceof BinaryExpression be) {
            switch (be.getOperation().getType()) {
              case LOGICAL_AND:
              case BITWISE_AND:
              case BITWISE_XOR:
                // AND-like / XOR: !(L op R) doesn't pin down each operand's truth value
                return false;
              case LOGICAL_OR:
              case BITWISE_OR:
                // OR-like: !(L op R) <=> !L && !R, so recurse into both operands
                return canInvertNarrowingForElseBranch(be.getLeftExpression())
                    && canInvertNarrowingForElseBranch(be.getRightExpression());
              default:
                return true; // instanceof, ==, comparisons, etc.
            }
        }
        if (expr instanceof TernaryExpression te) {
            return canInvertNarrowingForElseBranch(te.getBooleanExpression().getExpression())
                && canInvertNarrowingForElseBranch(te.getTrueExpression())
                && canInvertNarrowingForElseBranch(te.getFalseExpression());
        }
        return true;
    }

    /**
     * Computes the key to use for {@link TypeCheckingContext#temporaryIfBranchTypeInformation}.
     */
    protected Object extractTemporaryTypeInfoKey(final Expression expression) {
        return expression instanceof VariableExpression ? findTargetVariable((VariableExpression) expression) : expression.getText();
    }

    /**
     * A helper method which determines which receiver class should be used in error messages when a field or attribute
     * is not found. The returned type class depends on whether we have temporary type information available (due to
     * instanceof checks) and whether there is a single candidate in that case.
     *
     * @param expression the expression for which an unknown field has been found
     * @param type the type of the expression (used as fallback type)
     * @return if temporary information is available and there's only one type, returns the temporary type class
     * otherwise falls back to the provided type class.
     */
    protected ClassNode findCurrentInstanceOfClass(final Expression expression, final ClassNode type) {
        List<ClassNode> tempTypes = getTemporaryTypesForExpression(expression);
        if (tempTypes.size() == 1) return tempTypes.get(0);
        return type;
    }

    /**
     * Returns temporary narrowed types recorded for the supplied expression.
     */
    protected List<ClassNode> getTemporaryTypesForExpression(final Expression expression) {
        Object key = extractTemporaryTypeInfoKey(expression);
        List<ClassNode> tempTypes = typeCheckingContext.temporaryIfBranchTypeInformation.stream().flatMap(tti ->
            tti.getOrDefault(key, Collections.emptyList()).stream()
        ).collect(Collectors.toList());
        int i = tempTypes.lastIndexOf(VOID_TYPE);
        if (i != -1) { // assignment overwrites instanceof
            tempTypes = tempTypes.subList(i + 1, tempTypes.size());
        }
        return DefaultGroovyMethods.unique(tempTypes); // GROOVY-6429
    }

    private ClassNode getInferredTypeFromTempInfo(final Expression expression, final ClassNode expressionType) {
        if ((expression instanceof VariableExpression || expression instanceof PropertyExpression) && !isPrimitiveType(expressionType)) {
            List<ClassNode> tempTypes = getTemporaryTypesForExpression(expression);
            if (!tempTypes.isEmpty()) {
                ClassNode   superclass;
                ClassNode[] interfaces;
                if (expressionType instanceof WideningCategories.LowestUpperBoundClassNode) {
                    superclass = expressionType.getUnresolvedSuperClass(); // GROOVY-11815
                    interfaces = expressionType.getInterfaces();
                } else if (expressionType != null && expressionType.isInterface()) {
                    superclass = OBJECT_TYPE;
                    interfaces = new ClassNode[]{expressionType};
                } else {
                    superclass = expressionType;
                    interfaces = ClassNode.EMPTY_ARRAY;
                }

                List<ClassNode> types = new ArrayList<>();
                if (superclass != null && !superclass.equals(OBJECT_TYPE) // GROOVY-7333
                        && tempTypes.stream().noneMatch(t -> !t.equals(superclass) && t.isDerivedFrom(superclass))) { // GROOVY-9769
                    types.add(superclass);
                }
                for (ClassNode anInterface : interfaces) {
                    if (tempTypes.stream().noneMatch(t -> t.implementsInterface(anInterface))) { // GROOVY-9769
                        types.add(anInterface);
                    }
                }
                int tempTypesCount = tempTypes.size();
                if (tempTypesCount == 1 && types.isEmpty()) {
                    types.add(tempTypes.get(0));
                } else for (ClassNode tempType : tempTypes) {
                    if (!tempType.isInterface() // GROOVY-11290: keep most-specific types
                            ? (superclass == null || !superclass.isDerivedFrom(tempType))
                                    && (tempTypesCount == 1 || tempTypes.stream().noneMatch(t -> !t.equals(tempType) && t.isDerivedFrom(tempType)))
                            : (expressionType == null || !isOrImplements(expressionType, tempType))
                                    && (tempTypesCount == 1 || tempTypes.stream().noneMatch(t -> t != tempType && t.implementsInterface(tempType)))) {
                        types.add(tempType);
                    }
                }

                int typesCount = types.size();
                if (typesCount == 1) {
                    return types.get(0);
                } else if (typesCount > 1) {
                    return newIntersectionTypeClassNode(types);
                }
            }
        }
        return expressionType;
    }

    private static ClassNode newIntersectionTypeClassNode(final Collection<ClassNode> types) {
        Map<Boolean, List<ClassNode>> spec = types.stream().collect(Collectors.partitioningBy(ClassNode::isInterface));
        ClassNode[] interfaces = spec.get(Boolean.TRUE ).toArray(ClassNode[]::new);
        List<ClassNode> supers = spec.get(Boolean.FALSE);
        if (interfaces.length == 0) return supers.get(0);
        supers.add(OBJECT_TYPE); // ensure get(0) nothrow
        return new WideningCategories.LowestUpperBoundClassNode("IntersectionTypeClassNode", supers.get(0), interfaces);
    }

    private static ClassNode newUnionTypeClassNode(final Collection<ClassNode> types) {
        if (types.size() == 1) return types.iterator().next();
        var set = new LinkedHashSet<ClassNode>();
        for (ClassNode t : types) {
            if (t instanceof UnionTypeClassNode u) {
                Collections.addAll(set, u.getDelegates());
            } else {
                set.add(t);
            }
        }
        assert set.size() > 1;
        return new UnionTypeClassNode(set.toArray(ClassNode[]::new));
    }

    //--------------------------------------------------------------------------

    /**
     * Creates signature codecs for serialized type checking metadata.
     */
    public static class SignatureCodecFactory {
        /**
         * Returns the codec implementation for the requested signature version.
         */
        public static SignatureCodec getCodec(final int version, final ClassLoader classLoader) {
            return switch (version) {
                case 1 -> new SignatureCodecVersion1(classLoader);
                default -> null;
            };
        }
    }

    // class only used to store setter information when an expression of type
    // a.x = foo or x=foo is found and that it corresponds to a setter call
    private static class SetterInfo {
        final ClassNode receiverType;
        final String name;
        final List<MethodNode> setters;

        private SetterInfo(final ClassNode receiverType, final String name, final List<MethodNode> setters) {
            this.receiverType = receiverType;
            this.setters = setters;
            this.name = name;
        }
    }

    private class PropertyLookup extends ClassCodeVisitorSupport {
        private final ClassNode receiverType;
        private final Consumer<ClassNode> propertyType;

        PropertyLookup(final ClassNode receiverType, final Consumer<ClassNode> propertyType) {
            this.receiverType = receiverType;
            this.propertyType = propertyType;
        }

        @Override
        protected SourceUnit getSourceUnit() {
            return StaticTypeCheckingVisitor.this.getSourceUnit();
        }

        @Override
        public void visitField(final FieldNode node) {
            reportPropertyType(node.getType(), node.isStatic() ? null : node.getDeclaringClass());
        }

        @Override
        public void visitMethod(final MethodNode node) {
            reportPropertyType(node.getReturnType(), node.isStatic() ? null : node.getDeclaringClass());
        }

        @Override
        public void visitProperty(final PropertyNode node) {
            reportPropertyType(node.getOriginType(), node.isStatic() ? null : node.getDeclaringClass());
        }

        private void reportPropertyType(ClassNode type, final ClassNode declaringClass) {
            if (declaringClass != null && GenericsUtils.hasUnresolvedGenerics(type)) { // GROOVY-10787
                Map<GenericsTypeName, GenericsType> spec = extractPlaceHolders(receiverType, declaringClass);
                type = applyGenericsContext(spec, type);
            }
            propertyType.accept(type);
        }
    }

    /**
     * Wrapper for a Parameter so it can be treated like a VariableExpression
     * and tracked in the {@code ifElseForWhileAssignmentTracker}.
     */
    private class ParameterVariableExpression extends VariableExpression {
        private final Parameter parameter;

        ParameterVariableExpression(final Parameter parameter) {
            super(parameter);
            this.parameter = parameter;

            ClassNode inferredType = getNodeMetaData(INFERRED_TYPE);
            if (inferredType == null) {
                inferredType = typeCheckingContext.controlStructureVariables.get(parameter); // for/catch/closure
                if (inferredType == null) {
                    inferredType = getTypeFromClosureArguments(parameter); // @ClosureParams or SAM-type coercion
                }
                setNodeMetaData(INFERRED_TYPE, inferredType != null ? inferredType : parameter.getType()); // GROOVY-10651
            }
        }

        @Override
        public Map<?, ?> getMetaDataMap() {
            return parameter.getMetaDataMap();
        }

        @Override
        public void setMetaDataMap(final Map<?, ?> metaDataMap) {
            parameter.setMetaDataMap(metaDataMap);
        }
    }

    /**
     * Memoizes inferred variable types so they can be restored after temporary rewrites.
     */
    protected class VariableExpressionTypeMemoizer extends ClassCodeVisitorSupport {
        private final boolean onlySharedVariables;
        private final Set<VariableExpression> decl = new HashSet<>();
        private final Map<VariableExpression, ClassNode> varOrigType;

        /**
         * Creates a memoizer that records all visible variable expressions.
         */
        public VariableExpressionTypeMemoizer(final Map<VariableExpression, ClassNode> varOrigType) {
            this(varOrigType, false);
        }

        /**
         * Creates a memoizer with optional filtering for shared variables only.
         */
        public VariableExpressionTypeMemoizer(final Map<VariableExpression, ClassNode> varOrigType, final boolean onlySharedVariables) {
            this.varOrigType = varOrigType;
            this.onlySharedVariables = onlySharedVariables;
        }

        /** {@inheritDoc} */
        @Override
        protected SourceUnit getSourceUnit() {
            return StaticTypeCheckingVisitor.this.getSourceUnit();
        }

        private boolean isOuterScopeShared(final Variable var) {
            return var.isClosureSharedVariable() && !decl.contains(var);
        }

        /** {@inheritDoc} */
        @Override
        public void visitVariableExpression(final VariableExpression expression) {
            Variable var = findTargetVariable(expression);
            if ((!onlySharedVariables || isOuterScopeShared(var)) && var instanceof VariableExpression ve) {
                ClassNode cn = ve.getNodeMetaData(INFERRED_TYPE);
                if (cn == null) cn = ve.getOriginType();
                varOrigType.put(ve, cn);
            }
            super.visitVariableExpression(expression);
        }

        /** {@inheritDoc} */
        @Override
        @SuppressWarnings("unchecked")
        public void visitDeclarationExpression(final DeclarationExpression expression) {
            if (expression.isMultipleAssignmentDeclaration()) {
                List<?> vars = expression.getTupleExpression().getExpressions();
                decl.addAll((List<VariableExpression>) vars);
            } else {
                decl.add(expression.getVariableExpression());
            }
            super.visitDeclarationExpression(expression);
        }
    }
}
