blob: 5b3e8dcc0e2ab0f23abafe3499193c160a845283 [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.compiler.internal.tree.mxml;
import java.io.Reader;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
import org.apache.royale.compiler.common.ISourceLocation;
import org.apache.royale.compiler.internal.mxml.MXMLDialect;
import org.apache.royale.compiler.internal.parsing.ISourceFragment;
import org.apache.royale.compiler.internal.parsing.SourceFragment;
import org.apache.royale.compiler.internal.parsing.SourceFragmentsReader;
import org.apache.royale.compiler.internal.parsing.as.ASParser;
import org.apache.royale.compiler.internal.parsing.as.IProjectConfigVariables;
import org.apache.royale.compiler.internal.projects.RoyaleProject;
import org.apache.royale.compiler.internal.tree.as.ExpressionNodeBase;
import org.apache.royale.compiler.internal.tree.as.LiteralNode;
import org.apache.royale.compiler.internal.tree.as.NodeBase;
import org.apache.royale.compiler.internal.workspaces.Workspace;
import org.apache.royale.compiler.problems.ICompilerProblem;
import org.apache.royale.compiler.projects.ICompilerProject;
import org.apache.royale.compiler.tree.as.IASNode;
import org.apache.royale.compiler.tree.as.IExpressionNode;
import org.apache.royale.compiler.tree.as.ILiteralNode;
import org.apache.royale.compiler.tree.as.ILiteralNode.LiteralType;
import org.apache.royale.compiler.tree.mxml.IMXMLSingleDataBindingNode;
import org.apache.royale.compiler.tree.mxml.IMXMLNode;
import com.google.common.collect.LinkedListMultimap;
import com.google.common.collect.ListMultimap;
/**
* This class scans character-by-character across the logical text of multiple
* source fragments (coming from attribute values or text units), looking for
* databinding expressions using the same simple brace-matching algorithm as the
* old compiler. It breaks fragments at each <code>{</code> that begins a
* databinding and each <code>}</code> that ends one.
* <p>
* We have to deal with fragments because physical MXML constructs such as
* entities, CDATA blocks, and MXML comments cannot be presented to the
* ActionScript parser, but the logical ActionScript code that does get parsed
* must produce ActionScript nodes that have the correct physical source
* locations within the file.
*/
class MXMLDataBindingParser
{
/**
* An MXML databinding starts with this character.
*/
private static final char LEFT_BRACE = '{';
/**
* An MXML databinding ends with this character.
*/
private static final char RIGHT_BRACE = '}';
/**
* This character can be used to escape the '{' so that it doesn't indicate
* databinding.
*/
private static final char BACKSLASH = '\\';
/**
* Parses source fragments looking for databinding expressions.
* <p>
* If none are found, a String formed by concatenating the logical text of
* the fragments is returned. If one databinding is found, with no
* surrounding text, an {@code IMXMLDataBindingNode} representing it is
* returned. If one or more databindings, with surrounding or interspersed
* text is found, an {@code IMXMLConcatenatedDataBindingNode} is returned.
*/
public static Object parse(IMXMLNode parent,
ISourceLocation sourceLocation,
ISourceFragment[] fragments,
Collection<ICompilerProblem> problems,
Workspace workspace,
MXMLDialect mxmlDialect,
ICompilerProject project)
{
assert fragments != null : "Expected an array of source fragments";
// Find pairs of '{' and '}' in the fragments that represent databindings.
ListMultimap<ISourceFragment, Integer> scanResult = scan(fragments);
// A null result means there are no databindings.
// Return the concatenated logical text.
if (scanResult == null)
return SourceFragmentsReader.concatLogicalText(fragments);
// Split the fragments as necessary around the '{' and '}' that
// begin and end databindings and produce a list of subfragment lists.
// Each entry in the top-level list is either a DataBindingFragmentList
// containing subfragments inside a databinding or a NonDataBindingFragmentList
// containing subfragments outside a databinding.
List<FragmentList> splitResult = split(fragments, scanResult);
// Create an MXMLConcatenatedDataBindingNode with children.
// Each DataBindingFragmentList creates a child MXMLDataBindingNode.
// Each NonDataBindingFragmentList creates a child LiteralNode of type STRING.
return createNode(parent, sourceLocation, splitResult, problems, workspace, mxmlDialect, project);
}
/**
* Scans character-by-character across the logical text of each source
* fragment, doing brace-matching and building up a list (in
* dataBindingRanges) of DataBindingRange objects that keep track of where
* each databinding starts and ends.
*/
private static ListMultimap<ISourceFragment, Integer> scan(ISourceFragment[] fragments)
{
// We'll return a map mapping a source fragment to a list
// of character indexes where the '{' and '}' for databindings
// are located.
ListMultimap<ISourceFragment, Integer> result = null;
// This counter is incremented by each unescaped '{'
// and decremented by each '}'.
int nesting = 0;
// These keep track of where we found a '{' that starts
// a databinding, while we look for the matching '}'.
ISourceFragment leftBraceFragment = null;
int leftBraceCharIndex = -1;
// This flag keeps track of whether we've seen a backslash,
// which escapes the databinding meaning of '{'.
boolean escape = false;
// Iterate over each input fragment.
for (ISourceFragment fragment : fragments)
{
// Iterate over each character of logical text in the current fragment.
String text = fragment.getLogicalText();
int n = text.length();
for (int i = 0; i < n; i++)
{
switch (text.charAt(i))
{
default:
{
escape = false;
break;
}
case LEFT_BRACE:
{
// If this '{' isn't nested and isn't escaped,
// it might start a databinding, so remember where it is.
if (nesting == 0 & !escape)
{
leftBraceFragment = fragment;
leftBraceCharIndex = i;
}
nesting++;
escape = false;
break;
}
case RIGHT_BRACE:
{
nesting--;
if (nesting == 0)
{
// We've found the matching '}' for the '{'.
// Record where the '{' and '}' are.
if (result == null)
result = LinkedListMultimap.<ISourceFragment, Integer> create();
result.put(leftBraceFragment, leftBraceCharIndex);
result.put(fragment, i);
}
escape = false;
break;
}
case BACKSLASH:
{
escape = true;
break;
}
}
}
}
return result;
}
/**
* Builds a list of DataBindingFragmentList and NonDataBindingFragmentList
* objects that organizes the fragments that are inside databindings and
* outside databindings.
*/
private static List<FragmentList> split(
ISourceFragment[] fragments,
ListMultimap<ISourceFragment, Integer> scanResult)
{
List<FragmentList> result = new ArrayList<FragmentList>();
boolean inDataBinding = false;
FragmentList currentList = null;
for (ISourceFragment fragment : fragments)
{
// Get the character indexes where '{' and '}' for databindings are.
List<Integer> braceIndices = scanResult.get(fragment);
if (braceIndices == null)
{
if (currentList == null)
currentList = newFragmentList(result, inDataBinding);
// If there is no '{' or '}' in this fragment,
// add the fragment to the current fragment list.
currentList.add(fragment);
}
else
{
// Otherwise, we'll have to break up the fragment.
// Add an end-of-fragment index at the end of the list
// so that we don't miss the last subfragment.
braceIndices.add(fragment.getLogicalText().length());
// Break up the fragment into subfragments around
// the '{' and '}' characters.
// Add each subfragment to the appropriate list.
int beginIndex = 0;
for (int braceIndex : braceIndices)
{
ISourceFragment subfragment =
((SourceFragment)fragment).subfragment(beginIndex, braceIndex);
if (subfragment != null)
{
currentList = newFragmentList(result, inDataBinding);
currentList.add(subfragment);
}
beginIndex = braceIndex + 1;
inDataBinding = !inDataBinding;
}
}
}
return result;
}
private static FragmentList newFragmentList(List<FragmentList> result, boolean inDataBinding)
{
FragmentList list = inDataBinding ?
new DataBindingFragmentList() :
new NonDataBindingFragmentList();
result.add(list);
return list;
}
private static IASNode createNode(IMXMLNode parent,
ISourceLocation sourceLocation,
List<FragmentList> listOfFragmentLists,
Collection<ICompilerProblem> problems,
Workspace workspace,
MXMLDialect mxmlDialect,
ICompilerProject project)
{
MXMLConcatenatedDataBindingNode node = new MXMLConcatenatedDataBindingNode((NodeBase)parent);
node.setLocation(sourceLocation.getSourcePath(),
sourceLocation.getAbsoluteStart(), sourceLocation.getAbsoluteEnd(),
sourceLocation.getLine(), sourceLocation.getColumn(),
sourceLocation.getEndLine(), sourceLocation.getEndColumn());
// Build a list of children for the MXMLConcatenatedDataBindingNode.
List<IASNode> children = new ArrayList<IASNode>();
for (List<ISourceFragment> fragmentList : listOfFragmentLists)
{
if (fragmentList instanceof DataBindingFragmentList)
{
// For each DataBindingFragmentList, add an IMXMLDataBindingNode
// containing an IExpressionNode created by the ActionScript parser.
children.add(createDataBindingNode(node, sourceLocation, fragmentList, problems, workspace, project));
}
else if (fragmentList instanceof NonDataBindingFragmentList)
{
// For each NonDataBindingFragmentList, add an ILiteralNode
// of type STRING from the concatenated logical text.
children.add(createStringLiteralNode(node, sourceLocation, fragmentList));
}
}
// If no nodes were built, we probably had an empty databinding ( "{}" )
// But we must make a node here, otherwise the tree will be malformed or
// invalid. So let's create an empty one
if (children.isEmpty())
{
assert listOfFragmentLists.isEmpty(); // should only happen if we
// were passed no frags
children.add(createEmptyDatabindingNode(node, sourceLocation));
}
// If the leading/trailing child is a whitespace string literal node, remove it.
trim(children, mxmlDialect);
node.setChildren(children.toArray(new IASNode[0]));
// If the only thing inside is a single IMXMLDataBindingNode,
// return that, because we're not concatenating anything.
if (node.getChildCount() == 1)
{
IASNode child = node.getChild(0);
if (child instanceof IMXMLSingleDataBindingNode)
return child;
}
// Otherwise, return the IConcatenatedDataBindingNode.
return node;
}
private static ILiteralNode createStringLiteralNode(
MXMLConcatenatedDataBindingNode parent,
ISourceLocation sourceLocation,
List<ISourceFragment> fragmentList)
{
ISourceFragment[] fragments = fragmentList.toArray(new ISourceFragment[0]);
String text = SourceFragmentsReader.concatLogicalText(fragments);
// LiteralNode automatically strips out quote characters at the
// beginning and end of the string.
// with that in mind, if the original text starts or ends with a quote,
// that will get stripped if we don't wrap it manually.
// apache/royale-compiler#49
if(text.indexOf("\"") != -1)
{
text = "'" + text + "'";
}
else
{
text = "\"" + text + "\"";
}
LiteralNode stringLiteralNode = new LiteralNode(LiteralType.STRING, text);
stringLiteralNode.setParent(parent);
ISourceFragment firstFragment = fragments[0];
ISourceFragment lastFragment = fragments[fragments.length - 1];
stringLiteralNode.setSourcePath(sourceLocation.getSourcePath());
stringLiteralNode.setStart(firstFragment.getPhysicalStart());
stringLiteralNode.setEnd(lastFragment.getPhysicalStart() + lastFragment.getPhysicalText().length());
stringLiteralNode.setLine(firstFragment.getPhysicalLine());
stringLiteralNode.setColumn(firstFragment.getPhysicalColumn());
return stringLiteralNode;
}
private static IMXMLSingleDataBindingNode createDataBindingNode(
IMXMLNode parent,
ISourceLocation sourceLocation,
List<ISourceFragment> fragments,
Collection<ICompilerProblem> problems,
Workspace workspace,
ICompilerProject project)
{
MXMLSingleDataBindingNode result = new MXMLSingleDataBindingNode((NodeBase)parent);
// Set location information for the MXMLDataBindingNode.
ISourceFragment firstFragment = fragments.get(0);
ISourceFragment lastFragment = fragments.get(fragments.size() - 1);
result.setSourcePath(sourceLocation.getSourcePath());
result.setStart(firstFragment.getPhysicalStart() - 1);
result.setEnd(lastFragment.getPhysicalStart() + lastFragment.getPhysicalText().length() + 1);
result.setLine(firstFragment.getPhysicalLine());
result.setColumn(firstFragment.getPhysicalColumn() - 1);
// Parse the fragments inside the databinding expression.
Reader reader = new SourceFragmentsReader(sourceLocation.getSourcePath(), fragments.toArray(new ISourceFragment[0]));
// IExpressionNode expressionNode = ASParser.parseDataBinding(workspace, reader, problems);
IProjectConfigVariables projectConfigVariables =
((RoyaleProject)project).getProjectConfigVariables();
IExpressionNode expressionNode = ASParser.parseExpression(workspace, reader, problems,
projectConfigVariables, sourceLocation);
// If the parse of the databinding expression failed,
// substitute an empty string literal node
// (which is the result of the empty databinding expression {}).
if (expressionNode == null)
expressionNode = new LiteralNode(LiteralType.STRING, "");
// ASParser creates the ExpressionNodeBase as a child of a FileNode.
// Make it a child of the MXMLDataBindingNode.
((ExpressionNodeBase)expressionNode).setParent(result);
result.setExpressionNode(expressionNode);
// double check that the node tree has its children's parent chain set up
validateParents((NodeBase)expressionNode);
return result;
}
private static void validateParents(NodeBase expressionNode)
{
for (int i = 0; i < expressionNode.getChildCount(); i++)
{
IASNode child = expressionNode.getChild(i);
if (child instanceof NodeBase)
{
if (child.getParent() == null)
((NodeBase)child).setParent((ExpressionNodeBase)expressionNode);
validateParents((NodeBase)child);
}
}
}
// Makes a databinding node whose expression is just an empty string
private static IMXMLSingleDataBindingNode createEmptyDatabindingNode(IMXMLNode parent, ISourceLocation sourceLocation)
{
MXMLSingleDataBindingNode result = new MXMLSingleDataBindingNode((NodeBase)parent);
// Since we are empty, we can't set our source location based on our children.
// But in the special case we can set our location to the same thing as our parent.
result.setSourceLocation((NodeBase)parent);
IExpressionNode expressionNode = new LiteralNode(LiteralType.STRING, "");
((ExpressionNodeBase)expressionNode).setParent(result);
result.setExpressionNode(expressionNode);
return result;
}
private static void trim(List<IASNode> children, MXMLDialect mxmlDialect)
{
assert (children != null && !children.isEmpty()); // function as written requires at least one child
IASNode firstChild = children.get(0);
removeIfWhitespace(children, firstChild, mxmlDialect);
int n = children.size();
IASNode lastChild = children.get(n - 1);
removeIfWhitespace(children, lastChild, mxmlDialect);
}
private static void removeIfWhitespace(List<IASNode> children,
IASNode child,
MXMLDialect mxmlDialect)
{
if (child instanceof ILiteralNode)
{
String text = ((ILiteralNode)child).getValue();
if (mxmlDialect.isWhitespace(text))
children.remove(child);
}
assert !children.isEmpty(); // don't delete all the children
}
/**
* This is a typedef-like class to improve readability by avoiding nested
* parameterized types.
*/
@SuppressWarnings("serial")
private static abstract class FragmentList extends ArrayList<ISourceFragment>
{
}
/**
* This class is used to store sequences of {@code ISourceFragment}s that
* are inside databinding expressions.
*/
@SuppressWarnings("serial")
private static class DataBindingFragmentList extends FragmentList
{
}
/**
* This class is used to store sequences of {@code ISourceFragment}s that
* are outside databinding expressions.
*/
@SuppressWarnings("serial")
private static class NonDataBindingFragmentList extends FragmentList
{
}
}