/*******************************************************************************
 * Copyright (c) 2000, 2023 IBM Corporation and others.
 *
 * This program and the accompanying materials
 * are made available under the terms of the Eclipse Public License 2.0
 * which accompanies this distribution, and is available at
 * https://www.eclipse.org/legal/epl-2.0/
 *
 * SPDX-License-Identifier: EPL-2.0
 *
 * Contributors:
 *     IBM Corporation - initial API and implementation
 *     Stephan Herrmann - Contribution for
 *								bug 349326 - [1.7] new warning for missing try-with-resources
 *								bug 370930 - NonNull annotation not considered for enhanced for loops
 *								bug 365859 - [compiler][null] distinguish warnings based on flow analysis vs. null annotations
 *								bug 345305 - [compiler][null] Compiler misidentifies a case of "variable can only be null"
 *								bug 393719 - [compiler] inconsistent warnings on iteration variables
 *								Bug 411964 - [1.8][null] leverage null type annotation in foreach statement
 *								Bug 392099 - [1.8][compiler][null] Apply null annotation on types for null analysis
 *								Bug 453483 - [compiler][null][loop] Improve null analysis for loops
 *								Bug 415790 - [compiler][resource]Incorrect potential resource leak warning in for loop with close in try/catch
 *     Jesper S Moller -  Contribution for
 *								bug 401853 - Eclipse Java compiler creates invalid bytecode (java.lang.VerifyError)
 *                               bug 527554 - [18.3] Compiler support for JEP 286 Local-Variable Type
 *******************************************************************************/
package org.eclipse.jdt.internal.compiler.ast;

import java.util.ArrayList;
import java.util.List;

import org.eclipse.jdt.core.compiler.CharOperation;
import org.eclipse.jdt.internal.compiler.ASTVisitor;
import org.eclipse.jdt.internal.compiler.classfmt.ClassFileConstants;
import org.eclipse.jdt.internal.compiler.codegen.BranchLabel;
import org.eclipse.jdt.internal.compiler.codegen.CodeStream;
import org.eclipse.jdt.internal.compiler.flow.FlowContext;
import org.eclipse.jdt.internal.compiler.flow.FlowInfo;
import org.eclipse.jdt.internal.compiler.flow.LoopingFlowContext;
import org.eclipse.jdt.internal.compiler.flow.UnconditionalFlowInfo;
import org.eclipse.jdt.internal.compiler.impl.Constant;
import org.eclipse.jdt.internal.compiler.impl.JavaFeature;
import org.eclipse.jdt.internal.compiler.lookup.ArrayBinding;
import org.eclipse.jdt.internal.compiler.lookup.Binding;
import org.eclipse.jdt.internal.compiler.lookup.BlockScope;
import org.eclipse.jdt.internal.compiler.lookup.CaptureBinding;
import org.eclipse.jdt.internal.compiler.lookup.ImplicitNullAnnotationVerifier;
import org.eclipse.jdt.internal.compiler.lookup.LocalVariableBinding;
import org.eclipse.jdt.internal.compiler.lookup.MethodBinding;
import org.eclipse.jdt.internal.compiler.lookup.ParameterizedTypeBinding;
import org.eclipse.jdt.internal.compiler.lookup.ProblemReasons;
import org.eclipse.jdt.internal.compiler.lookup.ProblemReferenceBinding;
import org.eclipse.jdt.internal.compiler.lookup.ReferenceBinding;
import org.eclipse.jdt.internal.compiler.lookup.TagBits;
import org.eclipse.jdt.internal.compiler.lookup.TypeBinding;
import org.eclipse.jdt.internal.compiler.lookup.TypeConstants;
import org.eclipse.jdt.internal.compiler.lookup.TypeIds;

public class ForeachStatement extends Statement {

	public LocalDeclaration elementVariable;
	public int elementVariableImplicitWidening = -1;
	public Expression collection;
	public Statement originalAction;
	public Statement action;
	public RecordPattern pattern;

	// set the kind of foreach
	private int kind;
	// possible kinds of iterating behavior
	private static final int ARRAY = 0;
	private static final int RAW_ITERABLE = 1;
	private static final int GENERIC_ITERABLE = 2;

	private TypeBinding iteratorReceiverType;
	private TypeBinding collectionElementType;

	// loop labels
	private BranchLabel breakLabel;
	private BranchLabel continueLabel;

	public BlockScope scope;

