/* * * 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.flex.compiler.internal.tree.mxml; import java.io.Reader; import java.util.ArrayList; import java.util.Collection; import java.util.List; import org.apache.flex.compiler.common.ISourceLocation; import org.apache.flex.compiler.internal.mxml.MXMLDialect; import org.apache.flex.compiler.internal.parsing.ISourceFragment; import org.apache.flex.compiler.internal.parsing.SourceFragment; import org.apache.flex.compiler.internal.parsing.SourceFragmentsReader; import org.apache.flex.compiler.internal.parsing.as.ASParser; import org.apache.flex.compiler.internal.parsing.as.IProjectConfigVariables; import org.apache.flex.compiler.internal.projects.FlexProject; import org.apache.flex.compiler.internal.tree.as.ExpressionNodeBase; import org.apache.flex.compiler.internal.tree.as.LiteralNode; import org.apache.flex.compiler.internal.tree.as.NodeBase; import org.apache.flex.compiler.internal.workspaces.Workspace; import org.apache.flex.compiler.problems.ICompilerProblem; import org.apache.flex.compiler.projects.ICompilerProject; import org.apache.flex.compiler.tree.as.IASNode; import org.apache.flex.compiler.tree.as.IExpressionNode; import org.apache.flex.compiler.tree.as.ILiteralNode; import org.apache.flex.compiler.tree.as.ILiteralNode.LiteralType; import org.apache.flex.compiler.tree.mxml.IMXMLSingleDataBindingNode; import org.apache.flex.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()); // 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 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 = ((FlexProject)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 { } }