/** * Aptana Studio * Copyright (c) 2005-2011 by Appcelerator, Inc. All Rights Reserved. * Licensed under the terms of the Eclipse Public License (EPL). * Please see the license-epl.html included with this distribution for details. * Any modifications to this file must keep this entire header intact. */ package com.aptana.editor.php.internal.parser.nodes; import java.util.ArrayList; import java.util.HashMap; import java.util.HashSet; import java.util.LinkedHashSet; import java.util.LinkedList; import java.util.List; import java.util.Map; import java.util.Queue; import java.util.Set; import java.util.Stack; import org2.eclipse.php.core.compiler.PHPFlags; import org2.eclipse.php.internal.core.ast.nodes.Identifier; import org2.eclipse.php.internal.core.documentModel.phpElementData.IPHPDocBlock; import com.aptana.core.logging.IdeLog; import com.aptana.core.util.StringUtil; import com.aptana.editor.html.IHTMLConstants; import com.aptana.editor.html.parsing.HTMLParseState; import com.aptana.editor.html.parsing.ast.HTMLElementNode; import com.aptana.editor.html.parsing.ast.HTMLTextNode; import com.aptana.editor.php.PHPEditorPlugin; import com.aptana.parsing.ParseState; import com.aptana.parsing.ParserPoolFactory; import com.aptana.parsing.ast.IParseNode; import com.aptana.parsing.ast.IParseRootNode; import com.aptana.parsing.ast.ParseNode; /** * PHP Node Builder.<br> * This builder is used by the {@link NodeBuildingVisitor} when a PHP node is to be created.<br> * The builder also parses the HTML parts in the source, and creates a nodes tree that is a composition of the PHP nodes * and the HTML nodes.<br> * Once the node building is done, calling the {@link #populateNodes()} will return a root parse node that holds the * nodes tree. * * @author Pavel Petrochenko, Shalom Gibly */ public class NodeBuilder { private IPHPParseNode current; private IPHPParseNode root; private Stack<Object> stack = new Stack<Object>(); private List<Object> phpStarts = new ArrayList<Object>(); private List<Object> phpEnds = new ArrayList<Object>(); private List<Parameter> parameters = new ArrayList<Parameter>(); /** * Whether to collect variables. */ private boolean collectVariables = false; private boolean hasSyntaxErrors; private String source; private boolean parseHTML; public boolean hasSyntaxErrors() { return hasSyntaxErrors; } /** * Constructs a new NodeBuilder * * @param source * @param root */ public NodeBuilder(String source, IPHPParseNode root) { this.source = source; this.current = root; this.root = root; this.parseHTML = true; } /** * Constructs a new NodeBuilder * * @param source */ public NodeBuilder(String source) { this(source, new PHPBaseParseNode((short) 0, 0, 0, 0, StringUtil.EMPTY)); } /** * Constructs a new NodeBuilder * * @param source * @param collectVariables * - whether to collect variables. * @param parseHTML * - whether */ public NodeBuilder(String source, boolean collectVariables, boolean parseHTML) { this(source); this.collectVariables = collectVariables; this.parseHTML = parseHTML; } public void handleClassConstDeclaration(String constName, IPHPDocBlock docInfo, int startPosition, int endPosition, int stopPosition) { PHPVariableParseNode pn = new PHPVariableParseNode(0, startPosition, endPosition, constName); pn.setField(true); current.addChild(pn); } public void handleUse(String useName, String useAs, int startPosition, int stopPosition) { PHPUseNode un = new PHPUseNode(startPosition, stopPosition, useName, "use"); //$NON-NLS-1$ un.setNameNode(useName, startPosition, stopPosition); current.addChild(un); } public void handleNamespaceDeclaration(String namespaceName, int startPosition, int endPosition, int stopPosition) { PHPNamespaceNode un = new PHPNamespaceNode(startPosition, endPosition, namespaceName, StringUtil.EMPTY); un.setNameNode(namespaceName, startPosition, stopPosition); pushNode(un); } /** * Handle class declaration. * * @param className * @param modifier * @param docInfo * @param startPosition * @param endPosition * @param lineNumber */ public void handleClassDeclaration(String className, int modifier, IPHPDocBlock docInfo, int startPosition, int endPosition, int lineNumber) { PHPClassParseNode pn = new PHPClassParseNode(modifier, startPosition, endPosition, className); if (docInfo != null) { pn.setDocumentation(docInfo); } pushNode(pn); } /** * Handle a Trait declaration. * * @param traitName * @param modifier * @param docInfo * @param startPosition * @param endPosition * @param lineNumber */ public void handleTraitDeclaration(String traitName, int modifier, IPHPDocBlock docInfo, int startPosition, int endPosition, int lineNumber) { PHPTraitParseNode pn = new PHPTraitParseNode(modifier, startPosition, endPosition, traitName); if (docInfo != null) { pn.setDocumentation(docInfo); } pushNode(pn); } /** * Handle the 'extends' section in the class declaration part. * * @param superClassName * @param startPosition * @param endPosition */ public void handleSuperclass(String superClassName, int startPosition, int endPosition) { if (superClassName != null) { String decodeClassName = decodeClassName(superClassName); PHPClassParseNode classNode = (PHPClassParseNode) current; classNode.setSuperClassName(decodeClassName); PHPExtendsNode superClass = new PHPExtendsNode(0, startPosition, endPosition, decodeClassName); superClass.setNameNode(decodeClassName, startPosition, endPosition); classNode.addChild(superClass); } } /** * Handle the 'extends' section in a Trait declaration part. * * @param superClassName * @param startPosition * @param endPosition */ public void handleTraitSuperclass(String superClassName, int startPosition, int endPosition) { if (superClassName != null) { String decodeClassName = decodeClassName(superClassName); PHPTraitParseNode traitNode = (PHPTraitParseNode) current; traitNode.setSuperClassName(decodeClassName); PHPExtendsNode superClass = new PHPExtendsNode(0, startPosition, endPosition, decodeClassName); superClass.setNameNode(decodeClassName, startPosition, endPosition); traitNode.addChild(superClass); } } /** * Handle an 'implements' section in the class declaration part. * * @param interfacesNames * An array of interfaces names. * @param startEndPositions * The start and the end of each interface in the interfaces names. */ public void handleImplements(String[] interfacesNames, int[][] startEndPositions) { if (interfacesNames != null && interfacesNames.length > 0 && startEndPositions != null) { PHPClassParseNode classNode = (PHPClassParseNode) current; List<String> interfaces = new ArrayList<String>(interfacesNames.length); for (int i = 0; i < interfacesNames.length; i++) { String interfaceName = decodeClassName(interfacesNames[i]); interfaces.add(interfaceName); classNode.addChild(new PHPExtendsNode(PHPFlags.AccInterface, startEndPositions[i][0], startEndPositions[i][1], interfaceName)); } classNode.setInterfaces(interfaces); } } private void pushNode(PHPBaseParseNode pn) { current.addChild(pn); stack.push(current); current = pn; } public void handleClassVariablesDeclaration(String variables, int modifier, IPHPDocBlock docInfo, int startPosition, int endPosition, int stopPosition) { PHPVariableParseNode pn = new PHPVariableParseNode(modifier, startPosition, endPosition, variables); pn.setField(true); current.addChild(pn); pn.setNameNode(variables, startPosition, endPosition); } /** * {@inheritDoc} */ public void handleVariableName(String variableName, int line) { if (collectVariables && current == root) { PHPVariableParseNode pn = new PHPVariableParseNode(0, -1, -1, variableName); pn.setField(false); pn.setLocalVariable(false); pn.setParameter(false); current.addChild(pn); } } public void handleDefine(String name, String value, IPHPDocBlock docInfo, int startPosition, int endPosition, int stopPosition) { PHPConstantNode pn = new PHPConstantNode(startPosition, endPosition, name); pn.setDocumentation(docInfo); pn.setField(true); pn.setNameNode(name, startPosition, endPosition); current.addChild(pn); } public void handleError(String description, int startPosition, int endPosition, int lineNumber) { // TODO: Shalom - See what needs to be done to handle those errors. IdeLog.logInfo(PHPEditorPlugin.getDefault(), "NodeBuilderClient.handleError() --> " + description, null, //$NON-NLS-1$ PHPEditorPlugin.DEBUG_SCOPE); } public void handleFunctionDeclaration(String functionName, boolean isClassFunction, int modifier, IPHPDocBlock docInfo, int startPosition, int stopPosition, int lineNumber) { PHPFunctionParseNode pn = new PHPFunctionParseNode(modifier, startPosition, stopPosition, functionName); pn.setMethod(isClassFunction); pn.setParameters(parameters); if (docInfo != null) { pn.setDocumentation(docInfo); } parameters = new ArrayList<Parameter>(); pushNode(pn); } public void handleFunctionParameter(String classType, String variableName, boolean isReference, boolean isConst, String defaultValue, int startPosition, int endPosition, int stopPosition, int lineNumber) { Parameter pr = new Parameter(classType, variableName, defaultValue, isReference, isConst); parameters.add(pr); } public void handleGlobalVar(String variableName) { PHPVariableParseNode pn = new PHPVariableParseNode(0, -1, -1, variableName); current.addChild(pn); } public void handlePHPStart(int startOffset, int endOffset) { phpStarts.add(new Integer(startOffset)); } public void handlePHPEnd(int startOffset, int endOffset) { phpEnds.add(new Integer(startOffset)); } public void handleStaticVar(String variableName) { PHPVariableParseNode pn = new PHPVariableParseNode(1, -1, -1, variableName); current.addChild(pn); } /** * @return all nodes */ public PHPBlockNode populateNodes() { PHPBlockNode bn = new PHPBlockNode(0, 0, "php"); //$NON-NLS-1$ for (int a = 0; a < current.getChildCount(); a++) { bn.addChild(current.getChild(a)); } if (parseHTML) { replaceHtmlNodes(bn); } return bn; } /** * Recursively go deeper into the nodes hierarchy and replace the PHP-HTML nodes with nodes that are generated using * the HTML parser * * @param pn */ private void replaceHtmlNodes(IParseNode pn) { IParseRootNode htmlParseResult = null; try { ParseState parseState = new HTMLParseState(source); htmlParseResult = ParserPoolFactory.parse(IHTMLConstants.CONTENT_TYPE_HTML, parseState).getRootNode(); } catch (Exception e) { IdeLog.logWarning(PHPEditorPlugin.getDefault(), "A problem while integrating the HTML parse result nodes into the PHP parse result nodes", e); //$NON-NLS-1$ } integrateNodesRecursively(pn, mapHTMLElementNodes(htmlParseResult.getChildren())); } /** * Integrate the PHP nodes and the HTML nodes. * * @param phpParseNode * @param htmlElementNodes */ private void integrateNodesRecursively(IParseNode phpParseNode, Map<Integer, HTMLElementNode> htmlElementNodes) { Queue<IParseNode> queue = new LinkedList<IParseNode>(); HTMLElementNode htmlNode = null; for (IParseNode node : phpParseNode.getChildren()) { if (htmlNode != null) { // check if the next child is under the HTML node if (htmlNode.contains(node.getStartingOffset())) { if (node.getNodeType() != PHPBaseParseNode.HTML_NODE) { repositionNode((ParseNode) node, htmlNode); // We still need to visit that PHP node, so adding it to the queue queue.offer(node); } continue; } else { // reset the HTML node htmlNode = null; } } if (node.getNodeType() == PHPBaseParseNode.HTML_NODE) { // find the first non-white character in the node int offset = node.getStartingOffset(); for (; offset < node.getEndingOffset(); offset++) { if (!Character.isWhitespace(source.charAt(offset))) { break; } } htmlNode = htmlElementNodes.get(offset); if (htmlNode != null) { // Found a matching HTML element node at this offset. // Replace the PHP node that represents the HTML part with the real HTML node IParseNode parent = node.getParent(); while (parent != null && parent.isFilteredFromOutline() && parent.getNodeType() != PHPBaseParseNode.BLOCK_NODE) { node = parent; parent = node.getParent(); } if (parent != null) { IParseNode nextSibling = htmlNode.getNextSibling(); Set<IParseNode> toInsert = new HashSet<IParseNode>(); toInsert.add(htmlNode); // check if we need to insert any siblings as well while (nextSibling != null && nextSibling.getStartingOffset() < node.getEndingOffset()) { if (nextSibling instanceof HTMLElementNode) { if (!toInsert.add(nextSibling)) { // #APSTUD-3662 ==> In case the next sibling is already there, break the loop. break; } } nextSibling = nextSibling.getNextSibling(); } // avoid any index out of bound in some cases. int nodeIndex = node.getIndex(); if (nodeIndex > -1 && parent.getChildCount() > nodeIndex) { // Insert the node and the siblings that are nested in the PHP HTML node. IParseNode[] children = parent.getChildren(); for (IParseNode child : children) { if (toInsert.contains(child)) { toInsert.remove(child); } } int siblingsCount = toInsert.size(); IParseNode[] newChildren = new IParseNode[children.length + siblingsCount]; System.arraycopy(children, 0, newChildren, 0, nodeIndex); System.arraycopy(toInsert.toArray(), 0, newChildren, nodeIndex, siblingsCount); System.arraycopy(children, nodeIndex, newChildren, siblingsCount + nodeIndex, newChildren.length - nodeIndex - siblingsCount); ((ParseNode) parent).setChildren(newChildren); } } } } else { queue.offer(node); } } // Recursively call the nodes in the queue for (IParseNode node : queue) { integrateNodesRecursively(node, htmlElementNodes); } } /** * Reposition a node under a new parent * * @param phpNode * @param newHtmlParent * @param phpNodeIndex */ private void repositionNode(ParseNode phpNode, HTMLElementNode newHtmlParent) { IParseNode phpNodeParent = phpNode.getParent(); int phpNodeIndex = phpNode.getIndex(); if (newHtmlParent.getParent() != phpNodeParent) { // position the HTML at the PHP nodes tree newHtmlParent.setParent(phpNodeParent); phpNodeParent.replaceChild(phpNodeIndex, newHtmlParent); } // At this point, we already injected the HTML into the PHP tree. // We set the HTML node as a parent of the given PHP node. IParseNode closesedHTMLNode = newHtmlParent.getNodeAtOffset(phpNode.getStartingOffset()); if (closesedHTMLNode.getParent() instanceof HTMLElementNode) { newHtmlParent = (HTMLElementNode) closesedHTMLNode.getParent(); } phpNode.setParent(newHtmlParent); // We inject the PHP node into the HTML node, replacing the text-node that represents it. IParseNode[] htmlChildren = newHtmlParent.getChildren(); Set<IParseNode> newChildren = new LinkedHashSet<IParseNode>(htmlChildren.length); boolean phpChildInserted = false; for (IParseNode child : htmlChildren) { if (!phpChildInserted && child.contains(phpNode.getStartingOffset()) && child instanceof HTMLTextNode) { // found the PHP representation in the HTML nodes. newChildren.add(phpNode); phpChildInserted = true; } else if (phpChildInserted) { // consume any child that is nested in this php element if (!phpNode.contains(child.getStartingOffset())) { newChildren.add(child); } } else { newChildren.add(child); } } htmlChildren = newChildren.toArray(new IParseNode[newChildren.size()]); newHtmlParent.setChildren(htmlChildren); // Finally, we need to remove the PHP node from its original PHP parent. To do so, we manipulate the array of // children and re-insert it to the parent. IParseNode[] phpChildren = phpNodeParent.getChildren(); IParseNode[] newPhpChildren = new IParseNode[phpChildren.length - 1]; System.arraycopy(phpChildren, 0, newPhpChildren, 0, phpNodeIndex); System.arraycopy(phpChildren, phpNodeIndex + 1, newPhpChildren, phpNodeIndex, phpChildren.length - phpNodeIndex - 1); ((ParseNode) phpNodeParent).setChildren(newPhpChildren); } /** * Generate a {@link Map} that holds {@link HTMLElementNode}s offsets to node references in the given * {@link IParseNode} tree. * * @param parseNode * @return A Map of nodes-offset to node. */ private Map<Integer, HTMLElementNode> mapHTMLElementNodes(IParseNode[] parseNodes) { // Recursively traverse the HTML tree to collect the HTMLElementNode Map<Integer, HTMLElementNode> offsetToNode = new HashMap<Integer, HTMLElementNode>(); for (IParseNode child : parseNodes) { if (child instanceof HTMLElementNode) { HTMLElementNode previous = offsetToNode.put(child.getStartingOffset(), (HTMLElementNode) child); if (previous != null) { // Just in case, have this check in case something is wrong in the HTML result. // This will prevent any infinite recursion. IdeLog.logError(PHPEditorPlugin.getDefault(), "Invalid HTML parse result structure"); //$NON-NLS-1$ return offsetToNode; } offsetToNode.putAll(mapHTMLElementNodes(child.getChildren())); } } return offsetToNode; } /** * @param includingType * @param includeFileName * @param docInfo * @param startPosition * @param endPosition * @param stopPosition * @param lineNumber */ public void handleIncludedFile(String includingType, String includeFileName, IPHPDocBlock docInfo, int startPosition, int endPosition, int stopPosition, int lineNumber) { PHPIncludeNode node = new PHPIncludeNode(startPosition, endPosition, includeFileName, includingType); node.setNameNode(includeFileName, startPosition, endPosition); current.addChild(node); } /** * Decodes class or interface name. * * @param encodedName * - encoded name. * @return decoded name. */ private String decodeClassName(String encodedName) { int bracketIndex = encodedName.indexOf(']'); if (bracketIndex == -1 || bracketIndex == encodedName.length() - 1) { return encodedName; } return encodedName.substring(bracketIndex + 1); } public void handleSyntaxError(int currToken, String currText, short[] rowOfProbe, int startPosition, int endPosition, int lineNumber) { hasSyntaxErrors = true; } public void setNodeName(Identifier nameIdentifier) { if (current != null && nameIdentifier != null) { current.setNameNode(nameIdentifier.getName(), nameIdentifier.getStart(), nameIdentifier.getEnd() - 1); } else { if (nameIdentifier == null) { IdeLog.logWarning(PHPEditorPlugin.getDefault(), "PHP NodeBuilder.setNodeName got a null identifier.", PHPEditorPlugin.DEBUG_SCOPE); //$NON-NLS-1$ } else { IdeLog.logWarning( PHPEditorPlugin.getDefault(), "PHP NodeBuilder.setNodeName didn't hold any current node to set a name on.", PHPEditorPlugin.DEBUG_SCOPE); //$NON-NLS-1$ } } } /** * Handle a common declaration end. */ public void handleCommonDeclarationEnd() { try { current = (IPHPParseNode) stack.pop(); } catch (Exception e) { IdeLog.logError(PHPEditorPlugin.getDefault(), "Error building the PHP nodes.", e); //$NON-NLS-1$ } } /** * Handles an inline HTML content. * * @param start * @param end */ public void handleInlineHtml(int start, int end) { handlePHPEnd(start, -1); handlePHPStart(end, -1); // Check if the last child of the current node is also a HTML node. If so, we should unify both to one node with // a larger offset. if (current.getChildCount() > 0 && current.getLastChild().getNodeType() == PHPBaseParseNode.HTML_NODE) { PHPBaseParseNode lastChild = (PHPBaseParseNode) current.getLastChild(); lastChild.setLocation(lastChild.getStart(), end); } else { // We temporarily insert that html node into the stack. This node will be popped and replaced with the real // HTML nodes once we verify that we are no longer receiving new inline-html nodes from the PHP parser. current.addChild(new PHPHTMLNode(start, end)); } } /** * @param start * @param end */ public void handleTryStatement(int start, int end) { pushNode(new PHPTryNode(start, end)); } /** * @param start * @param end */ public void handleCatchStatement(int start, int end) { pushNode(new PHPCatchNode(start, end)); } /** * @param start * @param end */ public void handleDoStatement(int start, int end) { pushNode(new PHPDoNode(start, end)); } /** * @param start * @param end */ public void handleForStatement(int start, int end) { pushNode(new PHPForNode(start, end, PHPForNode.FOR_TYPE.FOR)); } /** * @param start * @param end */ public void handleForEachStatement(int start, int end) { pushNode(new PHPForNode(start, end, PHPForNode.FOR_TYPE.FOREACH)); } /** * @param start * @param end */ public void handleSwitchCaseStatement(int start, int end) { pushNode(new PHPSwitchCaseNode(start, end)); } /** * @param start * @param end */ public void handleSwitchStatement(int start, int end) { pushNode(new PHPSwitchNode(start, end)); } /** * @param start * @param end */ public void handleWhileStatement(int start, int end) { pushNode(new PHPWhileNode(start, end)); } /** * @param start * @param end * @param type */ public void handleIfElseStatement(int start, int end, String type) { pushNode(new PHPIfElseNode(start, end, type)); } }