	// secret variables for codegen
	public LocalVariableBinding indexVariable;
	public LocalVariableBinding collectionVariable;	// to store the collection expression value
	public LocalVariableBinding maxVariable;
	// secret variable names
	private static final char[] SecretIteratorVariableName = " iterator".toCharArray(); //$NON-NLS-1$
	private static final char[] SecretIndexVariableName = " index".toCharArray(); //$NON-NLS-1$
	private static final char[] SecretCollectionVariableName = " collection".toCharArray(); //$NON-NLS-1$
	private static final char[] SecretMaxVariableName = " max".toCharArray(); //$NON-NLS-1$
	public static final char[] SecretRecordPatternVariableName = " recordPatternVar".toCharArray(); //$NON-NLS-1$

	int postCollectionInitStateIndex = -1;
	int mergedInitStateIndex = -1;

	public ForeachStatement(
		LocalDeclaration elementVariable,
		int start) {

		this.elementVariable = elementVariable;
		this.sourceStart = start;
		this.kind = -1;
	}

	public ForeachStatement(RecordPattern recordPattern, int start) {
		this(new LocalDeclaration(ForeachStatement.SecretRecordPatternVariableName, 0, 0), start);
		this.pattern = recordPattern;
		this.elementVariable.type = this.pattern.type;
	}
	public void transformAction() {
		if (this.pattern != null && this.action != null) {
			SwitchStatement switchStatement = new SwitchStatement();
			switchStatement.switchBits |= SwitchStatement.Synthetic;
			switchStatement.containsPatterns = true;
			switchStatement.containsNull = true;
			switchStatement.expression = new SingleNameReference(this.elementVariable.name, 0);

			List<Statement> stmts = new ArrayList<>();

			stmts.add(new CaseStatement(this.pattern, 0, 0));
			stmts.add(this.action);
			stmts.add(new BreakStatement(null, 0, 0));

			stmts.add(new CaseStatement(0, 0, new Expression[] { new NullLiteral(0, 0), new FakeDefaultLiteral(0, 0) }));

			// TODO: Need to enable MatchException
			AllocationExpression allocationExpression = new AllocationExpression();
			allocationExpression.type = new SingleTypeReference("NullPointerException".toCharArray(), 0); //$NON-NLS-1$ ;
			stmts.add(new ThrowStatement(allocationExpression, 0, 0));

			switchStatement.statements = stmts.toArray(new Statement[0]);
			switchStatement.sourceStart = this.action.sourceStart;
			switchStatement.sourceEnd = this.action.sourceEnd;
			this.originalAction = this.action;
			this.action = switchStatement;
		}
	}
	@Override
	public FlowInfo analyseCode(BlockScope currentScope, FlowContext flowContext, FlowInfo flowInfo) {
		// initialize break and continue labels
		this.breakLabel = new BranchLabel();
		this.continueLabel = new BranchLabel();
		int initialComplaintLevel = (flowInfo.reachMode() & FlowInfo.UNREACHABLE) != 0 ? Statement.COMPLAINED_FAKE_REACHABLE : Statement.NOT_COMPLAINED;

		// process the element variable and collection
		flowInfo = this.elementVariable.analyseCode(this.scope, flowContext, flowInfo);
		FlowInfo condInfo = this.collection.analyseCode(this.scope, flowContext, flowInfo.copy());
		this.collection.checkNPE(currentScope, flowContext, condInfo.copy(), 1);
		LocalVariableBinding elementVarBinding = this.elementVariable.binding;

		// element variable will be assigned when iterating
		condInfo.markAsDefinitelyAssigned(elementVarBinding);

		this.postCollectionInitStateIndex = currentScope.methodScope().recordInitializationStates(condInfo);


		// process the action
		LoopingFlowContext loopingContext =
			new LoopingFlowContext(flowContext, flowInfo, this, this.breakLabel,
				this.continueLabel, this.scope, true);
		UnconditionalFlowInfo actionInfo =
			condInfo.nullInfoLessUnconditionalCopy();
		actionInfo.markAsDefinitelyUnknown(elementVarBinding);
		if (currentScope.compilerOptions().isAnnotationBasedNullAnalysisEnabled) {
			int elementNullStatus = NullAnnotationMatching.nullStatusFromExpressionType(this.collectionElementType);
			int nullStatus = NullAnnotationMatching.checkAssignment(currentScope, flowContext, elementVarBinding, null, // have no useful flowinfo for element var
																		elementNullStatus, this.collection, this.collectionElementType);
			if ((this.kind == GENERIC_ITERABLE || this.kind == RAW_ITERABLE) && !currentScope.compilerOptions().usesNullTypeAnnotations()) {
				// respect declaration annotation on Iterator.next():
				ReferenceBinding iterator = currentScope.getJavaUtilIterator();
				if (iterator != null) {
					MethodBinding next = iterator.getExactMethod(TypeConstants.NEXT, Binding.NO_TYPES, currentScope.compilationUnitScope);
					ImplicitNullAnnotationVerifier.ensureNullnessIsKnown(next, currentScope);
					if (next != null && ((next.tagBits & TagBits.AnnotationNullMASK) != 0)) {
						nullStatus = FlowInfo.tagBitsToNullStatus(next.tagBits);
					}
				}
			}
			if ((elementVarBinding.type.tagBits & TagBits.IsBaseType) == 0) {
				actionInfo.markNullStatus(elementVarBinding, nullStatus);
			}
		}
		FlowInfo exitBranch;
		if (!(this.action == null || (this.action.isEmptyBlock()
				&& currentScope.compilerOptions().complianceLevel <= ClassFileConstants.JDK1_3))) {

			if (this.action.complainIfUnreachable(actionInfo, this.scope, initialComplaintLevel, true) < Statement.COMPLAINED_UNREACHABLE) {
				actionInfo = this.action.analyseCode(this.scope, loopingContext, actionInfo).unconditionalCopy();
				FakedTrackingVariable.markForeachElementVar(this.elementVariable);
				// action.analyseCode() missed the following check due to identical scopes of ForeachStatement and action:
				FlowInfo actionNullInfo = condInfo.copy().addNullInfoFrom(actionInfo); // previously action did not see nullinfo from condInfo
				this.scope.checkUnclosedCloseables(actionNullInfo, loopingContext, null, null);
			}

			// code generation can be optimized when no need to continue in the loop
			exitBranch = flowInfo.unconditionalCopy().
					addInitializationsFrom(condInfo.initsWhenFalse());
			// TODO (maxime) no need to test when false: can optimize (same for action being unreachable above)
			if ((actionInfo.tagBits & loopingContext.initsOnContinue.tagBits &
					FlowInfo.UNREACHABLE_OR_DEAD) != 0) {
				this.continueLabel = null;
			} else {
				actionInfo = actionInfo.mergedWith(loopingContext.initsOnContinue);
				loopingContext.complainOnDeferredFinalChecks(this.scope, actionInfo);
				exitBranch.addPotentialInitializationsFrom(actionInfo);
			}
		} else {
			exitBranch = condInfo.initsWhenFalse();
			if (this.action instanceof Block && !this.action.isEmptyBlock()) {
				this.scope.checkUnclosedCloseables(actionInfo, loopingContext, null, null);
			}
		}

		// we need the variable to iterate the collection even if the
		// element variable is not used
		final boolean hasEmptyAction = this.action == null
		|| this.action.isEmptyBlock()
		|| ((this.action.bits & IsUsefulEmptyStatement) != 0);

		switch(this.kind) {
			case ARRAY :
				if (!hasEmptyAction
						|| elementVarBinding.resolvedPosition != -1) {
					this.collectionVariable.useFlag = LocalVariableBinding.USED;
					if (this.continueLabel != null) {
						this.indexVariable.useFlag = LocalVariableBinding.USED;
						this.maxVariable.useFlag = LocalVariableBinding.USED;
					}
				}
				break;
			case RAW_ITERABLE :
			case GENERIC_ITERABLE :
				this.indexVariable.useFlag = LocalVariableBinding.USED;
				break;
		}
		//end of loop
		loopingContext.complainOnDeferredNullChecks(currentScope, actionInfo);
		if (loopingContext.hasEscapingExceptions()) { // https://bugs.eclipse.org/bugs/show_bug.cgi?id=321926
			FlowInfo loopbackFlowInfo = flowInfo.copy();
			if (this.continueLabel != null) {  // we do get to the bottom
				// loopback | (loopback + action):
				loopbackFlowInfo = loopbackFlowInfo.mergedWith(loopbackFlowInfo.unconditionalCopy().addNullInfoFrom(actionInfo).unconditionalInits());
			}
			loopingContext.simulateThrowAfterLoopBack(loopbackFlowInfo);
		}

		FlowInfo mergedInfo = FlowInfo.mergedOptimizedBranches(
				(loopingContext.initsOnBreak.tagBits &
						FlowInfo.UNREACHABLE) != 0 ?
								loopingContext.initsOnBreak :
									flowInfo.addInitializationsFrom(loopingContext.initsOnBreak), // recover upstream null info
									false,
									exitBranch,
									false,
									true /*for(;;){}while(true); unreachable(); */);
		mergedInfo.resetAssignmentInfo(this.elementVariable.binding);
		this.mergedInitStateIndex = currentScope.methodScope().recordInitializationStates(mergedInfo);
		return mergedInfo;
	}

