/**
* Aptana Studio
* Copyright (c) 2005-2012 by Appcelerator, Inc. All Rights Reserved.
* Licensed under the terms of the GNU Public License (GPL) v3 (with exceptions).
* Please see the license.html included with this distribution for details.
* Any modifications to this file must keep this entire header intact.
*/
package com.aptana.editor.php.formatter;
import static com.aptana.editor.php.formatter.PHPFormatterConstants.BRACE_POSITION_BLOCK;
import static com.aptana.editor.php.formatter.PHPFormatterConstants.BRACE_POSITION_BLOCK_IN_CASE;
import static com.aptana.editor.php.formatter.PHPFormatterConstants.BRACE_POSITION_BLOCK_IN_SWITCH;
import static com.aptana.editor.php.formatter.PHPFormatterConstants.BRACE_POSITION_FUNCTION_DECLARATION;
import static com.aptana.editor.php.formatter.PHPFormatterConstants.BRACE_POSITION_TYPE_DECLARATION;
import static com.aptana.editor.php.formatter.PHPFormatterConstants.FORMATTER_INDENTATION_SIZE;
import static com.aptana.editor.php.formatter.PHPFormatterConstants.FORMATTER_OFF;
import static com.aptana.editor.php.formatter.PHPFormatterConstants.FORMATTER_OFF_ON_ENABLED;
import static com.aptana.editor.php.formatter.PHPFormatterConstants.FORMATTER_ON;
import static com.aptana.editor.php.formatter.PHPFormatterConstants.FORMATTER_TAB_CHAR;
import static com.aptana.editor.php.formatter.PHPFormatterConstants.FORMATTER_TAB_SIZE;
import static com.aptana.editor.php.formatter.PHPFormatterConstants.INDENT_BREAK_IN_CASE;
import static com.aptana.editor.php.formatter.PHPFormatterConstants.INDENT_CASE_BODY;
import static com.aptana.editor.php.formatter.PHPFormatterConstants.INDENT_CURLY_BLOCKS;
import static com.aptana.editor.php.formatter.PHPFormatterConstants.INDENT_FUNCTION_BODY;
import static com.aptana.editor.php.formatter.PHPFormatterConstants.INDENT_NAMESPACE_BLOCKS;
import static com.aptana.editor.php.formatter.PHPFormatterConstants.INDENT_PHP_BODY;
import static com.aptana.editor.php.formatter.PHPFormatterConstants.INDENT_SWITCH_BODY;
import static com.aptana.editor.php.formatter.PHPFormatterConstants.INDENT_TYPE_BODY;
import static com.aptana.editor.php.formatter.PHPFormatterConstants.LINES_AFTER_FUNCTION_DECLARATION;
import static com.aptana.editor.php.formatter.PHPFormatterConstants.LINES_AFTER_TYPE_DECLARATION;
import static com.aptana.editor.php.formatter.PHPFormatterConstants.NEW_LINES_BEFORE_CATCH_STATEMENT;
import static com.aptana.editor.php.formatter.PHPFormatterConstants.NEW_LINES_BEFORE_DO_WHILE_STATEMENT;
import static com.aptana.editor.php.formatter.PHPFormatterConstants.NEW_LINES_BEFORE_ELSE_STATEMENT;
import static com.aptana.editor.php.formatter.PHPFormatterConstants.NEW_LINES_BEFORE_IF_IN_ELSEIF_STATEMENT;
import static com.aptana.editor.php.formatter.PHPFormatterConstants.NEW_LINES_BETWEEN_ARRAY_CREATION_ELEMENTS;
import static com.aptana.editor.php.formatter.PHPFormatterConstants.PRESERVED_LINES;
import static com.aptana.editor.php.formatter.PHPFormatterConstants.SPACES_AFTER_ARITHMETIC_OPERATOR;
import static com.aptana.editor.php.formatter.PHPFormatterConstants.SPACES_AFTER_ARROW_OPERATOR;
import static com.aptana.editor.php.formatter.PHPFormatterConstants.SPACES_AFTER_ASSIGNMENT_OPERATOR;
import static com.aptana.editor.php.formatter.PHPFormatterConstants.SPACES_AFTER_CASE_COLON_OPERATOR;
import static com.aptana.editor.php.formatter.PHPFormatterConstants.SPACES_AFTER_COLON;
import static com.aptana.editor.php.formatter.PHPFormatterConstants.SPACES_AFTER_COMMAS;
import static com.aptana.editor.php.formatter.PHPFormatterConstants.SPACES_AFTER_CONCATENATION_OPERATOR;
import static com.aptana.editor.php.formatter.PHPFormatterConstants.SPACES_AFTER_CONDITIONAL_OPERATOR;
import static com.aptana.editor.php.formatter.PHPFormatterConstants.SPACES_AFTER_FOR_SEMICOLON;
import static com.aptana.editor.php.formatter.PHPFormatterConstants.SPACES_AFTER_KEY_VALUE_OPERATOR;
import static com.aptana.editor.php.formatter.PHPFormatterConstants.SPACES_AFTER_NAMESPACE_SEPARATOR;
import static com.aptana.editor.php.formatter.PHPFormatterConstants.SPACES_AFTER_OPENING_ARRAY_ACCESS_PARENTHESES;
import static com.aptana.editor.php.formatter.PHPFormatterConstants.SPACES_AFTER_OPENING_CONDITIONAL_PARENTHESES;
import static com.aptana.editor.php.formatter.PHPFormatterConstants.SPACES_AFTER_OPENING_DECLARATION_PARENTHESES;
import static com.aptana.editor.php.formatter.PHPFormatterConstants.SPACES_AFTER_OPENING_INVOCATION_PARENTHESES;
import static com.aptana.editor.php.formatter.PHPFormatterConstants.SPACES_AFTER_OPENING_LOOP_PARENTHESES;
import static com.aptana.editor.php.formatter.PHPFormatterConstants.SPACES_AFTER_OPENING_PARENTHESES;
import static com.aptana.editor.php.formatter.PHPFormatterConstants.SPACES_AFTER_POSTFIX_OPERATOR;
import static com.aptana.editor.php.formatter.PHPFormatterConstants.SPACES_AFTER_PREFIX_OPERATOR;
import static com.aptana.editor.php.formatter.PHPFormatterConstants.SPACES_AFTER_RELATIONAL_OPERATORS;
import static com.aptana.editor.php.formatter.PHPFormatterConstants.SPACES_AFTER_SEMICOLON;
import static com.aptana.editor.php.formatter.PHPFormatterConstants.SPACES_AFTER_STATIC_INVOCATION_OPERATOR;
import static com.aptana.editor.php.formatter.PHPFormatterConstants.SPACES_AFTER_UNARY_OPERATOR;
import static com.aptana.editor.php.formatter.PHPFormatterConstants.SPACES_BEFORE_ARITHMETIC_OPERATOR;
import static com.aptana.editor.php.formatter.PHPFormatterConstants.SPACES_BEFORE_ARROW_OPERATOR;
import static com.aptana.editor.php.formatter.PHPFormatterConstants.SPACES_BEFORE_ASSIGNMENT_OPERATOR;
import static com.aptana.editor.php.formatter.PHPFormatterConstants.SPACES_BEFORE_CASE_COLON_OPERATOR;
import static com.aptana.editor.php.formatter.PHPFormatterConstants.SPACES_BEFORE_CLOSING_ARRAY_ACCESS_PARENTHESES;
import static com.aptana.editor.php.formatter.PHPFormatterConstants.SPACES_BEFORE_CLOSING_CONDITIONAL_PARENTHESES;
import static com.aptana.editor.php.formatter.PHPFormatterConstants.SPACES_BEFORE_CLOSING_DECLARATION_PARENTHESES;
import static com.aptana.editor.php.formatter.PHPFormatterConstants.SPACES_BEFORE_CLOSING_INVOCATION_PARENTHESES;
import static com.aptana.editor.php.formatter.PHPFormatterConstants.SPACES_BEFORE_CLOSING_LOOP_PARENTHESES;
import static com.aptana.editor.php.formatter.PHPFormatterConstants.SPACES_BEFORE_CLOSING_PARENTHESES;
import static com.aptana.editor.php.formatter.PHPFormatterConstants.SPACES_BEFORE_COLON;
import static com.aptana.editor.php.formatter.PHPFormatterConstants.SPACES_BEFORE_COMMAS;
import static com.aptana.editor.php.formatter.PHPFormatterConstants.SPACES_BEFORE_CONCATENATION_OPERATOR;
import static com.aptana.editor.php.formatter.PHPFormatterConstants.SPACES_BEFORE_CONDITIONAL_OPERATOR;
import static com.aptana.editor.php.formatter.PHPFormatterConstants.SPACES_BEFORE_FOR_SEMICOLON;
import static com.aptana.editor.php.formatter.PHPFormatterConstants.SPACES_BEFORE_KEY_VALUE_OPERATOR;
import static com.aptana.editor.php.formatter.PHPFormatterConstants.SPACES_BEFORE_NAMESPACE_SEPARATOR;
import static com.aptana.editor.php.formatter.PHPFormatterConstants.SPACES_BEFORE_OPENING_ARRAY_ACCESS_PARENTHESES;
import static com.aptana.editor.php.formatter.PHPFormatterConstants.SPACES_BEFORE_OPENING_CONDITIONAL_PARENTHESES;
import static com.aptana.editor.php.formatter.PHPFormatterConstants.SPACES_BEFORE_OPENING_DECLARATION_PARENTHESES;
import static com.aptana.editor.php.formatter.PHPFormatterConstants.SPACES_BEFORE_OPENING_INVOCATION_PARENTHESES;
import static com.aptana.editor.php.formatter.PHPFormatterConstants.SPACES_BEFORE_OPENING_LOOP_PARENTHESES;
import static com.aptana.editor.php.formatter.PHPFormatterConstants.SPACES_BEFORE_OPENING_PARENTHESES;
import static com.aptana.editor.php.formatter.PHPFormatterConstants.SPACES_BEFORE_POSTFIX_OPERATOR;
import static com.aptana.editor.php.formatter.PHPFormatterConstants.SPACES_BEFORE_PREFIX_OPERATOR;
import static com.aptana.editor.php.formatter.PHPFormatterConstants.SPACES_BEFORE_RELATIONAL_OPERATORS;
import static com.aptana.editor.php.formatter.PHPFormatterConstants.SPACES_BEFORE_SEMICOLON;
import static com.aptana.editor.php.formatter.PHPFormatterConstants.SPACES_BEFORE_STATIC_INVOCATION_OPERATOR;
import static com.aptana.editor.php.formatter.PHPFormatterConstants.SPACES_BEFORE_UNARY_OPERATOR;
import static com.aptana.editor.php.formatter.PHPFormatterConstants.WRAP_COMMENTS;
import static com.aptana.editor.php.formatter.PHPFormatterConstants.WRAP_COMMENTS_LENGTH;
import java.io.StringReader;
import java.util.AbstractQueue;
import java.util.Iterator;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import org.eclipse.jface.text.IDocument;
import org.eclipse.jface.text.IRegion;
import org.eclipse.jface.text.ITypedRegion;
import org.eclipse.jface.text.formatter.FormattingContextProperties;
import org.eclipse.jface.text.formatter.IFormattingContext;
import org.eclipse.osgi.util.NLS;
import org.eclipse.text.edits.MultiTextEdit;
import org.eclipse.text.edits.ReplaceEdit;
import org.eclipse.text.edits.TextEdit;
import org2.eclipse.php.internal.core.ast.match.ASTMatcher;
import org2.eclipse.php.internal.core.ast.nodes.Comment;
import org2.eclipse.php.internal.core.ast.nodes.Program;
import org2.eclipse.php.internal.core.ast.rewrite.ASTRewriteFlattener;
import org2.eclipse.php.internal.core.ast.rewrite.RewriteEventStore;
import com.aptana.core.logging.IdeLog;
import com.aptana.core.util.EclipseUtil;
import com.aptana.core.util.StringUtil;
import com.aptana.editor.common.util.EditorUtil;
import com.aptana.editor.php.epl.PHPEplPlugin;
import com.aptana.editor.php.internal.core.IPHPConstants;
import com.aptana.editor.php.internal.parser.PHPParseRootNode;
import com.aptana.editor.php.internal.parser.PHPParser;
import com.aptana.editor.php.internal.parser.nodes.PHPASTWrappingNode;
import com.aptana.formatter.AbstractScriptFormatter;
import com.aptana.formatter.FormatterDocument;
import com.aptana.formatter.FormatterIndentDetector;
import com.aptana.formatter.FormatterUtils;
import com.aptana.formatter.FormatterWriter;
import com.aptana.formatter.IDebugScopes;
import com.aptana.formatter.IFormatterContext;
import com.aptana.formatter.IFormatterIndentGenerator;
import com.aptana.formatter.IScriptFormatter;
import com.aptana.formatter.epl.FormatterPlugin;
import com.aptana.formatter.nodes.IFormatterContainerNode;
import com.aptana.formatter.ui.FormatterException;
import com.aptana.formatter.ui.FormatterMessages;
import com.aptana.formatter.ui.ScriptFormattingContextProperties;
import com.aptana.parsing.ast.IParseRootNode;
import com.aptana.parsing.ast.ParseNode;
import com.aptana.ui.util.StatusLineMessageTimerManager;
/**
* PHP code formatter.
*
* @author Shalom Gibly <sgibly@aptana.com>
*/
public class PHPFormatter extends AbstractScriptFormatter implements IScriptFormatter
{
/**
* Brace positions constants
*/
protected static final String[] BRACE_POSITIONS = { BRACE_POSITION_BLOCK, BRACE_POSITION_BLOCK_IN_CASE,
BRACE_POSITION_BLOCK_IN_SWITCH, BRACE_POSITION_FUNCTION_DECLARATION, BRACE_POSITION_TYPE_DECLARATION };
/**
* New-lines constants
*/
protected static final String[] NEW_LINES_POSITIONS = { NEW_LINES_BEFORE_CATCH_STATEMENT,
NEW_LINES_BEFORE_DO_WHILE_STATEMENT, NEW_LINES_BEFORE_ELSE_STATEMENT,
NEW_LINES_BEFORE_IF_IN_ELSEIF_STATEMENT, NEW_LINES_BETWEEN_ARRAY_CREATION_ELEMENTS };
/**
* Indentation constants
*/
protected static final String[] INDENTATIONS = { INDENT_PHP_BODY, INDENT_CURLY_BLOCKS, INDENT_NAMESPACE_BLOCKS,
INDENT_CASE_BODY, INDENT_SWITCH_BODY, INDENT_FUNCTION_BODY, INDENT_TYPE_BODY, INDENT_BREAK_IN_CASE };
/**
* Spaces constants
*/
protected static final String[] SPACES = { SPACES_AFTER_STATIC_INVOCATION_OPERATOR,
SPACES_BEFORE_STATIC_INVOCATION_OPERATOR, SPACES_BEFORE_ASSIGNMENT_OPERATOR,
SPACES_AFTER_ASSIGNMENT_OPERATOR, SPACES_BEFORE_COMMAS, SPACES_AFTER_COMMAS,
SPACES_BEFORE_CASE_COLON_OPERATOR, SPACES_AFTER_CASE_COLON_OPERATOR, SPACES_BEFORE_COLON,
SPACES_AFTER_COLON, SPACES_BEFORE_SEMICOLON, SPACES_AFTER_SEMICOLON, SPACES_BEFORE_CONCATENATION_OPERATOR,
SPACES_AFTER_CONCATENATION_OPERATOR, SPACES_BEFORE_ARROW_OPERATOR, SPACES_AFTER_ARROW_OPERATOR,
SPACES_BEFORE_KEY_VALUE_OPERATOR, SPACES_AFTER_KEY_VALUE_OPERATOR, SPACES_BEFORE_RELATIONAL_OPERATORS,
SPACES_AFTER_RELATIONAL_OPERATORS, SPACES_BEFORE_CONDITIONAL_OPERATOR, SPACES_AFTER_CONDITIONAL_OPERATOR,
SPACES_BEFORE_POSTFIX_OPERATOR, SPACES_AFTER_POSTFIX_OPERATOR, SPACES_BEFORE_PREFIX_OPERATOR,
SPACES_AFTER_PREFIX_OPERATOR, SPACES_BEFORE_ARITHMETIC_OPERATOR, SPACES_AFTER_ARITHMETIC_OPERATOR,
SPACES_BEFORE_UNARY_OPERATOR, SPACES_AFTER_UNARY_OPERATOR, SPACES_BEFORE_NAMESPACE_SEPARATOR,
SPACES_AFTER_NAMESPACE_SEPARATOR, SPACES_BEFORE_FOR_SEMICOLON, SPACES_AFTER_FOR_SEMICOLON,
SPACES_BEFORE_OPENING_PARENTHESES, SPACES_AFTER_OPENING_PARENTHESES, SPACES_BEFORE_CLOSING_PARENTHESES,
SPACES_BEFORE_OPENING_DECLARATION_PARENTHESES, SPACES_AFTER_OPENING_DECLARATION_PARENTHESES,
SPACES_BEFORE_CLOSING_DECLARATION_PARENTHESES, SPACES_BEFORE_OPENING_INVOCATION_PARENTHESES,
SPACES_AFTER_OPENING_INVOCATION_PARENTHESES, SPACES_BEFORE_CLOSING_INVOCATION_PARENTHESES,
SPACES_BEFORE_OPENING_ARRAY_ACCESS_PARENTHESES, SPACES_AFTER_OPENING_ARRAY_ACCESS_PARENTHESES,
SPACES_BEFORE_CLOSING_ARRAY_ACCESS_PARENTHESES, SPACES_BEFORE_OPENING_LOOP_PARENTHESES,
SPACES_AFTER_OPENING_LOOP_PARENTHESES, SPACES_BEFORE_CLOSING_LOOP_PARENTHESES,
SPACES_BEFORE_OPENING_CONDITIONAL_PARENTHESES, SPACES_AFTER_OPENING_CONDITIONAL_PARENTHESES,
SPACES_BEFORE_CLOSING_CONDITIONAL_PARENTHESES };
// PHP basic prefixes
private static final String PHP_SHORT_TAG_OPEN = "<?"; //$NON-NLS-1$
private static final String PHP_SHORT_ASSIGN_TAG_OPEN = "<?="; //$NON-NLS-1$
private static final String PHP_PREFIX = "<?php "; //$NON-NLS-1$
private static final String PHP_CLOSE_TAG = "?>"; //$NON-NLS-1$
// Regex patterns
private static final Pattern PHP_OPEN_TAG_PATTERNS = Pattern.compile("<\\?php|<\\?=|<\\?"); //$NON-NLS-1$
// multi-line comment flattening pattern
private static final Pattern MULTI_LINE_FLATTEN_PATTERN = Pattern.compile("\\s|/|\\*"); //$NON-NLS-1$
// single-line comment flattening pattern
private static final Pattern SINGLE_LINE_FLATTEN_PATTERN = Pattern.compile("\\s|/|#"); //$NON-NLS-1$
/**
* Constructor.
*
* @param preferences
*/
protected PHPFormatter(String lineSeparator, Map<String, String> preferences, String mainContentType)
{
super(preferences, mainContentType, lineSeparator);
}
/**
* Detects the indentation level.
*/
public int detectIndentationLevel(IDocument document, int offset, boolean isSelection,
IFormattingContext formattingContext)
{
int indent = 0;
try
{
// detect the indentation offset with the parser, only if the given offset is not the first one in the
// current partition.
ITypedRegion partition = document.getPartition(offset);
if (partition != null && partition.getOffset() == offset)
{
int indentationLevel = super.detectIndentationLevel(document, offset);
if (!getBoolean(INDENT_PHP_BODY))
{
// Do some checks to see if we need to return a reduced indentation level.
// In php, we don't want the indent addition at the beginning.
char onOffset = document.getChar(offset);
if (onOffset == '\r')
{
if (document.getChar(offset - 1) != '\n')
{
return indentationLevel - 1;
}
}
else if (onOffset == '\n')
{
return indentationLevel - 1;
}
}
return indentationLevel;
}
String source = document.get();
PHPParser parser = (PHPParser) checkoutParser();
Program ast = parser.parseAST(new StringReader(source));
checkinParser(parser);
if (ast != null)
{
// we wrap the Program with a parser root node to match the API
IParseRootNode rootNode = new PHPParseRootNode(new ParseNode[0], ast.getStart(), ast.getEnd());
rootNode.addChild(new PHPASTWrappingNode(ast));
final PHPFormatterNodeBuilder builder = new PHPFormatterNodeBuilder();
final FormatterDocument formatterDocument = createFormatterDocument(source, offset);
IFormatterContainerNode root = builder.build(rootNode, formatterDocument);
new PHPFormatterNodeRewriter(rootNode, formatterDocument).rewrite(root);
IFormatterContext context = new PHPFormatterContext(0);
FormatterIndentDetector detector = new FormatterIndentDetector(offset);
try
{
root.accept(context, detector);
return detector.getLevel();
}
catch (Exception e)
{
// ignore
}
}
}
catch (Throwable t)
{
return super.detectIndentationLevel(document, offset);
}
return indent;
}
/*
* (non-Javadoc)
* @see com.aptana.formatter.IScriptFormatter#format(java.lang.String, int, int, int, boolean,
* org.eclipse.jface.text.formatter.IFormattingContext, java.lang.String)
*/
public TextEdit format(String source, int offset, int length, int indentationLevel, boolean isSelection,
IFormattingContext context, String indentSufix) throws FormatterException
{
int offsetIncludedOpenTag = offset;
int offsetBySelection = 0;
int lengthBySelection = 0;
if (isSelection)
{
IRegion selectedRegion = (IRegion) context.getProperty(FormattingContextProperties.CONTEXT_REGION);
offsetBySelection = selectedRegion.getOffset();
lengthBySelection = selectedRegion.getLength();
}
offsetIncludedOpenTag = Math.max(0,
findOpenTagOffset(source, offset, offsetBySelection, offsetBySelection + lengthBySelection));
String input = source.substring(offsetIncludedOpenTag, offset + length);
// We do not use a parse-state for the PHP, since we are just interested in the AST and do not want to update
// anything in the indexing.
try
{
boolean forcedPHPEndTag = false;
if (!input.startsWith(PHP_SHORT_TAG_OPEN))
{
input = leftTrim(input, 0); // #APSTUD-4027
input = PHP_PREFIX + input;
}
else if (input.startsWith(PHP_SHORT_ASSIGN_TAG_OPEN))
{
// We have to me sure the input is closed with a semicolon or a close tag (APSTUD-3554)
String trimmed = input.trim();
if (shouldAppendPHPCloseTag(trimmed))
{
input += PHP_CLOSE_TAG;
forcedPHPEndTag = true;
}
}
PHPParser parser = (PHPParser) checkoutParser(IPHPConstants.CONTENT_TYPE_PHP);
Program ast = parser.parseAST(new StringReader(input));
checkinParser(parser);
if (ast != null)
{
String suffix = (beginsWithCloseTag(source, offset + length)) ? " " : StringUtil.EMPTY; //$NON-NLS-1$
// we wrap the Program with a parser root node to match the API
IParseRootNode rootNode = new PHPParseRootNode(new ParseNode[0], ast.getStart(), ast.getEnd());
rootNode.addChild(new PHPASTWrappingNode(ast));
String output = format(input, rootNode, indentationLevel, offsetIncludedOpenTag, isSelection, suffix,
indentSufix);
if (forcedPHPEndTag)
{
input = input.substring(0, input.length() - PHP_CLOSE_TAG.length());
}
if (output != null)
{
if (!input.equals(output))
{
if (equalContent(ast, input, output))
{
// We match the output to all possible PHP open-tags and then trim it to remove it with any
// other white-space that appear before it.
// For example, this output:
// <?php
// function foo() {}
// Will be trimmed to:
// <-- new-line
// function foo() {}
Matcher matcher = PHP_OPEN_TAG_PATTERNS.matcher(output);
if (matcher.find())
{
output = output.substring(matcher.end());
}
return new ReplaceEdit(offset, length, output);
}
else
{
if (ast.getAST().hasErrors())
{
// Fatal syntax errors prevented a proper formatting.
StatusLineMessageTimerManager.setErrorMessage(
FormatterMessages.PHPFormatter_fatalSyntaxErrors, ERROR_DISPLAY_TIMEOUT, true);
}
else
{
logError(input, output);
}
}
}
else
{
return new MultiTextEdit(); // NOP
}
}
}
else
{
// Fatal syntax errors
StatusLineMessageTimerManager.setErrorMessage(FormatterMessages.PHPFormatter_fatalSyntaxErrors,
ERROR_DISPLAY_TIMEOUT, true);
}
}
catch (FormatterException e)
{
StatusLineMessageTimerManager.setErrorMessage(
NLS.bind(FormatterMessages.Formatter_formatterParsingErrorStatus, e.getMessage()),
ERROR_DISPLAY_TIMEOUT, true);
}
catch (Exception e)
{
StatusLineMessageTimerManager.setErrorMessage(FormatterMessages.Formatter_formatterErrorStatus,
ERROR_DISPLAY_TIMEOUT, true);
IdeLog.logError(PHPCodeFormatterPlugin.getDefault(), e, IDebugScopes.DEBUG);
}
return null;
}
/**
* This method is called when the PHP block is a short-assignment (<?=). We need to make sure that the php content
* ends with a valid terminator, or with a closing tag. Otherwise we'll get a parse error (see APSTUD-3554)
*
* @param content
* a trimmed content
* @return
*/
private boolean shouldAppendPHPCloseTag(String content)
{
return !(content.endsWith(";") || content.endsWith("}") || content.endsWith(PHP_CLOSE_TAG)); //$NON-NLS-1$ //$NON-NLS-2$
}
/**
* Returns <code>true</code> if the source contains a PHP close tag string as the first non-whitespace characters
* from the given offset.
*
* @param source
* @param offset
* @return <code>true</code> in case the string at the offset contains a PHP close-tag; <code>false</code>,
* otherwise.
*/
private boolean beginsWithCloseTag(String source, int offset)
{
int closeTagIndex = source.indexOf(PHP_CLOSE_TAG, offset);
return (closeTagIndex > -1 && source.substring(offset, closeTagIndex).trim().length() == 0);
}
/**
* Check if the formatter did not mess with the AST structure of the code.
*
* @param inputAST
* The pre-formatted AST (never null)
* @param inputString
* The original input string
* @param outputString
* The output string that the formatter generated.
* @return true, if the new AST is equals to the original one; False, otherwise.
*/
private boolean equalContent(Program inputAST, String inputString, String outputString)
{
if (outputString == null)
{
return false;
}
outputString = outputString.trim();
if (outputString.startsWith(PHP_SHORT_ASSIGN_TAG_OPEN))
{
if (shouldAppendPHPCloseTag(outputString))
{
outputString += PHP_CLOSE_TAG;
}
}
else
{
// Add a new-line to the end of the output to deal with cases where we have a HEREDOC at the end, which
// requires
// a new-line terminator to avoid a parsing error.
outputString += '\n';
}
PHPParser parser = (PHPParser) checkoutParser(IPHPConstants.CONTENT_TYPE_PHP);
Program outputAST = parser.parseAST(new StringReader(outputString));
checkinParser(parser);
if (outputAST == null)
{
// the inputAST is never null, so we can just return false here
return false;
}
ASTMatcher matcher = new ASTMatcher(true);
// We need to check if the formatter is set to split log comments.
// If so, we'll have to check the comments in a different way. Otherwise, the matcher will throw a false
boolean result = true;
boolean matchWithoutComments = true;
if (getBoolean(WRAP_COMMENTS))
{
matchWithoutComments = matcher.match(inputAST.getProgramRoot(), outputAST.getProgramRoot(), false);
result = matchWithoutComments
&& matchComments(inputAST.comments(), outputAST.comments(), inputString, outputString);
}
else
{
result = matcher.match(inputAST.getProgramRoot(), outputAST.getProgramRoot(), true);
}
if (!result && (FormatterPlugin.getDefault().isDebugging() || EclipseUtil.isTesting()))
{
// Log the failure
if (matchWithoutComments)
{
String flattenedInputAST = ASTRewriteFlattener.asString(inputAST, new RewriteEventStore());
String flattenedOutputAST = ASTRewriteFlattener.asString(outputAST, new RewriteEventStore());
FormatterUtils.logDiff(flattenedInputAST, flattenedOutputAST);
}
}
return result;
}
/**
* Returns the offset of the PHP open tag that that precedes the given offset location. We look for any legal PHP
* open tag
*
* @param source
* @param offset
* @param leftBound
* @param rightBound
* @return
*/
private int findOpenTagOffset(String source, int offset, int leftBound, int rightBound)
{
// We just look for the "<?" and that should cover all cases.
if (leftBound > 0 && rightBound > leftBound)
{
source = source.substring(leftBound, rightBound);
}
int openOffset = source.lastIndexOf(PHP_SHORT_TAG_OPEN, offset);
if (openOffset > -1)
{
if (leftBound > 0)
{
return leftBound - openOffset;
}
return openOffset;
}
return offset;
}
/*
* (non-Javadoc)
* @see com.aptana.formatter.ui.IScriptFormatter#getIndentSize()
*/
public int getIndentSize()
{
return getInt(FORMATTER_INDENTATION_SIZE, 1);
}
/*
* (non-Javadoc)
* @see com.aptana.formatter.ui.IScriptFormatter#getIndentType()
*/
public String getIndentType()
{
return getString(FORMATTER_TAB_CHAR);
}
/*
* (non-Javadoc)
* @see com.aptana.formatter.ui.IScriptFormatter#getTabSize()
*/
public int getTabSize()
{
return getInt(FORMATTER_TAB_SIZE, getEditorSpecificTabWidth());
}
/*
* (non-Javadoc)
* @see com.aptana.formatter.IScriptFormatter#getEditorSpecificTabWidth()
*/
public int getEditorSpecificTabWidth()
{
return EditorUtil.getSpaceIndentSize(PHPEplPlugin.getDefault().getBundle().getSymbolicName());
}
/*
* (non-Javadoc)
* @see com.aptana.formatter.IScriptFormatter#isEditorInsertSpacesForTabs()
*/
public boolean isEditorInsertSpacesForTabs()
{
return FormatterUtils.isInsertSpacesForTabs(PHPEplPlugin.getDefault().getPreferenceStore());
}
/**
* Do the actual formatting of the PHP.
*
* @param input
* The String input
* @param parseResult
* A PHP parser result - {@link com.aptana.parsing.ast.IParseNode}
* @param indentationLevel
* The indentation level to start from
* @return A formatted string
* @throws Exception
*/
private String format(String input, IParseRootNode parseResult, int indentationLevel, int offset,
boolean isSelection, String suffix, String indentSufix) throws Exception
{
final PHPFormatterNodeBuilder builder = new PHPFormatterNodeBuilder();
final FormatterDocument document = createFormatterDocument(input, offset);
IFormatterContainerNode root = builder.build(parseResult, document);
new PHPFormatterNodeRewriter(parseResult, document).rewrite(root);
IFormatterContext context = new PHPFormatterContext(indentationLevel);
IFormatterIndentGenerator indentGenerator = createIndentGenerator();
FormatterWriter writer = new FormatterWriter(document, lineSeparator, indentGenerator);
writer.setWrapLength(getInt(WRAP_COMMENTS_LENGTH));
writer.setLinesPreserve(getInt(PRESERVED_LINES));
root.accept(context, writer);
writer.flush(context);
// Unlike other formatters, we allow errors in the PHP AST for now.
// We just notify the user that there were errors in the PHP file.
if (builder.hasErrors())
{
StatusLineMessageTimerManager.setErrorMessage(
FormatterMessages.Formatter_formatterErrorCompletedWithErrors, ERROR_DISPLAY_TIMEOUT, true);
}
String output = writer.getOutput();
List<IRegion> offOnRegions = builder.getOffOnRegions();
if (offOnRegions != null && !offOnRegions.isEmpty())
{
// We re-parse the output to extract its On-Off regions, so we will be able to compute the offsets and
// adjust it.
List<IRegion> outputOnOffRegions = getOutputOnOffRegions(output,
getString(PHPFormatterConstants.FORMATTER_OFF), getString(PHPFormatterConstants.FORMATTER_ON));
output = FormatterUtils.applyOffOnRegions(input, output, offOnRegions, outputOnOffRegions);
}
if (indentationLevel > 1 && StringUtil.EMPTY.equals(indentSufix))
{
StringBuilder indentBuilder = new StringBuilder();
indentGenerator.generateIndent(Math.max(1, indentationLevel - 1), indentBuilder);
indentSufix = indentBuilder.toString();
}
output = processNestedOutput(output.trim(), lineSeparator, suffix, indentSufix, false, true);
return output;
}
/*
* (non-Javadoc)
* @see com.aptana.formatter.AbstractScriptFormatter#getOutputOnOffRegions(java.lang.String, java.lang.String,
* java.lang.String)
*/
@Override
protected List<IRegion> getOutputOnOffRegions(String output, String formatterOffPattern, String formatterOnPattern)
{
PHPParser parser = (PHPParser) checkoutParser(IPHPConstants.CONTENT_TYPE_PHP);
Program ast = parser.parseAST(new StringReader(output));
checkinParser(parser);
List<IRegion> onOffRegions = null;
if (ast != null)
{
LinkedHashMap<Integer, String> commentsMap = new LinkedHashMap<Integer, String>(ast.comments().size());
for (Comment comment : ast.comments())
{
int start = comment.getStart();
int end = comment.getEnd();
String commentStr = output.substring(start, end);
commentsMap.put(start, commentStr);
}
// Generate the OFF/ON regions
if (!commentsMap.isEmpty())
{
Pattern onPattern = Pattern.compile(Pattern.quote(formatterOnPattern));
Pattern offPattern = Pattern.compile(Pattern.quote(formatterOffPattern));
onOffRegions = FormatterUtils.resolveOnOffRegions(commentsMap, onPattern, offPattern,
output.length() - 1);
}
}
return onOffRegions;
}
private FormatterDocument createFormatterDocument(String input, int offset)
{
FormatterDocument document = new FormatterDocument(input);
document.setInt(FORMATTER_TAB_SIZE, getInt(FORMATTER_TAB_SIZE));
document.setBoolean(WRAP_COMMENTS, getBoolean(WRAP_COMMENTS));
document.setInt(LINES_AFTER_TYPE_DECLARATION, getInt(LINES_AFTER_TYPE_DECLARATION));
document.setInt(LINES_AFTER_FUNCTION_DECLARATION, getInt(LINES_AFTER_FUNCTION_DECLARATION));
document.setInt(ScriptFormattingContextProperties.CONTEXT_ORIGINAL_OFFSET, offset);
document.setBoolean(FORMATTER_OFF_ON_ENABLED, getBoolean(FORMATTER_OFF_ON_ENABLED));
document.setString(FORMATTER_ON, getString(FORMATTER_ON));
document.setString(FORMATTER_OFF, getString(FORMATTER_OFF));
// Set the indentation values
for (String key : INDENTATIONS)
{
document.setBoolean(key, getBoolean(key));
}
// Set the new-lines values
for (String key : NEW_LINES_POSITIONS)
{
document.setBoolean(key, getBoolean(key));
}
// Set the braces values
for (String key : BRACE_POSITIONS)
{
document.setString(key, getString(key));
}
// Set the spaces values
for (String key : SPACES)
{
document.setInt(key, getInt(key));
}
return document;
}
/**
* This method will strip the comments content from any whitespace characters, and will do a string comparison for
* their content.
*
* @param inputComments
* @param outputComments
* @param outputString
* @param inputString
* @return True, in case the comments have the same total content.
*/
private boolean matchComments(List<Comment> inputComments, List<Comment> outputComments, String inputString,
String outputString)
{
// Loop through the comments. Multi-line comments will be compared one-to-one, while single-line comments will
// be grouped before being compared.
IteratorQueue<Comment> inputIterator = new IteratorQueue<Comment>(inputComments.iterator());
IteratorQueue<Comment> outputIterator = new IteratorQueue<Comment>(outputComments.iterator());
while (inputIterator.hasNext() && outputIterator.hasNext())
{
String nextInputComment = getNextFlattenedComment(inputIterator, inputString);
String nextOutputComment = getNextFlattenedComment(outputIterator, outputString);
if (!nextInputComment.equals(nextOutputComment))
{
if (FormatterPlugin.getDefault().isDebugging())
{
IdeLog.logError(PHPCodeFormatterPlugin.getDefault(),
"PHP Formatter error. The following comments content did not match after the formatting: \nINPUT:\n" //$NON-NLS-1$
+ nextInputComment + "\nOUTPUT:\n" + nextOutputComment); //$NON-NLS-1$
}
return false;
}
}
// check for any remaining comments in the iterators
if (inputIterator.hasNext() || outputIterator.hasNext())
{
if (FormatterPlugin.getDefault().isDebugging())
{
IdeLog.logError(PHPCodeFormatterPlugin.getDefault(),
"PHP Formatter error: The formatter changed the comments count in the document"); //$NON-NLS-1$
}
return false;
}
return true;
}
/**
* Returns the next comment from a comments iterator in a flattened form (no whitespace)
*
* @param comments
* @param source
* @return The next comment, flattened.
*/
private String getNextFlattenedComment(IteratorQueue<Comment> comments, String source)
{
StringBuilder builder = new StringBuilder(256);
boolean isSingleLine = false;
if (comments.hasNext())
{
isSingleLine = comments.peek().getCommentType() == Comment.TYPE_SINGLE_LINE;
}
while (comments.hasNext())
{
Comment comment = comments.peek();
String commentContent = source.substring(comment.getStart(), comment.getEnd());
if (comment.getCommentType() == Comment.TYPE_SINGLE_LINE)
{
if (!isSingleLine)
{
break;
}
builder.append(SINGLE_LINE_FLATTEN_PATTERN.matcher(commentContent).replaceAll(StringUtil.EMPTY));
if (comments.hasNext())
{
comments.poll();
}
}
else
{
if (isSingleLine)
{
break;
}
builder.append(MULTI_LINE_FLATTEN_PATTERN.matcher(commentContent).replaceAll(StringUtil.EMPTY));
if (comments.hasNext())
{
comments.poll();
}
break;
}
}
return builder.toString();
}
/**
* A queue wrapper for an iterator.
*/
class IteratorQueue<E> extends AbstractQueue<E>
{
private Iterator<E> iterator;
private E nextItem;
public IteratorQueue(Iterator<E> iterator)
{
this.iterator = iterator;
}
/**
* Returns true if there is another item in the queue.
*
* @return true if there is another item in the queue; false, otherwise.
*/
public boolean hasNext()
{
return nextItem != null || iterator.hasNext();
}
/*
* (non-Javadoc)
* @see java.util.Queue#offer(java.lang.Object)
*/
public boolean offer(E o)
{
throw new UnsupportedOperationException();
}
/*
* (non-Javadoc)
* @see java.util.Queue#poll()
*/
public E poll()
{
E next = (nextItem != null) ? nextItem : iterator.next();
nextItem = null;
return next;
}
/*
* (non-Javadoc)
* @see java.util.Queue#peek()
*/
public E peek()
{
if (nextItem == null)
{
nextItem = iterator.next();
}
return nextItem;
}
/*
* (non-Javadoc)
* @see java.util.AbstractCollection#iterator()
*/
@Override
public Iterator<E> iterator()
{
throw new UnsupportedOperationException();
}
/*
* (non-Javadoc)
* @see java.util.AbstractCollection#size()
*/
@Override
public int size()
{
throw new UnsupportedOperationException();
}
}
}