blob: c1da3006f05b4ebdca874bed637a624fc097476c [file] [log] [blame]
/*
*
* 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.apache.royale.utils;
import org.apache.royale.compiler.common.SourceLocation;
import org.apache.royale.compiler.constants.IASLanguageConstants;
import org.apache.royale.compiler.constants.IMetaAttributeConstants;
import org.apache.royale.compiler.definitions.*;
import org.apache.royale.compiler.definitions.metadata.IMetaTag;
import org.apache.royale.compiler.internal.definitions.FunctionDefinition;
import org.apache.royale.compiler.internal.parsing.as.ASToken;
import org.apache.royale.compiler.internal.parsing.as.ASTokenTypes;
import org.apache.royale.compiler.internal.scopes.ASProjectScope;
import org.apache.royale.compiler.internal.scopes.ASScope;
import org.apache.royale.compiler.internal.semantics.PostProcessStep;
import org.apache.royale.compiler.internal.semantics.SemanticUtils;
import org.apache.royale.compiler.internal.tree.as.*;
import org.apache.royale.compiler.problems.ArrayLikeConfigurationErrorProblem;
import org.apache.royale.compiler.problems.ArrayLikeUsageErrorProblem;
import org.apache.royale.compiler.problems.ICompilerProblem;
import org.apache.royale.compiler.projects.ICompilerProject;
import org.apache.royale.compiler.projects.IRoyaleProject;
import org.apache.royale.compiler.tree.as.*;
import java.util.*;
/**
* Support for processing a variety of characteristics via metadata on classes or interfaces
* that translate Array like access and iteration from as3 language level to implementation support for
* the annotated definition(s)
* example:
* [RoyaleArrayLike(getValue="getItemAt",setValue="setItemAt",length="length",lengthAccess="getter")]
*
* This class contains utilities to support both mutating AST of 'for each' loops with target (rightOperand) types that
* are classes or interfaces which have [RoyaleArrayLike] metadata. It also supports mutation of array-like
* dynamic access or assignment when the dynamic access keys are numeric types (int, uint, Number).
* Subclasses of RoyaleArrayLike classes or implementers of interfaces which are RoyaleArrayLike
* 'inherit' the RoyaleArrayLike special treatment by the compiler.
*/
public class ArrayLikeUtil
{
//the metadata arg that specifies the member name for accessing length value of and instance of the decorated type
final private static String LENGTH_ARG = "length";
//the metadata arg that specifies the type of access that the length arg specifies ('getter' or 'method', default value is 'getter', see defaults below)
final private static String LENGTH_ACCESS_ARG = "lengthAccess";
//the metadata arg that specifies how numeric index level 'get' is performed
final private static String GETVALUE_ARG = "getValue";
//the metadata arg that specifies how numeric index level 'set' is performed
final private static String SETVALUE_ARG = "setValue";
//the metadata arg that specifies the order of arguments used in the setter ('value,index' or 'index,value', default value is 'value,index')
final private static String SETTER_ARG_SEQUENCE_ARG = "setterArgSequence";
//the valid SETTER_ARG_SEQUENCE_ARG value options.
//Whether the 'setter' has the index arg first or the value to be set at the specified index (or vice versa)
//checking the method signature in the class to determine this is not appropriate, because in the general case
//there may be classes whose values are also typed as numeric, so it should be specified. (default is value,index, see defaults below)
final private static String SETTER_ARG_SEQUENCE_VALUE_INDEX = "value,index";
final private static String SETTER_ARG_SEQUENCE_INDEX_VALUE = "index,value";
//the valid options for length access. Used when generated iteration support in for-each loops
final private static String LENGTH_ACCESS_METHOD = "method";
final private static String LENGTH_ACCESS_GETTER = "getter";
//a value for either getValue or setValue that indicates regular array access should be used,
//This results in no alteration compared to original source code for either the 'getter' or 'setter'
// (or both, if both 'getValue' and 'setValue' have this value)
final private static String INDEX_ACCESS_UNCHANGED = "[]";
//default values for certain args that can be omitted.
final private static String LENGTH_ACCESS_DEFAULT = LENGTH_ACCESS_GETTER;
final private static String SETTER_ARG_SEQUENCE_DEFAULT = SETTER_ARG_SEQUENCE_VALUE_INDEX;
//this is currently 'assumed safe', based on the chance of naming collision being extremely unlikely
//with an actual user-coded variable name. It also assumes that the target platform will optimize its own local variable names,
//therefore there is no attempt to make a 'short' name.
final private static String ARRAY_LIKE_FOREACH_ITERATOR_VARNAME_BASE = "royale$for$Each$Iterator";
/**
* todo Consider the possibility of specific iterator factories instead of one generic function for all... the qName for the specific function could be specified in the Metadata def.
* These could simply take the instance as an argument and already know the way to construct the 'iterator' because they are specific
* (e.g. collections could have a specific iterator function, XMLish its own, BinaryData its own etc).
*/
private final static String ARRAYLIKE_GENERIC_SUPPORT_ITERATOR_FACTORY_FUNC_QNAME = "org.apache.royale.language.iterator.arrayLike";
private final static String ARRAYLIKE_GENERIC_SUPPORT_ITERATOR_FACTORY_FUNC = "arrayLike";
final static String ARRAYLIKE_HAS_NEXT = "hasNext";
final static String ARRAYLIKE_GET_NEXT = "next";
private static String wrapQuotes(String original){
return "'" + original + "'";
}
/**
* pre-processes dynamic access nodes, replacing them where applicable with get or set value calls
* on the arrayLike instance
*/
public static void preProcessGetterSetters(ICompilerProject project, ContainerNode node, NodeBase lower ) {
if (!(project instanceof IRoyaleProject)) {
return;
}
NodeBase parent = lower != null ? lower : node;
int childCount = parent.getChildCount();
for (int i=0; i < childCount; i++) {
IASNode child = parent.getChild(i);
//future: consider ways to consolidate/abstract these various checks
if (child instanceof BinaryOperatorAssignmentNode) {
if (((BinaryOperatorAssignmentNode) child).getLeftOperandNode() instanceof DynamicAccessNode) {
DynamicAccessNode dynNode = (DynamicAccessNode) ((BinaryOperatorAssignmentNode) child).getLeftOperandNode();
if (isArrayLikeCandidate(dynNode, project)) {
IDefinition target = dynNode.getLeftOperandNode().resolveType(project);
IDefinition metaSource = ArrayLikeUtil.resolveArrayLikeDefinitionSource(target, project);
IMetaTag arrayLikeTag = ArrayLikeUtil.getArrayLikeMetaData(metaSource);
String setterArg;
setterArg = getSetterArg(arrayLikeTag);
if (!setterArg.equals(INDEX_ACCESS_UNCHANGED)) { //otherwise we leave it as is
//get the FunctionCallNode replacement's arguments from the original assignment
ExpressionNodeBase indexArg = (ExpressionNodeBase) dynNode.getRightOperandNode();
ExpressionNodeBase valueArg = (ExpressionNodeBase) ((BinaryOperatorAssignmentNode) child).getRightOperandNode();
FunctionCallNode replacement = createDynamicAccessMutation(dynNode, setterArg);
String argSequence = getSetterArgSequenceArg(arrayLikeTag);
if (argSequence.equals(SETTER_ARG_SEQUENCE_VALUE_INDEX)) {
replacement.getArgumentsNode().addChild(valueArg);
replacement.getArgumentsNode().addChild(indexArg);
} else { //index,value instead
replacement.getArgumentsNode().addChild(indexArg);
replacement.getArgumentsNode().addChild(valueArg);
}
replacement.getArgumentsNode().setParent(replacement);
if (parent instanceof ContainerNode) {
((ContainerNode) parent).removeItem((NodeBase) child);
((ContainerNode) parent).addChild(replacement, i);
((BinaryOperatorAssignmentNode) child).setParent(null);
child = replacement; //we need to allow for recursive checking of the assigned value as well as some other 'getter'
} /*else {
System.out.println("Problem: " + (node.getNodeID() + ":" + node.getLine()));
}*/
}
}
}
} else if (child instanceof DynamicAccessNode) {
DynamicAccessNode dynNode = (DynamicAccessNode)child;
if (isArrayLikeCandidate(dynNode, project)) {
if (parent instanceof BaseStatementExpressionNode) {
IDefinition target = dynNode.getLeftOperandNode().resolveType(project);
IDefinition metaSource = ArrayLikeUtil.resolveArrayLikeDefinitionSource(target, project);
IMetaTag arrayLikeTag = ArrayLikeUtil.getArrayLikeMetaData(metaSource);
String getterArg = getGetterArg(arrayLikeTag);
if (!getterArg.equals(INDEX_ACCESS_UNCHANGED)) {
ExpressionNodeBase indexArg = (ExpressionNodeBase) dynNode.getRightOperandNode();
FunctionCallNode replacement = createDynamicAccessMutation(dynNode, getterArg);
replacement.getArgumentsNode().addChild(indexArg);
replacement.getArgumentsNode().setParent(replacement);
((BaseStatementExpressionNode) parent).setStatementExpression(replacement);
replacement.setParent( parent);
dynNode.setParent(null);
child = replacement;
}
}
else if (parent instanceof ContainerNode) {
IDefinition target = dynNode.getLeftOperandNode().resolveType(project);
IDefinition metaSource = ArrayLikeUtil.resolveArrayLikeDefinitionSource(target, project);
IMetaTag arrayLikeTag = ArrayLikeUtil.getArrayLikeMetaData(metaSource);
String getterArg = getGetterArg(arrayLikeTag);
if (!getterArg.equals(INDEX_ACCESS_UNCHANGED)) { //otherwise we leave it as is
ExpressionNodeBase indexArg = (ExpressionNodeBase) dynNode.getRightOperandNode();
FunctionCallNode replacement = createDynamicAccessMutation(dynNode, getterArg);
replacement.getArgumentsNode().addChild(indexArg);
replacement.getArgumentsNode().setParent(replacement);
((ContainerNode) parent).removeItem((NodeBase) child);
((ContainerNode) parent).addChild(replacement, i);
dynNode.setParent(null);
child = replacement;
}
} else if (parent instanceof BinaryOperatorNodeBase) {
BinaryOperatorNodeBase binaryOp = (BinaryOperatorNodeBase) parent;
IDefinition target = dynNode.getLeftOperandNode().resolveType(project);
IDefinition metaSource = ArrayLikeUtil.resolveArrayLikeDefinitionSource(target, project);
IMetaTag arrayLikeTag = ArrayLikeUtil.getArrayLikeMetaData(metaSource);
String getterArg = getGetterArg(arrayLikeTag);
if (!getterArg.equals(INDEX_ACCESS_UNCHANGED)) { //otherwise we leave it as is
ExpressionNodeBase indexArg = (ExpressionNodeBase) dynNode.getRightOperandNode();
FunctionCallNode replacement = createDynamicAccessMutation(dynNode, getterArg);
replacement.getArgumentsNode().addChild(indexArg);
replacement.getArgumentsNode().setParent(replacement);
if (binaryOp.getLeftOperandNode() == child) {
binaryOp.setLeftOperandNode(replacement);
} else {
binaryOp.setRightOperandNode(replacement);
}
replacement.setParent(binaryOp);
dynNode.setParent(null);
child = replacement;
}
} else if (parent instanceof VariableNode) {
VariableNode variableNode = (VariableNode) parent;
IDefinition target = dynNode.getLeftOperandNode().resolveType(project);
IDefinition metaSource = ArrayLikeUtil.resolveArrayLikeDefinitionSource(target, project);
IMetaTag arrayLikeTag = ArrayLikeUtil.getArrayLikeMetaData(metaSource);
String getterArg = getGetterArg(arrayLikeTag);
if (!getterArg.equals(INDEX_ACCESS_UNCHANGED)) { //otherwise we leave it as is
ExpressionNodeBase indexArg = (ExpressionNodeBase) dynNode.getRightOperandNode();
FunctionCallNode replacement = createDynamicAccessMutation(dynNode, getterArg);
replacement.getArgumentsNode().addChild(indexArg);
replacement.getArgumentsNode().setParent(replacement);
variableNode.setAssignedValue(null, replacement);
replacement.setParent(variableNode);
dynNode.setParent(null);
child = replacement;
}
} else if (parent instanceof UnaryOperatorNodeBase) {
IDefinition target = dynNode.getLeftOperandNode().resolveType(project);
IDefinition metaSource = ArrayLikeUtil.resolveArrayLikeDefinitionSource(target, project);
IMetaTag arrayLikeTag = ArrayLikeUtil.getArrayLikeMetaData(metaSource);
String getterArg = getGetterArg(arrayLikeTag);
String setterArg = getSetterArg(arrayLikeTag);
if (!(INDEX_ACCESS_UNCHANGED.equals(getterArg) && INDEX_ACCESS_UNCHANGED.equals(setterArg))) {
//report an error - unless we have native get/set bracket [] access (where we can assume it should work), we can't easily migrate unary operators
UnaryOperatorNodeBase unaryOperatorNodeBase = (UnaryOperatorNodeBase) parent;
ArrayLikeUsageErrorProblem usageError;
SourceLocation loc = new SourceLocation();
loc.setSourcePath(unaryOperatorNodeBase.getSourcePath());
if (unaryOperatorNodeBase.getOperandNode().getAbsoluteStart() > unaryOperatorNodeBase.getOperatorAbsoluteStart()) {
//prepended unary operator
loc.setColumn(unaryOperatorNodeBase.getColumn());
loc.setLine(unaryOperatorNodeBase.getLine());
loc.setEndColumn(unaryOperatorNodeBase.getOperatorAbsoluteStart() - unaryOperatorNodeBase.getOperandNode().getAbsoluteStart());
loc.setEndLine(unaryOperatorNodeBase.getLine());
loc.setStart(unaryOperatorNodeBase.getStart());
loc.setEnd(unaryOperatorNodeBase.getOperatorAbsoluteStart() - 1);
} else {
//post-pended unary operator
loc.setColumn(unaryOperatorNodeBase.getOperandNode().getEndColumn());
loc.setLine(unaryOperatorNodeBase.getOperandNode().getEndLine());
loc.setEndColumn(unaryOperatorNodeBase.getEndColumn());
loc.setEndLine(unaryOperatorNodeBase.getEndLine());
loc.setStart(unaryOperatorNodeBase.getOperandNode().getAbsoluteStart());
loc.setStart(unaryOperatorNodeBase.getAbsoluteEnd());
}
usageError = new ArrayLikeUsageErrorProblem(loc,
"Unary Operation not supported");
project.getProblems().add(usageError);
}
} /*else {
System.out.println("Skipped: " + (child.getNodeID()));
}*/
}
}/* else {
System.out.println("Skipped: " + (child.getNodeID()));
}*/
if (child instanceof FixedChildrenNode || child instanceof TreeNode) {
preProcessGetterSetters(/*searchScope,*/ project, node, (NodeBase) child);
}
}
}
private static boolean isArrayLikeCandidate(DynamicAccessNode dynNode, ICompilerProject project) {
if (!(project instanceof IRoyaleProject)) {
return false;
}
boolean isCandidate = false;
//first check to see if the access is numeric... if it is not then we consider that it is not a candidate
IDefinition dynType = dynNode.getRightOperandNode().resolveType(project);
if (project.getBuiltinType(IASLanguageConstants.BuiltinType.NUMBER).equals(dynType)
|| project.getBuiltinType(IASLanguageConstants.BuiltinType.UINT).equals(dynType)
|| project.getBuiltinType(IASLanguageConstants.BuiltinType.INT).equals(dynType)
) {
IDefinition target = dynNode.getLeftOperandNode().resolveType(project);
isCandidate = ArrayLikeUtil.isArrayLike(target, project);
}
return isCandidate;
}
private static FunctionCallNode createDynamicAccessMutation(DynamicAccessNode original, String methodName){
ExpressionNodeBase base = (ExpressionNodeBase) original.getLeftOperandNode();
IdentifierNode methodCallName = new IdentifierNode(methodName);
MemberAccessExpressionNode nameNode;
nameNode = new MemberAccessExpressionNode(base, null, methodCallName);
base.setParent(nameNode);
methodCallName.setParent(nameNode);
FunctionCallNode replacement = new FunctionCallNode(nameNode);
nameNode.setParent(replacement);
//the scaffolding for the replacement is set up... adding the arguments will be managed by the call site
return replacement;
}
/**
* main support for loop mutations to support RoyaleArrayLikes as targets of 'for each' loops
* @param searchScope
* @param project
*/
public static void preProcessLoopChecks(ASScope searchScope, IRoyaleProject project) {
ScopedBlockNode funcScopeNode = (ScopedBlockNode) searchScope.getScopeNode();
IForLoopNode[] forLoops = searchScope.getLoopChecks(true);
List<IForLoopNode> forLoopList = Arrays.asList(forLoops);
boolean importAdded = false;
ArrayList<String> usedIterators = new ArrayList<String>();
for (IForLoopNode loopNode : forLoopList) {
int depth = 0;
IASNode nodeCheck = loopNode;
while (nodeCheck.getParent() != null && nodeCheck.getParent() != funcScopeNode) {
if (nodeCheck.getParent() instanceof IForLoopNode && forLoopList.indexOf(nodeCheck.getParent()) != -1) {
depth++;
}
nodeCheck = nodeCheck.getParent();
}
//are we dealing with a regular for..in or a for each..in loop:
boolean isForeach = loopNode.getKind() == IForLoopNode.ForLoopKind.FOR_EACH;
//create a valid name for the iterator at the current depth of for-each loops, re-using previous declared variables, where possible
String arrIter = ARRAY_LIKE_FOREACH_ITERATOR_VARNAME_BASE + depth;
IDefinition targetType;
final IDefinition xmlListDef = project.getBuiltinType(IASLanguageConstants.BuiltinType.XMLLIST);
try {
//check the rightOperandType
BinaryOperatorInNode conditionalExpressions = (BinaryOperatorInNode) loopNode.getConditionalExpressionNodes()[0];
if (conditionalExpressions.getRightOperandNode() instanceof IFunctionCallNode) {
IASNode nameNode = ((FunctionCallNode) conditionalExpressions.getRightOperandNode()).getNameNode();
if (nameNode instanceof IMemberAccessExpressionNode) {
IFunctionDefinition funcDef = (IFunctionDefinition) ((IMemberAccessExpressionNode) nameNode).getRightOperandNode().resolve(project);
targetType = funcDef.resolveReturnType(project);
} else {
//back to error until we cover all cases
targetType = conditionalExpressions.getRightOperandNode().resolveType(project);
}
} else {
if (conditionalExpressions.getRightOperandNode() instanceof IMemberAccessExpressionNode) {
//this does not resolve... check for XMLish on the left
targetType = conditionalExpressions.getRightOperandNode().resolveType(project);
IExpressionNode left = ((IMemberAccessExpressionNode) conditionalExpressions.getRightOperandNode()).getLeftOperandNode();
while (targetType == null && left != null) {
targetType = left.resolveType(project);
if (targetType == null) {
if (left instanceof IMemberAccessExpressionNode || left instanceof IDynamicAccessNode) {
left = ((IBinaryOperatorNode) left).getLeftOperandNode();
} else {
left = null;
}
} else {
if (SemanticUtils.isXMLish(targetType, project)) {
//assume we have an XMLList
targetType = xmlListDef;
}
if (targetType != xmlListDef) {
//this does not make sense, so ignore it or add a problem?
targetType = null;
left = null;
} /*else {
System.out.println("Searching, found left target type to "+targetType.getQualifiedName());
}*/
}
}
} else {
targetType = conditionalExpressions.getRightOperandNode().resolveType(project);
}
}
} catch (Exception e) {
/* System.out.println("Failed to resolve the target type ");
System.out.println(e.getStackTrace());*/
continue;
}
if (ArrayLikeUtil.isArrayLike(targetType, project)) {
// System.out.println("processing ArrayLike "+targetType.getQualifiedName());
if (!importAdded) {
if (searchScope.getImports() != null) {
List<String> imports = Arrays.asList(searchScope.getImports());
if (imports.contains(ArrayLikeUtil.ARRAYLIKE_GENERIC_SUPPORT_ITERATOR_FACTORY_FUNC_QNAME)) {
importAdded = true;
}
}
if (!importAdded) {
searchScope.addImport(ArrayLikeUtil.ARRAYLIKE_GENERIC_SUPPORT_ITERATOR_FACTORY_FUNC_QNAME);
importAdded = true;
}
}
ASProjectScope projectScope = (ASProjectScope) project.getScope();
boolean alreadyUsed = usedIterators.contains(arrIter); //otherwise we need a typed var declaration
if (!alreadyUsed)
usedIterators.add(arrIter); // we can use it again without var declaration in the current scope
IDefinition metaSource = ArrayLikeUtil.resolveArrayLikeDefinitionSource(targetType, project);
IMetaTag arrayLikeTag = ArrayLikeUtil.getArrayLikeMetaData(metaSource);
//change the loop node to: for(arrIter:Object = arrayLike(instance,"{lengthCheck}","{getterCheck}" or (null if getterCheck=="[]"), Boolean(lengthAccess=="method"), Boolean(!isForEach)); arrIter.hasNext();)
// {
// {!alreadyUsed: var} originalName{!alreadyUsed: :OriginalType} = arrIter.getNext();
// {originalBody Loop body}
// }
boolean useDynamicAccess = !project.isStaticTypedTarget();
ForLoopNode.ILoopMutation mutation = ArrayLikeUtil.createArrayLikeLoopMutation((ForLoopNode) loopNode, arrIter, useDynamicAccess);
String generatorFuncName = ArrayLikeUtil.ARRAYLIKE_GENERIC_SUPPORT_ITERATOR_FACTORY_FUNC;
IdentifierNode funcName = new IdentifierNode(generatorFuncName);
FunctionCallNode specificIteratorFunc = new FunctionCallNode(funcName);
specificIteratorFunc.getNameNode().setParent(specificIteratorFunc);
//now add the arguments
//use the original iteration target node {from: for each(iteratee in target) }, reparent it to the generator function args as the target 'instance'
specificIteratorFunc.getArgumentsNode().addItem((NodeBase) mutation.getIterationTarget());
specificIteratorFunc.getArgumentsNode().setParent(specificIteratorFunc);
//now add the metadata driven args
//length check:
LiteralNode metaArg = new LiteralNode(ILiteralNode.LiteralType.STRING, wrapQuotes(ArrayLikeUtil.getLengthArg(arrayLikeTag)));
metaArg.setSynthetic(true); //may not be needed
specificIteratorFunc.getArgumentsNode().addItem(metaArg);
//index getter:
String getter = ArrayLikeUtil.getGetterArg(arrayLikeTag);
if (getter.equals(INDEX_ACCESS_UNCHANGED)) {
// pass null for the getter
metaArg = new LiteralNode(ILiteralNode.LiteralType.NULL, "null");
metaArg.setSynthetic(true); //may not be needed
} else {
metaArg = new LiteralNode(ILiteralNode.LiteralType.STRING, wrapQuotes(ArrayLikeUtil.getGetterArg(arrayLikeTag)));
metaArg.setSynthetic(true); //may not be needed
}
specificIteratorFunc.getArgumentsNode().addItem(metaArg);
//boolean indicating true if the length check is a method call, false if reguler 'getter' style:
metaArg = new LiteralNode(ILiteralNode.LiteralType.BOOLEAN, ArrayLikeUtil.getLengthAccessArg(arrayLikeTag).equals("method") ? "true" : "false");
metaArg.setSynthetic(true); //may not be needed
specificIteratorFunc.getArgumentsNode().addItem(metaArg);
//distinguish whether this iterator is values (for each(x in y)) or keys (regular for (x in y))
if (!isForeach) {
//we add an extra boolean true indicating that we only want to iterate over keys (and not values)
metaArg = new LiteralNode(ILiteralNode.LiteralType.BOOLEAN, "true");
metaArg.setSynthetic(true); //may not be needed
specificIteratorFunc.getArgumentsNode().addItem(metaArg);
}
mutation.prepareConditionals(!alreadyUsed, specificIteratorFunc);
mutation.prepareContent();
Collection<ICompilerProblem> problems = ((ForLoopNode) loopNode).processMutation(mutation, searchScope);
if (problems.size() > 0) {
project.getProblems().addAll(problems);
}
}
//@todo if the targetType is null we could output an opt-in or opt-out (via config) warning suggesting that Strongly typed for each loop targets are more reliable
//That would help people a lot when porting things to dynamic targets like js. for each(var something:Object in event.target.someField).... is sometimes a problem
/*if (targetType == null){
System.out.println("FOUND NULL FOREACH TARGET TYPE AT "+loopNode.getLine()+","+loopNode.getSourcePath());
} else {
System.out.println("Target type was "+targetType.getQualifiedName() + " at " + loopNode.getLine() +"," +loopNode.getSourcePath());
}*/
}
}
private static int currentProject = -1;
private static HashMap<String, String> arrayLikeLookups = null;
private static void resetLookupsIfNeeded(ICompilerProject project) {
if (currentProject == project.hashCode()) return;
arrayLikeLookups = new HashMap<String, String>(20);
currentProject = project.hashCode();
}
/**
*
* @param definition the definition to check
* @return true if this specific definition has the ArrayLike annotation
*/
public static boolean definitionIsArrayLike(IDefinition definition) {
return ((definition instanceof IClassDefinition || definition instanceof IInterfaceDefinition) &&
definition.hasMetaTagByName(IMetaAttributeConstants.ATTRIBUTE_ARRAYLIKE));
}
/**
* Method to check an annotated definition in the current project, and verify that it is valid
* This method poplulates the external problems collection argument with the first observed problem
* @param definition the definition to verify
* @param project the current project
* @param problems a problem list to populate with any detected issues
* @return boolean true if there were no issues, otherwise false
*/
public static boolean validateArrayLikeDefinition(IDefinition definition, ICompilerProject project, List<ICompilerProblem> problems) {
IMetaTag arrayLikeTag = definition.getMetaTagByName(IMetaAttributeConstants.ATTRIBUTE_ARRAYLIKE);
if (!(project instanceof IRoyaleProject)) {
if (problems != null) {
problems.add(new ArrayLikeConfigurationErrorProblem(arrayLikeTag, "This is not a Royale Project. Only Royale projects support "+IMetaAttributeConstants.ATTRIBUTE_ARRAYLIKE));
}
return false;
}
//mandatory
String lengthCheck = arrayLikeTag.getAttributeValue(LENGTH_ARG);
boolean pass = true;
if (lengthCheck == null) {
pass = false;
if (problems != null) {
problems.add(new ArrayLikeConfigurationErrorProblem(arrayLikeTag, "Missing '"+LENGTH_ARG+"' metadata argument"));
}
} //verify further after we check the lengthAccess arg
if (pass) {
String getterCheck = arrayLikeTag.getAttributeValue(GETVALUE_ARG);
if (getterCheck == null) {
pass = false;
if (problems != null) {
problems.add(new ArrayLikeConfigurationErrorProblem(arrayLikeTag, "Missing '"+GETVALUE_ARG+"' metadata argument"));
}
} else {
//check the definition
if (getterCheck != INDEX_ACCESS_UNCHANGED) { //'[]' is a special case where regular Array access is not changed
//@todo extra safety: check the definition has the method that is specified in the metadata, add a problem if not.
}
}
}
String setterCheck = null;
if (pass) {
setterCheck = arrayLikeTag.getAttributeValue(SETVALUE_ARG);
if (setterCheck == null) {
pass = false;
if (problems != null) {
problems.add(new ArrayLikeConfigurationErrorProblem(arrayLikeTag, "Missing '"+SETVALUE_ARG+"' metadata argument"));
}
} else {
//check the definition
if (setterCheck != INDEX_ACCESS_UNCHANGED) {//'[]' is a special case where regular Array access is not changed
//@todo extra safety: check the definition has the method that is specified in the metadata, add a problem if not.
}
}
}
if (pass) {
if (!setterCheck.equals(INDEX_ACCESS_UNCHANGED)) {//otherwise we can ignore the setter arg sequence, even if it is (incorrectly) specified.
String setterSequenceCheck = arrayLikeTag.getAttributeValue(SETTER_ARG_SEQUENCE_ARG);
if (setterSequenceCheck == null) {
setterSequenceCheck = SETTER_ARG_SEQUENCE_DEFAULT;
}
if (!setterSequenceCheck.equals(SETTER_ARG_SEQUENCE_VALUE_INDEX) && !setterSequenceCheck.equals(SETTER_ARG_SEQUENCE_INDEX_VALUE)) {
pass = false;
if (problems != null) {
problems.add(new ArrayLikeConfigurationErrorProblem(arrayLikeTag, "Missing '"+SETVALUE_ARG+"' metadata argument"));
}
} else {
//check the definition
//@todo extra safety: check the definition has the method signature that is specified in the metadata, add a problem if not.
//the index argument should be a numeric type, and the specified method should have the two arguments
}
}
}
if (pass) {
//can be method or getter, defaults to getter
String lengthAccess = arrayLikeTag.getAttributeValue(LENGTH_ACCESS_ARG);
if (lengthAccess == null) lengthAccess = LENGTH_ACCESS_DEFAULT;
//only "getter" or "method" are allowed options
if (!lengthAccess.equals(LENGTH_ACCESS_GETTER) && !lengthAccess.equals(LENGTH_ACCESS_METHOD)) {
pass = false;
if (problems != null) {
//problems.add(new ArrayLikeConfigurationErrorProblem(definition.getNode(), "Metadata argument for 'lengthAccess' missing or invalid"));
problems.add(new ArrayLikeConfigurationErrorProblem(arrayLikeTag, "Metadata argument for '"+LENGTH_ACCESS_ARG+"' missing or invalid"));
}
} else {
//@todo extra safety: check the definition has the length getter or method that is specified that is specified in the metadata, add a problem if not.
}
}
if (pass) {
ASProjectScope projectScope = ((ASProjectScope)project.getScope());
IDefinition def = projectScope.findDefinitionByName(ARRAYLIKE_GENERIC_SUPPORT_ITERATOR_FACTORY_FUNC_QNAME);
if (!(def instanceof FunctionDefinition)) {
//@todo extra safety: this may not be sufficient to verify a concrete reference
problems.add(new ArrayLikeConfigurationErrorProblem(arrayLikeTag, "The RoyaleArrayLike Tag is valid, but there is a missing concrete reference in the project to: "+ARRAYLIKE_GENERIC_SUPPORT_ITERATOR_FACTORY_FUNC_QNAME+ " (which is required)"));
pass = false;
}
}
return pass;
}
/**
* Checks whether an instance is ArrayLike at a usage site.
* This is typically either array access or assignment, or for the target of a 'for each' loop
* @param definition the definition check to verify if an instance is ArrayLike at a usage site
* @return true if this definition either has its own ArrayLike annotation, or inherits via its ancestors or interfaces
*/
synchronized public static boolean isArrayLike(IDefinition definition, ICompilerProject project) {
if (definition != null && project instanceof IRoyaleProject) {
resetLookupsIfNeeded(project);
if (definition instanceof IClassDefinition) {
String qName = definition.getQualifiedName();
if (arrayLikeLookups.containsKey(qName)) return arrayLikeLookups.get(qName) != null;
return checkClass((IClassDefinition) definition, (IRoyaleProject) project);
} else if (definition instanceof IInterfaceDefinition) {
String qName = definition.getQualifiedName();
if (arrayLikeLookups.containsKey(qName)) return arrayLikeLookups.get(qName) != null;
return checkInterface((IInterfaceDefinition) definition, (IRoyaleProject) project);
}
}
return false;
}
private static boolean checkClass(IClassDefinition definition, IRoyaleProject project){
String qName = definition.getQualifiedName();
resetLookupsIfNeeded(project);
if (arrayLikeLookups.containsKey(qName)) return arrayLikeLookups.get(qName) != null;
boolean isArrayLike = false;
//check the class
isArrayLike = definition.hasMetaTagByName(IMetaAttributeConstants.ATTRIBUTE_ARRAYLIKE);
String originalQName = qName;
if (!isArrayLike) {
//check this definition's interfaces and their ancestry
Set<IInterfaceDefinition> interfaces = definition.resolveAllInterfaces(project);
for (IInterfaceDefinition interfaceDef:interfaces) {
isArrayLike = checkInterface(interfaceDef, project);
if (isArrayLike) {
qName = arrayLikeLookups.get(interfaceDef.getQualifiedName());
break;
}
}
//check this definition's parent class (recursive check)
if (!isArrayLike) {
IClassDefinition baseClassDef = definition.resolveBaseClass(project);
if (baseClassDef != null) {
isArrayLike = checkClass(baseClassDef, project);
if (isArrayLike) {
//we need to share the same lookup that was used when checking the parent chain
qName = arrayLikeLookups.get(baseClassDef.getQualifiedName());
}
}
}
}
arrayLikeLookups.put(originalQName, isArrayLike ? qName : null);
return isArrayLike;
}
private static boolean checkInterface(IInterfaceDefinition interfaceDefinition, IRoyaleProject project){
String qName = interfaceDefinition.getQualifiedName();
resetLookupsIfNeeded(project);
if (arrayLikeLookups.containsKey(qName)) return arrayLikeLookups.get(qName) != null;
boolean isArrayLike = interfaceDefinition.hasMetaTagByName(IMetaAttributeConstants.ATTRIBUTE_ARRAYLIKE);
String originalQName = qName;
if (!isArrayLike) qName = null;
ArrayList<String> ancestry = null;
if (!isArrayLike) {
// if we find an ancestor in the interface extension chain, we can cache the lookups back to it as the source of ArrayLike data
ancestry = new ArrayList<String>();
Iterator<IInterfaceDefinition> interfaceIterator = interfaceDefinition.interfaceIterator(project, false);
while (interfaceIterator.hasNext()) {
interfaceDefinition = interfaceIterator.next();
qName = interfaceDefinition.getQualifiedName();
if (arrayLikeLookups.containsKey(qName) ){
//we don't have to keep going, we now already know the answer
qName = arrayLikeLookups.get(qName); //this will either be null or the correct mapping to the ArrayLike source definition qName
break;
}
ancestry.add(qName);
isArrayLike = interfaceDefinition.hasMetaTagByName(IMetaAttributeConstants.ATTRIBUTE_ARRAYLIKE);
if (isArrayLike) break;
qName = null;
}
}
if (ancestry != null) {
//process ancestry lookups
for (String ancestorQName: ancestry) arrayLikeLookups.put(ancestorQName, qName);
}
arrayLikeLookups.put(originalQName, qName);
return isArrayLike;
}
public static IDefinition resolveArrayLikeDefinitionSource(IDefinition sourceDefinition, ICompilerProject project){
IDefinition metaSource = null;
if (sourceDefinition != null){
resetLookupsIfNeeded(project);
String sourceDefinitionQName = sourceDefinition.getQualifiedName();
if (arrayLikeLookups.containsKey(sourceDefinitionQName)) {
String resolvedQName = arrayLikeLookups.get(sourceDefinitionQName);
if (!sourceDefinitionQName.equals(resolvedQName)) {
metaSource = project.resolveQNameToDefinition(resolvedQName);
} else {
metaSource = sourceDefinition;
}
}
}
return metaSource;
}
/**
* Before calling the following methods, isArrayLike should be verified, and the source of the metadata should be used
* via resolveArrayLikeDefinitionSource
* @param definition
* @return
*/
public static IMetaTag getArrayLikeMetaData(IDefinition definition){
return definition.getMetaTagByName(IMetaAttributeConstants.ATTRIBUTE_ARRAYLIKE);
}
public static String getLengthArg(IMetaTag arrayLikeTag) {
return arrayLikeTag.getAttributeValue(LENGTH_ARG);
}
public static String getGetterArg(IMetaTag arrayLikeTag) {
return arrayLikeTag.getAttributeValue(GETVALUE_ARG);
}
public static String getSetterArg(IMetaTag arrayLikeTag) {
return arrayLikeTag.getAttributeValue(SETVALUE_ARG);
}
public static String getSetterArgSequenceArg(IMetaTag arrayLikeTag) {
String val = arrayLikeTag.getAttributeValue(SETTER_ARG_SEQUENCE_ARG);
if (val == null) val = SETTER_ARG_SEQUENCE_DEFAULT;
return val;
}
public static String getLengthAccessArg(IMetaTag arrayLikeTag) {
String val = arrayLikeTag.getAttributeValue(LENGTH_ACCESS_ARG);
if (val == null) val = LENGTH_ACCESS_DEFAULT;
return val;
}
public static ArrayLikeLoopMutation createArrayLikeLoopMutation(ForLoopNode loopNode, String arrIterName, boolean useDynamicAccess) {
return new ArrayLikeLoopMutation(loopNode, arrIterName, useDynamicAccess);
}
}
/**
* Support for mutating a 'for each' loop with the following signature
* for each(var iteratee:IterateeType in someInstance){ //swf has implicit cast here.
* {loop body}
* }
*
* into the following style of loop:
* for(var iteratorVarName:Object=arraylike(someInstance, { plus metadata driven data here}); iteratorVarName.hasNext(); ) {
* var iteratee:IterateeType = iteratorVarName.getNext(); //with implicit cast here
* {loop body}
* }
* someInstance is a class annotated with [RoyaleArrayLike] metadata that determines its eligibility for applying this change
* and also the specific data to pass to the arrayLike function which serves as an 'iterator factory' for the above 'for each' support
* An example would be [RoyaleArrayLike(getValue="getItemAt",setValue="setItemAt",setterArgSequence="item,index",length="length",lengthAccess="getter")]
* (The setValue and setterArgSequence metatag params are not needed in terms of for-each support... they, along with the getValue arg are to support
* transforming Array Access via [numericKey] Dynamic access to method call access for setting and getting values)
* This only works when the compiler knows the type of 'someInstance', otherwise it cannot check to see if it has an ArrayLike metatag
*/
class ArrayLikeLoopMutation implements ForLoopNode.ILoopMutation {
/**
* Constructor.
*/
public ArrayLikeLoopMutation(ForLoopNode target, String iteratorVarName, boolean useDynamicAccess)
{
super();
this.target = target;
this.iteratorVarName = iteratorVarName;
this.useDynamicAccess = useDynamicAccess;
}
final private IForLoopNode.ForLoopKind kind = IForLoopNode.ForLoopKind.FOR;
private boolean useDynamicAccess;
private String iteratorVarName;
private NodeBase iteratee;
private FunctionCallNode iterateeValueNode;
private ForLoopNode target;
private ArrayList<NodeBase> analyzeRequests;
private ContainerNode mutatedConditionals = new ContainerNode();
public boolean isValid(){
return mutatedConditionals !=null
&& mutatedConditionals.getChildCount() == 3
&& iteratee != null;
}
@Override
public List<NodeBase> getAnalyzeRequests() {
return analyzeRequests;
}
private FunctionCallNode createIteratorCall(String methodName){
IdentifierNode leftOp = new IdentifierNode(this.iteratorVarName);
ExpressionNodeBase nameNode;
if (useDynamicAccess) {
//this is the easiest way to keep things lightweight for JS without needing an actual iterator class or interface
//use iterator['hasNext']()/iterator['getNext']() etc
LiteralNode rightOp = new LiteralNode(ILiteralNode.LiteralType.STRING, "'"+methodName+"'");
DynamicAccessNode dynNode = new DynamicAccessNode(leftOp);
dynNode.setRightOperandNode(rightOp);
rightOp.setParent(dynNode);
leftOp.setParent(dynNode);
nameNode = dynNode;
} else {
IdentifierNode rightOp = new IdentifierNode(methodName);
nameNode = new MemberAccessExpressionNode(leftOp,null, rightOp);
leftOp.setParent(nameNode);
rightOp.setParent(nameNode);
}
FunctionCallNode functionCallNode = new FunctionCallNode(nameNode);
nameNode.setParent(functionCallNode);
functionCallNode.getArgumentsNode().setParent(functionCallNode);
return functionCallNode;
}
private NodeBase setupIterator(boolean firstUse, FunctionCallNode assignedValue){
IdentifierNode varName = new IdentifierNode(this.iteratorVarName);
NodeBase setupNode;
if (firstUse) {
//create something like: var iteratorName:Object = {functionCallNode}
IdentifierNode typeNode = new IdentifierNode(IASLanguageConstants.Object);
VariableNode varNode = new VariableNode(varName, typeNode);
varNode.setKeyword(new ASToken(ASToken.TOKEN_KEYWORD_VAR, -1,-1,-1,-1,"var"));
varNode.getKeywordNode().setParent(varNode);
varNode.setAssignedValue(null, assignedValue);
//explicitly set parent <--> child relationships
varNode.addChild(varNode.getKeywordNode());
varNode.addChild(varName);
varNode.addChild(typeNode);
varNode.addChild(assignedValue);
setupNode = new VariableExpressionNode(varNode);
} else {
//recycledIteratorName = {functionCallNode}
ASToken assignOp = new ASToken(ASTokenTypes.TOKEN_OPERATOR_ASSIGNMENT,-1,-1,-1,-1,"=");
setupNode = new BinaryOperatorAssignmentNode(assignOp, varName, assignedValue);
//explicitly set parent <--> child relationships
varName.setParent(setupNode);
assignedValue.setParent(setupNode);
}
return setupNode;
}
private NodeBase setupIteratee(){
//original part of conditionals group should be either a VariableExpressionNode or an IdentifierNode
IExpressionNode original = ((BinaryOperatorInNode)(target.getConditionalExpressionNodes()[0])).getLeftOperandNode();
NodeBase setupNode;
FunctionCallNode assignedValue = createIteratorCall(ArrayLikeUtil.ARRAYLIKE_GET_NEXT);
if (original instanceof VariableExpressionNode) {
VariableNode varNode = (VariableNode)((VariableExpressionNode) original).getTargetVariable();
//add the assigned value
varNode.setAssignedValue(null, assignedValue);
assignedValue.setParent(varNode);
setupNode = varNode;
} else if (original instanceof IdentifierNode) {
ASToken assignOp = new ASToken(ASTokenTypes.TOKEN_OPERATOR_ASSIGNMENT,-1,-1,-1,-1,"=");
BinaryOperatorAssignmentNode assignmentNode = new BinaryOperatorAssignmentNode(assignOp,(IdentifierNode)original, assignedValue);
((IdentifierNode) original).setParent(assignmentNode);
assignedValue.setParent(assignmentNode);
setupNode = assignmentNode;
} else {
setupNode = null;
//System.out.println("setupIteratee::UNEXPECTED");
}
if (setupNode != null) iterateeValueNode = assignedValue; //so we can run analyze on only the new content 'assignedValue'
return setupNode;
}
public ForLoopNode getLoopTarget(){
return target;
}
public IExpressionNode getIterationTarget(){
//don't use copy/clone
return ((BinaryOperatorInNode)(target.getConditionalExpressionNodes()[0])).getRightOperandNode();
}
public void prepareConditionals(boolean firstUse, FunctionCallNode factoryFuncCall){
assert !conditionalsChanged : "populateConditionals was already called";
//1st part of the substituted for(;;) loop
mutatedConditionals.addItem(setupIterator(firstUse, factoryFuncCall));
//2nd part of the substituted for(;;) loop
mutatedConditionals.addItem(createIteratorCall(ArrayLikeUtil.ARRAYLIKE_HAS_NEXT));
//3rd part of the substituted for(;;) loop is a NilNode in this case
mutatedConditionals.addItem(new NilNode());
conditionalsChanged = true;
}
public void prepareContent(){
assert !contentsChanged : "populateContent was already called";
iteratee = setupIteratee();
contentsChanged = iteratee != null;
}
public void processConditionals(ASScope scope, Collection<ICompilerProblem> problems){
ContainerNode conditionals = target.getConditionalsContainerNode();
conditionals.removeAllChildren();
conditionals.addChild((NodeBase) mutatedConditionals.getChild(0),0);
conditionals.addChild((NodeBase) mutatedConditionals.getChild(1),1);
conditionals.addChild((NodeBase) mutatedConditionals.getChild(2),2);
if (analyzeRequests == null) {
analyzeRequests = new ArrayList<NodeBase>();
}
for (int i=0;i<3;i++) {
analyzeRequests.add((NodeBase) conditionals.getChild(i));
}
}
public void processContents(ASScope scope, Collection<ICompilerProblem> problems){
BlockNode block = target.getContentsNode();
//'insert' at position 0 to prepend this to the original loop's block content
block.addChild(iteratee, 0);
EnumSet<PostProcessStep> set = EnumSet.of(
PostProcessStep.POPULATE_SCOPE);
//only add the iteratee to the analysis. re-evaluating the original variable node (if it is not a pre-existing identifier)
//would add it to the scope's definition store a second time, and we don't want that (probably no harm, but it would add a warning for duplicate variable declaration).
if (analyzeRequests == null) {
analyzeRequests = new ArrayList<NodeBase>();
}
analyzeRequests.add(iterateeValueNode);
}
@Override
public IForLoopNode.ForLoopKind getKind() {
return kind;
}
private boolean conditionalsChanged;
@Override
public boolean mutatesConditionals() {
return conditionalsChanged;
}
private boolean contentsChanged;
@Override
public boolean mutatesContents() {
return contentsChanged;
}
}