	/**
	 * For statement code generation
	 *
	 * @param currentScope org.eclipse.jdt.internal.compiler.lookup.BlockScope
	 * @param codeStream org.eclipse.jdt.internal.compiler.codegen.CodeStream
	 */
	@Override
	public void generateCode(BlockScope currentScope, CodeStream codeStream) {

		if ((this.bits & IsReachable) == 0) {
			return;
		}
		int pc = codeStream.position;
		final boolean hasEmptyAction = this.action == null
			|| this.action.isEmptyBlock()
			|| ((this.action.bits & IsUsefulEmptyStatement) != 0);

		if (hasEmptyAction
				&& this.elementVariable.binding.resolvedPosition == -1
				&& this.kind == ARRAY) {
			this.collection.generateCode(this.scope, codeStream, false);
			codeStream.exitUserScope(this.scope);
			if (this.mergedInitStateIndex != -1) {
				codeStream.removeNotDefinitelyAssignedVariables(currentScope, this.mergedInitStateIndex);
				codeStream.addDefinitelyAssignedVariables(currentScope, this.mergedInitStateIndex);
			}
			codeStream.recordPositionsFrom(pc, this.sourceStart);
			return;
		}

		// generate the initializations
		switch(this.kind) {
			case ARRAY :
				this.collection.generateCode(this.scope, codeStream, true);
				codeStream.store(this.collectionVariable, true);
				codeStream.addVariable(this.collectionVariable);
				if (this.continueLabel != null) {
					// int length = (collectionVariable = [collection]).length;
					codeStream.arraylength();
					codeStream.store(this.maxVariable, false);
					codeStream.addVariable(this.maxVariable);
					codeStream.iconst_0();
					codeStream.store(this.indexVariable, false);
					codeStream.addVariable(this.indexVariable);
				} else {
					// leave collectionVariable on execution stack (will be consumed when swapping condition further down)
				}
				break;
			case RAW_ITERABLE :
			case GENERIC_ITERABLE :
				this.collection.generateCode(this.scope, codeStream, true);
				// declaringClass.iterator();
				codeStream.invokeIterableIterator(this.iteratorReceiverType);
				codeStream.store(this.indexVariable, false);
				codeStream.addVariable(this.indexVariable);
				break;
		}
		// label management
		BranchLabel actionLabel = new BranchLabel(codeStream);
		actionLabel.tagBits |= BranchLabel.USED;
		BranchLabel conditionLabel = new BranchLabel(codeStream);
		conditionLabel.tagBits |= BranchLabel.USED;
		this.breakLabel.initialize(codeStream);
		if (this.continueLabel == null) {
			// generate the condition (swapped for optimizing)
			conditionLabel.place();
			int conditionPC = codeStream.position;
			switch(this.kind) {
				case ARRAY :
					// inline the arraylength call
					// collectionVariable is already on execution stack
					codeStream.arraylength();
					codeStream.ifeq(this.breakLabel);
					break;
				case RAW_ITERABLE :
				case GENERIC_ITERABLE :
					codeStream.load(this.indexVariable);
					codeStream.invokeJavaUtilIteratorHasNext();
					codeStream.ifeq(this.breakLabel);
					break;
			}
			codeStream.recordPositionsFrom(conditionPC, this.elementVariable.sourceStart);
		} else {
			this.continueLabel.initialize(codeStream);
			this.continueLabel.tagBits |= BranchLabel.USED;
			// jump over the actionBlock
			codeStream.goto_(conditionLabel);
		}

		// generate the loop action
		actionLabel.place();

		// generate the loop action
		switch(this.kind) {
			case ARRAY :
				if (this.elementVariable.binding.resolvedPosition != -1) {
					codeStream.load(this.collectionVariable);
					if (this.continueLabel == null) {
						codeStream.iconst_0(); // no continue, thus simply hardcode offset 0
					} else {
						codeStream.load(this.indexVariable);
					}
					codeStream.arrayAt(this.collectionElementType.id);
					if (this.elementVariableImplicitWidening != -1) {
						codeStream.generateImplicitConversion(this.elementVariableImplicitWidening);
					}
					codeStream.store(this.elementVariable.binding, false);
					codeStream.addVisibleLocalVariable(this.elementVariable.binding);
					if (this.postCollectionInitStateIndex != -1) {
						codeStream.addDefinitelyAssignedVariables(
							currentScope,
							this.postCollectionInitStateIndex);
					}
				}
				break;
			case RAW_ITERABLE :
			case GENERIC_ITERABLE :
				codeStream.load(this.indexVariable);
				codeStream.invokeJavaUtilIteratorNext();
				if (this.elementVariable.binding.type.id != T_JavaLangObject) {
					if (this.elementVariableImplicitWidening != -1) {
						codeStream.checkcast(this.collectionElementType);
						codeStream.generateImplicitConversion(this.elementVariableImplicitWidening);
					} else {
						codeStream.checkcast(this.elementVariable.binding.type);
					}
				}
				if (this.elementVariable.binding.resolvedPosition == -1) {
					switch (this.elementVariable.binding.type.id) {
						case TypeIds.T_long :
						case TypeIds.T_double :
							codeStream.pop2();
							break;
						default:
							codeStream.pop();
							break;
					}
				} else {
					codeStream.store(this.elementVariable.binding, false);
					codeStream.addVisibleLocalVariable(this.elementVariable.binding);
					if (this.postCollectionInitStateIndex != -1) {
						codeStream.addDefinitelyAssignedVariables(
							currentScope,
							this.postCollectionInitStateIndex);
					}
				}
				break;
		}

		if (!hasEmptyAction) {
			this.action.generateCode(this.scope, codeStream);
		}
		codeStream.removeVariable(this.elementVariable.binding);
		if (this.postCollectionInitStateIndex != -1) {
			codeStream.removeNotDefinitelyAssignedVariables(currentScope, this.postCollectionInitStateIndex);
		}
		// continuation point
		if (this.continueLabel != null) {
			this.continueLabel.place();
			int continuationPC = codeStream.position;
			// generate the increments for next iteration
			switch(this.kind) {
				case ARRAY :
					if (!hasEmptyAction || this.elementVariable.binding.resolvedPosition >= 0) {
						codeStream.iinc(this.indexVariable.resolvedPosition, 1);
					}
					// generate the condition
					conditionLabel.place();
					codeStream.load(this.indexVariable);
					codeStream.load(this.maxVariable);
					codeStream.if_icmplt(actionLabel);
					break;
				case RAW_ITERABLE :
				case GENERIC_ITERABLE :
					// generate the condition
					conditionLabel.place();
					codeStream.load(this.indexVariable);
					codeStream.invokeJavaUtilIteratorHasNext();
					codeStream.ifne(actionLabel);
					break;
			}
			codeStream.recordPositionsFrom(continuationPC, this.elementVariable.sourceStart);
		}
		switch(this.kind) {
			case ARRAY :
				codeStream.removeVariable(this.indexVariable);
				codeStream.removeVariable(this.maxVariable);
				codeStream.removeVariable(this.collectionVariable);
				break;
			case RAW_ITERABLE :
			case GENERIC_ITERABLE :
				// generate the condition
				codeStream.removeVariable(this.indexVariable);
				break;
		}
		codeStream.exitUserScope(this.scope);
		if (this.mergedInitStateIndex != -1) {
			codeStream.removeNotDefinitelyAssignedVariables(currentScope, this.mergedInitStateIndex);
			codeStream.addDefinitelyAssignedVariables(currentScope, this.mergedInitStateIndex);
		}
		this.breakLabel.place();
		codeStream.recordPositionsFrom(pc, this.sourceStart);
	}

	@Override
	public StringBuffer printStatement(int indent, StringBuffer output) {

		printIndent(indent, output).append("for ("); //$NON-NLS-1$
		if (this.pattern != null) {
			this.pattern.printExpression(0, output);
		} else {
			this.elementVariable.printAsExpression(0, output);
		}
		output.append(" : ");//$NON-NLS-1$
		if (this.collection != null) {
			this.collection.print(0, output).append(") "); //$NON-NLS-1$
		} else {
			output.append(')');
		}
		//block
		if (this.action == null) {
			output.append(';');
		} else {
			output.append('\n');
			this.action.printStatement(indent + 1, output);
		}
		return output;
	}

	public static TypeBinding getCollectionElementType(BlockScope scope, TypeBinding collectionType) {
		if (collectionType == null) return null;

		boolean isTargetJsr14 = scope.compilerOptions().targetJDK == ClassFileConstants.JDK1_4;
		if (collectionType.isCapture()) {
			TypeBinding upperBound = ((CaptureBinding)collectionType).firstBound;
			if (upperBound != null && upperBound.isArrayType())
				collectionType = upperBound; // partially anticipating the fix for https://bugs.openjdk.java.net/browse/JDK-8013843
		}
		if (collectionType.isArrayType()) { // for(E e : E[])
			return ((ArrayBinding) collectionType).elementsType();
		} else if (collectionType instanceof ReferenceBinding) {
			ReferenceBinding iterableType = ((ReferenceBinding)collectionType).findSuperTypeOriginatingFrom(T_JavaLangIterable, false /*Iterable is not a class*/);
			if (iterableType == null && isTargetJsr14) {
				iterableType = ((ReferenceBinding)collectionType).findSuperTypeOriginatingFrom(T_JavaUtilCollection, false /*Iterable is not a class*/);
			}
			if (iterableType == null) return null;

			TypeBinding[] arguments = null;
			switch (iterableType.kind()) {
				case Binding.RAW_TYPE : // for(Object o : Iterable)
					return scope.getJavaLangObject();

				case Binding.GENERIC_TYPE : // for (T t : Iterable<T>) - in case used inside Iterable itself
					arguments = iterableType.typeVariables();
					break;

				case Binding.PARAMETERIZED_TYPE : // for(E e : Iterable<E>)
					arguments = ((ParameterizedTypeBinding)iterableType).arguments;
					break;

				default:
					return null;
			}
			// generic or parameterized case
			if (arguments.length != 1) return null; // per construction can only be one
			return arguments[0];
		}
		return null;
	}
	@Override
	public void resolve(BlockScope upperScope) {
		// use the scope that will hold the init declarations
		this.scope = new BlockScope(upperScope);
		this.scope.blockStatement = this;
		this.elementVariable.resolve(this.scope); // collection expression can see itemVariable
		LocalVariableBinding[] patternVariablesInTrueScope = null;

		if (this.pattern != null && JavaFeature.RECORD_PATTERNS.isSupported(upperScope.compilerOptions())) {
			this.pattern.collectPatternVariablesToScope(null, this.scope);
			patternVariablesInTrueScope = this.pattern.getPatternVariablesWhenTrue();
			this.pattern.resolve(this.scope);
		}
		TypeBinding elementType = this.elementVariable.type.resolvedType;
		TypeBinding collectionType = this.collection == null ? null : this.collection.resolveType(upperScope);

		// Patch the resolved type
		if (this.elementVariable.isTypeNameVar(upperScope)) {
			if (this.elementVariable.type.dimensions() > 0 || this.elementVariable.type.extraDimensions() > 0) {
				upperScope.problemReporter().varLocalCannotBeArray(this.elementVariable);
			}
			if (TypeBinding.equalsEquals(TypeBinding.NULL, collectionType)) {
				upperScope.problemReporter().varLocalInitializedToNull(this.elementVariable);
				elementType = collectionType;
			} else if (TypeBinding.equalsEquals(TypeBinding.VOID, collectionType)) {
				upperScope.problemReporter().varLocalInitializedToVoid(this.elementVariable);
				elementType = collectionType;
			}
			if ((elementType = getCollectionElementType(this.scope, collectionType)) == null) {
				elementType = collectionType;
			} else {
				elementType = this.elementVariable.patchType(elementType);
			}
			if (elementType instanceof ReferenceBinding) {
				ReferenceBinding refBinding = (ReferenceBinding) elementType;
				if (!elementType.canBeSeenBy(upperScope)) {
					upperScope.problemReporter().invalidType(this.elementVariable,
							new ProblemReferenceBinding(
									CharOperation.splitOn('.', refBinding.shortReadableName()),
									refBinding,
									ProblemReasons.NotVisible));
				}
			}
			// additional check deferred from LocalDeclaration.resolve():
			if (this.elementVariable.binding != null && this.elementVariable.binding.isValidBinding()) {
				this.elementVariable.validateNullAnnotations(this.scope);
			}
		}

		TypeBinding expectedCollectionType = null;
		if (elementType != null && collectionType != null) {
			boolean isTargetJsr14 = this.scope.compilerOptions().targetJDK == ClassFileConstants.JDK1_4;
			if (collectionType.isCapture()) {
				TypeBinding upperBound = ((CaptureBinding)collectionType).firstBound;
				if (upperBound != null && upperBound.isArrayType())
					collectionType = upperBound; // partially anticipating the fix for https://bugs.openjdk.java.net/browse/JDK-8013843
			}
			if (collectionType.isArrayType()) { // for(E e : E[])
				this.kind = ARRAY;
				this.collectionElementType = ((ArrayBinding) collectionType).elementsType();
				if (!this.collectionElementType.isCompatibleWith(elementType)
						&& !this.scope.isBoxingCompatibleWith(this.collectionElementType, elementType)) {
					this.scope.problemReporter().notCompatibleTypesErrorInForeach(this.collection, this.collectionElementType, elementType);
				} else if (this.collectionElementType.needsUncheckedConversion(elementType)) { // https://bugs.eclipse.org/bugs/show_bug.cgi?id=321085
				    this.scope.problemReporter().unsafeElementTypeConversion(this.collection, this.collectionElementType, elementType);
				}
				// in case we need to do a conversion
				int compileTimeTypeID = this.collectionElementType.id;
				if (elementType.isBaseType()) {
					this.collection.computeConversion(this.scope, collectionType, collectionType);
					if (!this.collectionElementType.isBaseType()) {
						compileTimeTypeID = this.scope.environment().computeBoxingType(this.collectionElementType).id;
						this.elementVariableImplicitWidening = UNBOXING;
						if (elementType.isBaseType()) {
							this.elementVariableImplicitWidening |= (elementType.id << 4) + compileTimeTypeID;
							this.scope.problemReporter().autoboxing(this.collection, this.collectionElementType, elementType);
						}
					} else {
						this.elementVariableImplicitWidening = (elementType.id << 4) + compileTimeTypeID;
					}
				} else if (this.collectionElementType.isBaseType()) {
					this.collection.computeConversion(this.scope, collectionType, collectionType);
					int boxedID = this.scope.environment().computeBoxingType(this.collectionElementType).id;
					this.elementVariableImplicitWidening = BOXING | (compileTimeTypeID << 4) | compileTimeTypeID; // use primitive type in implicit conversion
					compileTimeTypeID = boxedID;
					this.scope.problemReporter().autoboxing(this.collection, this.collectionElementType, elementType);
				} else {
					expectedCollectionType = upperScope.createArrayType(elementType, 1);
					this.collection.computeConversion(this.scope, expectedCollectionType, collectionType);
				}
			} else if (collectionType instanceof ReferenceBinding) {
				ReferenceBinding iterableType = ((ReferenceBinding)collectionType).findSuperTypeOriginatingFrom(T_JavaLangIterable, false /*Iterable is not a class*/);
				if (iterableType == null && isTargetJsr14) {
					iterableType = ((ReferenceBinding)collectionType).findSuperTypeOriginatingFrom(T_JavaUtilCollection, false /*Iterable is not a class*/);
				}
				checkIterable: {
					if (iterableType == null) break checkIterable;

					this.iteratorReceiverType = collectionType.erasure();
					if (isTargetJsr14) {
						if (((ReferenceBinding)this.iteratorReceiverType).findSuperTypeOriginatingFrom(T_JavaUtilCollection, false) == null) {
							this.iteratorReceiverType = iterableType; // handle indirect inheritance thru variable secondary bound
							this.collection.computeConversion(this.scope, iterableType, collectionType);
						} else {
							this.collection.computeConversion(this.scope, collectionType, collectionType);
						}
					} else if (((ReferenceBinding)this.iteratorReceiverType).findSuperTypeOriginatingFrom(T_JavaLangIterable, false) == null) {
						this.iteratorReceiverType = iterableType; // handle indirect inheritance thru variable secondary bound
						this.collection.computeConversion(this.scope, iterableType, collectionType);
					} else {
						this.collection.computeConversion(this.scope, collectionType, collectionType);
					}

					TypeBinding[] arguments = null;
					switch (iterableType.kind()) {
						case Binding.RAW_TYPE : // for(Object o : Iterable)
							this.kind = RAW_ITERABLE;
							this.collectionElementType = this.scope.getJavaLangObject();
							if (!this.collectionElementType.isCompatibleWith(elementType)
									&& !this.scope.isBoxingCompatibleWith(this.collectionElementType, elementType)) {
								this.scope.problemReporter().notCompatibleTypesErrorInForeach(this.collection, this.collectionElementType, elementType);
							}
							// no conversion needed as only for reference types
							break checkIterable;

						case Binding.GENERIC_TYPE : // for (T t : Iterable<T>) - in case used inside Iterable itself
							arguments = iterableType.typeVariables();
							break;

						case Binding.PARAMETERIZED_TYPE : // for(E e : Iterable<E>)
							arguments = ((ParameterizedTypeBinding)iterableType).arguments;
							break;

						default:
							break checkIterable;
					}
					// generic or parameterized case
					if (arguments.length != 1) break checkIterable; // per construction can only be one
					this.kind = GENERIC_ITERABLE;

					this.collectionElementType = arguments[0];
					if (!this.collectionElementType.isCompatibleWith(elementType)
							&& !this.scope.isBoxingCompatibleWith(this.collectionElementType, elementType)) {
						this.scope.problemReporter().notCompatibleTypesErrorInForeach(this.collection, this.collectionElementType, elementType);
					} else if (this.collectionElementType.needsUncheckedConversion(elementType)) { // https://bugs.eclipse.org/bugs/show_bug.cgi?id=393719
					    this.scope.problemReporter().unsafeElementTypeConversion(this.collection, this.collectionElementType, elementType);
					}
					int compileTimeTypeID = this.collectionElementType.id;
					// no conversion needed as only for reference types
					if (elementType.isBaseType()) {
						if (!this.collectionElementType.isBaseType()) {
							compileTimeTypeID = this.scope.environment().computeBoxingType(this.collectionElementType).id;
							this.elementVariableImplicitWidening = UNBOXING;
							if (elementType.isBaseType()) {
								this.elementVariableImplicitWidening |= (elementType.id << 4) + compileTimeTypeID;
							}
						} else {
							this.elementVariableImplicitWidening = (elementType.id << 4) + compileTimeTypeID;
						}
					} else {
						if (this.collectionElementType.isBaseType()) {
							this.elementVariableImplicitWidening = BOXING | (compileTimeTypeID << 4) | compileTimeTypeID; // use primitive type in implicit conversion
						}
					}
				}
			}
			switch(this.kind) {
				case ARRAY :
					// allocate #index secret variable (of type int)
					this.indexVariable = new LocalVariableBinding(SecretIndexVariableName, TypeBinding.INT, ClassFileConstants.AccDefault, false);
					this.scope.addLocalVariable(this.indexVariable);
					this.indexVariable.setConstant(Constant.NotAConstant); // not inlinable
					// allocate #max secret variable
					this.maxVariable = new LocalVariableBinding(SecretMaxVariableName, TypeBinding.INT, ClassFileConstants.AccDefault, false);
					this.scope.addLocalVariable(this.maxVariable);
					this.maxVariable.setConstant(Constant.NotAConstant); // not inlinable
					// add #array secret variable (of collection type)
					if (expectedCollectionType == null) {
						this.collectionVariable = new LocalVariableBinding(SecretCollectionVariableName, collectionType, ClassFileConstants.AccDefault, false);
					} else {
						this.collectionVariable = new LocalVariableBinding(SecretCollectionVariableName, expectedCollectionType, ClassFileConstants.AccDefault, false);
					}
					this.scope.addLocalVariable(this.collectionVariable);
					this.collectionVariable.setConstant(Constant.NotAConstant); // not inlinable
					break;
				case RAW_ITERABLE :
				case GENERIC_ITERABLE :
					// allocate #index secret variable (of type Iterator)
					this.indexVariable = new LocalVariableBinding(SecretIteratorVariableName, this.scope.getJavaUtilIterator(), ClassFileConstants.AccDefault, false);
					this.scope.addLocalVariable(this.indexVariable);
					this.indexVariable.setConstant(Constant.NotAConstant); // not inlinable
					break;
				default :
					if (isTargetJsr14) {
						this.scope.problemReporter().invalidTypeForCollectionTarget14(this.collection);
					} else {
						this.scope.problemReporter().invalidTypeForCollection(this.collection);
					}
			}
		}
		if (this.action != null) {
			this.action.resolveWithPatternVariablesInScope(patternVariablesInTrueScope, this.scope);
		}
	}

	@Override
	public void traverse(
		ASTVisitor visitor,
		BlockScope blockScope) {

		if (visitor.visit(this, blockScope)) {
			this.elementVariable.traverse(visitor, this.scope);
			if (this.collection != null) {
				this.collection.traverse(visitor, this.scope);
			}
			if (this.action != null) {
				this.action.traverse(visitor, this.scope);
			}
		}
		visitor.endVisit(this, blockScope);
	}

	@Override
	public boolean doesNotCompleteNormally() {
		return false; // may not be entered at all.
	}

	@Override
	public boolean canCompleteNormally() {
		return true;
	}

}
