/** * 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. */ // $codepro.audit.disable platformSpecificLineSeparator package com.aptana.editor.php.internal.indexer; import java.io.BufferedReader; import java.io.IOException; import java.io.InputStream; import java.io.InputStreamReader; import java.io.Reader; import java.io.StringReader; import java.util.LinkedList; import java.util.List; import java.util.regex.Matcher; import java.util.regex.Pattern; import org.eclipse.jface.text.IDocument; import org2.eclipse.php.internal.core.PHPVersion; import org2.eclipse.php.internal.core.ast.nodes.ASTNode; import org2.eclipse.php.internal.core.ast.nodes.ASTParser; import org2.eclipse.php.internal.core.ast.nodes.Comment; import org2.eclipse.php.internal.core.ast.nodes.FunctionDeclaration; import org2.eclipse.php.internal.core.ast.nodes.MethodDeclaration; import org2.eclipse.php.internal.core.ast.nodes.Program; import org2.eclipse.php.internal.core.compiler.ast.nodes.PHPDocBlock; import org2.eclipse.php.internal.core.compiler.ast.nodes.PHPDocTag; import org2.eclipse.php.internal.core.compiler.ast.nodes.VarComment; import org2.eclipse.php.internal.core.documentModel.phpElementData.IPHPDoc; import org2.eclipse.php.internal.core.documentModel.phpElementData.IPHPDocBlock; import org2.eclipse.php.internal.core.documentModel.phpElementData.IPHPDocTag; import com.aptana.core.logging.IdeLog; import com.aptana.core.util.StringUtil; import com.aptana.editor.php.PHPEditorPlugin; import com.aptana.editor.php.core.PHPVersionProvider; import com.aptana.editor.php.indexer.IElementEntry; import com.aptana.editor.php.internal.contentAssist.ContentAssistUtils; import com.aptana.editor.php.internal.core.builder.IModule; import com.aptana.editor.php.internal.indexer.language.PHPBuiltins; import com.aptana.editor.php.internal.parser.phpdoc.FunctionDocumentation; import com.aptana.editor.php.internal.parser.phpdoc.TypedDescription; import com.aptana.editor.php.util.EncodingUtils; /** * PHPDoc utilities. * * @author Denis Denisenko */ public final class PHPDocUtils { private static final String OPEN_BRACKET = "{"; //$NON-NLS-1$ private static final String CLOSE_BRACKET = "}"; //$NON-NLS-1$ private static final String DOLLAR = "$"; //$NON-NLS-1$ private static final String EMPTY_STRING = ""; //$NON-NLS-1$ private static final Pattern INPUT_TAG_PATTERN = Pattern.compile("<input[^>]*/>|<input\\s*>"); //$NON-NLS-1$ /** * Locates a PHPDoc comment above the offset in the document specified. In case the given entry is a parameter * variable, the search for the documentation will include the documentation of the parameter's wrapping function. * * @param entry * @param document * @param offset * @return A {@link PHPDocBlock}, or <code>null</code>. */ public static PHPDocBlock findFunctionPHPDocComment(IElementEntry entry, IDocument document, int offset) { boolean isParameter = ((entry.getValue() instanceof VariablePHPEntryValue) && ((VariablePHPEntryValue) entry .getValue()).isParameter()); if (entry.getModule() != null) { return findFunctionPHPDocComment(entry.getModule(), document, offset, isParameter); } // In case that the entry module is null, it's probably a PHP API documentation item, so // parse the right item. try { String entryPath = entry.getEntryPath(); if (entryPath != null) { InputStream stream = PHPBuiltins.getInstance().getBuiltinResourceStream(entryPath); if (stream != null) { BufferedReader reader = new BufferedReader(new InputStreamReader(stream)); // $codepro.audit.disable // closeWhereCreated return innerParsePHPDoc(offset, reader, isParameter); } } } catch (Exception ex) { IdeLog.logError(PHPEditorPlugin.getDefault(), "Failed locating the PHP function doc", ex); //$NON-NLS-1$ return null; } return null; } /** * Finds a PHPDoc comment above the offset in the module specified. * * @param module * @param offset * @param isParameter * Indicate that the docs we are looking for are for a parameter. * @return comment contents or null if not found. */ public static PHPDocBlock findFunctionPHPDocComment(IModule module, IDocument document, int offset, boolean isParameter) { try { Reader innerReader; if (document != null) { innerReader = new StringReader(document.get());// $codepro.audit.disable closeWhereCreated } else { innerReader = new InputStreamReader(module.getContents(), EncodingUtils.getModuleEncoding(module));// $codepro.audit.disable // closeWhereCreated } BufferedReader reader = new BufferedReader(innerReader); return innerParsePHPDoc(offset, reader, isParameter); } catch (Exception ex) { return null; } } /** * Finds a PHPDoc comment above the offset in the source that is read from the given BufferedReader. * * @param offset * @param reader * @param isParameter * Indicate that the docs we are looking for are for a parameter. * @return * @throws IOException * @throws Exception */ private static PHPDocBlock innerParsePHPDoc(int offset, BufferedReader reader, boolean isParameter) throws IOException, Exception // $codepro.audit.disable { StringBuffer moduleData = new StringBuffer(); try { char[] buf = new char[1024]; int numRead = 0; while ((numRead = reader.read(buf)) != -1) // $codepro.audit.disable { String readData = String.valueOf(buf, 0, numRead); moduleData.append(readData); } } finally { if (reader != null) { try { reader.close(); } catch (IOException e) { IdeLog.logWarning(PHPEditorPlugin.getDefault(), "Error closing a BufferedReader in the PDTPHPModuleIndexer", e,//$NON-NLS-1$ PHPEditorPlugin.INDEXER_SCOPE); } } } String contents = moduleData.toString(); int b = -1; for (int a = offset; a >= 0; a--) { char c = contents.charAt(a); if (c == '(') { b = a; break; } if (c == '\r' || c == '\n') { b = a; break; } } if (b != -1) { String str = contents.substring(b, offset); if (str.indexOf(';') == -1) { offset = b; } // System.out.println(str); } // TODO: Shalom - Get the version from the module? PHPVersion version = PHPVersionProvider.getDefaultPHPVersion(); // TODO - Perhaps we'll need to pass a preference value for the 'short-tags' instead of passing 'true' by // default. ASTParser parser = ASTParser.newParser(new StringReader(contents), version, true); // $codepro.audit.disable // closeWhereCreated Program program = parser.createAST(null); CommentsVisitor commentsVisitor = new CommentsVisitor(); program.accept(commentsVisitor); List<Comment> _comments = commentsVisitor.getComments(); PHPDocBlock docBlock = findPHPDocComment(_comments, offset, contents); if (docBlock == null && isParameter) { // We could not locate a doc right before the given offset, so we traverse up to locate the docs for the // wrapping function. The includeWrappingFunction is true only when the entry we are looking for is a // parameter variable, so there is a function that wraps it. ASTNode node = program.getElementAt(offset); if (node instanceof FunctionDeclaration) { offset = node.getStart(); if (node.getParent() instanceof MethodDeclaration) { offset = node.getParent().getStart(); } docBlock = findPHPDocComment(_comments, offset, contents); } } return docBlock; } /** * Returns the parameter documentation (as a {@link FunctionDocumentation} instance) from a given * {@link IPHPDocBlock}. * * @param block * - The block to look at when extracting the parameter documetation. * @param parameterName * @return FunctionDocumentation or null. */ public static FunctionDocumentation getParameterDocumentation(IPHPDoc block, String parameterName) { if (block == null) { return null; } FunctionDocumentation result = new FunctionDocumentation(); IPHPDocTag[] tags = block.getTags(); if (tags != null) { for (IPHPDocTag tag : tags) { switch (tag.getTagKind()) { case PHPDocTag.PARAM: String value = tag.getValue(); if (value == null) { continue; } String[] parsedTag = parseParamTagValue(value); // Support two forms of @params docs: // @param $param1 this is param 1 // @param bool $param2 this is param 2 if (!StringUtil.isEmpty(parsedTag[2]) && parsedTag[2].startsWith(parameterName) || !StringUtil.isEmpty(parsedTag[0]) && parsedTag[0].startsWith(removeDollar(parameterName))) { result.setDescription('\n' + value); return result; } } } } return null; } /** * Returns the function documentation from a given {@link IPHPDocBlock}. * * @param block * - The block to convert to a {@link FunctionDocumentation}. * @return FunctionDocumentation or null. */ public static FunctionDocumentation getFunctionDocumentation(IPHPDoc block) { if (block == null) { return null; } FunctionDocumentation result = new FunctionDocumentation(); StringBuilder docBuilder = new StringBuilder(); docBuilder.append(block.getShortDescription()); String longDescription = block.getLongDescription(); if (!StringUtil.isEmpty(longDescription)) { docBuilder.append('\n'); docBuilder.append(longDescription); } result.setDescription(docBuilder.toString()); IPHPDocTag[] tags = block.getTags(); if (tags != null) { for (IPHPDocTag tag : tags) { switch (tag.getTagKind()) { case PHPDocTag.VAR: { String value = tag.getValue(); if (value == null) { continue; } TypedDescription typeDescr = new TypedDescription(); typeDescr.addType(value); result.addVar(typeDescr); break; } case PHPDocTag.PARAM: String value = tag.getValue(); if (value == null) { continue; } String[] parsedValue = parseParamTagValue(value); if (parsedValue == null) { continue; } TypedDescription typeDescr = new TypedDescription(); typeDescr.setName(parsedValue[0]); if (parsedValue[1] != null) { typeDescr.addType(parsedValue[1]); } if (parsedValue[2] != null) { typeDescr.setDescription(parsedValue[2]); } result.addParam(typeDescr); break; case PHPDocTag.RETURN: String returnTagValue = tag.getValue().trim(); if (returnTagValue == null) { continue; } String[] returnTypes = returnTagValue.split("\\|"); //$NON-NLS-1$ for (String returnType : returnTypes) { returnTagValue = clean(returnType.trim()); returnTagValue = firstWord(returnTagValue); result.getReturn().addType(returnTagValue); } break; } } } return result; } /** * Locates a var comment above the given offset (e.g. <code><b>@var $a Type</b></code>) * * @param comments * @param offset * @param content * @return An {@link VarComment} */ public static VarComment findTypedVarComment(List<Comment> comments, int offset, String content) { Comment c = getCommentByType(comments, offset, content, Comment.TYPE_MULTILINE); if (c instanceof VarComment) { return (VarComment) c; } return null; } /** * Finds the PHPDoc comment that appears right above the given offset. In case there is no comment, or there are * non-white characters between the offset and the comment, this method returns null. * * @param comments * - The list of comments as parsed with the AST * @param offset * - offset to start search from. * @param content * - The file content * @return IPHPDocBlock The PhpDoc, or null. */ public static PHPDocBlock findPHPDocComment(List<Comment> comments, int offset, String content) { return (PHPDocBlock) getCommentByType(comments, offset, content, Comment.TYPE_PHPDOC); } /** * Locate a comment that appears right above the given index (separated only with whitespace chars). * * @param comments * @param offset * @param content * @param type * The comment's type (e.g. Comment.TYPE_PHPDOC, Comment.TYPE_PHPDOC, Comment.TYPE_SINGLE_LINE). -1 for * any type. * @return A comment, or null if none was found. */ public static Comment getCommentByType(List<Comment> comments, int offset, String content, int type) { if (comments == null || comments.isEmpty()) { return null; } Comment nearestComment = null; int commentIndex = findUpperComment(comments, offset); if (commentIndex < 0) { // The nearest comment we found should always have a negative value, as it should never overlap with the // given offset nearestComment = comments.get(-commentIndex - 1); } if (nearestComment == null) { return null; } if (type != -1 && (nearestComment.getCommentType() & type) == 0) { return null; } if (content != null) { // checking if we have anything but whitespace between comment end and // offset if (offset - 2 < 0 || nearestComment.getEnd() >= content.length() || offset - 2 >= content.length()) { return null; } // checking if we have anything but white spaces between comment end and offset for (int i = nearestComment.getEnd() + 1; i < offset - 1; i++) { char ch = content.charAt(i); if (!Character.isWhitespace(ch)) { return null; } } } return nearestComment; } /** * Performs a binary search for a specific comment. * * @return The index of the comment that holds the offset; In case no such comment exists, the return value will be * <code>(-(insertion point) - 1)</code>. The insertion point is defined as the point at which a comment * would be inserted into the list in case it was matching the offset. */ public static int findComment(List<Comment> comments, int offset) { int low = 0; int high = comments.size() - 1; while (low <= high) { int mid = (low + high) >> 1; Comment midVal = comments.get(mid); int cmp; if (offset >= midVal.getStart() && offset < midVal.getEnd()) { return mid; // Found the comment! } cmp = midVal.getStart() - offset; if (cmp < 0) { low = mid + 1; } else if (cmp > 0) { high = mid - 1; } } return -(low + 1); // Can't find a match } /** * Perform a binary search for a comment that appears right on top of the given offset */ private static int findUpperComment(List<Comment> comments, int offset) { int low = 0; int high = comments.size() - 1; while (low <= high) { int mid = (low + high) >>> 1; Comment midVal = (Comment) comments.get(mid); int cmp = midVal.getStart() - offset; if (cmp < 0) { low = mid + 1; } else if (cmp > 0) { high = mid - 1; } else { return mid; // key found } } return -low; // key not found. } /** * Gets the first word of a sentence. * * @param str * - string. * @return first word of a sentence. */ private static String firstWord(String str) { int firstSpacePos = -1; for (int i = 0; i < str.length(); i++) { char ch = str.charAt(i); if (Character.isWhitespace(ch)) { firstSpacePos = i; break; } } if (firstSpacePos == -1) { return str; } else if (firstSpacePos == 0) { return EMPTY_STRING; } else { return str.substring(0, firstSpacePos); } } /** * Parses parameter tag value. * * @param toParse * @return array of parse results: first element is parameter name (without the $ symbol), next is parameter type if * available and the third is parameter description if available. */ private static String[] parseParamTagValue(String toParse) { if (toParse == null || toParse.length() == 0) { return null; } String[] parts = toParse.split("\\s+"); //$NON-NLS-1$ if (parts == null || parts.length == 0) { return null; } String[] result = new String[3]; boolean isJSLike = false; if (parts[0].contains(OPEN_BRACKET) || parts[0].contains(CLOSE_BRACKET)) { isJSLike = true; } if (parts[0].contains(DOLLAR)) { isJSLike = true; } if (parts.length > 1 && (parts[1].contains(DOLLAR))) { isJSLike = true; } if (isJSLike) { if (parts.length == 1) { result[0] = clean(parts[0]); } else if (parts.length == 2) { result[0] = clean(parts[1]); result[1] = clean(parts[0]); } else { result[0] = clean(parts[1]); result[1] = clean(parts[0]); StringBuffer buf = new StringBuffer(); for (int i = 2; i < parts.length; i++) { buf.append(parts[i]); if (i != parts.length) { buf.append(' '); } } result[2] = buf.toString(); } } else { if (parts.length == 1) { result[0] = clean(parts[0]); } else if (parts.length == 2) { result[0] = clean(parts[0]); result[1] = clean(parts[1]); } else { result[0] = clean(parts[0]); result[1] = clean(parts[1]); StringBuffer buf = new StringBuffer(); for (int i = 2; i < parts.length; i++) { buf.append(parts[i]); if (i != parts.length) { buf.append(' '); } } result[2] = buf.toString(); } } return result; } /** * Removes curves around the string. * * @param in * - input. * @return string with curves removed. */ private static String removeCurves(String in) { String result = in; if (result.startsWith(OPEN_BRACKET)) { result = result.substring(1); } if (result.equals(CLOSE_BRACKET)) { return EMPTY_STRING; } if (result.endsWith(CLOSE_BRACKET)) { result = result.substring(0, result.length() - 1); } return result; } /** * Cleans the string from curves and dollar. * * @param in * - input. * @return cleansed string */ private static String clean(String in) { String res1 = removeCurves(in); return removeDollar(res1); } /** * Removes dollar symbol. * * @param in * - input string. * @return cleansed string. */ private static String removeDollar(String in) { if (in.startsWith(DOLLAR)) { return in.substring(1); } return in; } public static String computeDocumentation(FunctionDocumentation documentation, IDocument document, String name) { String additionalInfo = Messages.PHPDocUtils_noAvailableDocs; StringBuilder bld = new StringBuilder(); bld.append("<b>" + name + "</b><br>"); //$NON-NLS-1$ //$NON-NLS-2$ if (documentation != null) { String longDescription = documentation.getDescription(); longDescription = longDescription.replaceAll("\r\n", "<br>"); //$NON-NLS-1$ //$NON-NLS-2$ longDescription = longDescription.replaceAll("\r", "<br>"); //$NON-NLS-1$ //$NON-NLS-2$ longDescription = longDescription.replaceAll("\n", "<br>"); //$NON-NLS-1$ //$NON-NLS-2$ if (longDescription.length() > 0) { bld.append(longDescription); bld.append("<br>"); //$NON-NLS-1$ } TypedDescription[] tagsAsArray = documentation.getParams(); // buf.append("<br>"); //$NON-NLS-1$ for (int a = 0; a < tagsAsArray.length; a++) { bld.append("<br>"); //$NON-NLS-1$ bld.append("@<b>"); //$NON-NLS-1$ bld.append("param "); //$NON-NLS-1$ // buf.append(); bld.append("</b>"); //$NON-NLS-1$ bld.append(tagsAsArray[a].getName()); bld.append(' '); for (String s : tagsAsArray[a].getTypes()) { bld.append(s); bld.append(' '); } bld.append(' '); bld.append(ContentAssistUtils.truncateLineIfNeeded(tagsAsArray[a].getDescription())); } } else { bld.append(additionalInfo); } if (documentation != null) { TypedDescription return1 = documentation.getReturn(); if (return1 != null) { String[] types = return1.getTypes(); if (types.length > 0) { bld.append("<br>"); //$NON-NLS-1$ bld.append("@<b>return </b>"); //$NON-NLS-1$ bld.append(ContentAssistUtils.truncateLineIfNeeded(return1.getDescription())); StringBuilder typesBuilder = new StringBuilder(); for (int a = 0; a < types.length; a++) { typesBuilder.append(types[a]); typesBuilder.append(' '); } bld.append(ContentAssistUtils.truncateLineIfNeeded(typesBuilder.toString())); } } List<TypedDescription> vars = documentation.getVars(); if (vars != null) { for (TypedDescription var : vars) { if (var != null) { String[] types = var.getTypes(); if (types.length > 0) { bld.append("<br>"); //$NON-NLS-1$ bld.append("<b>"); //$NON-NLS-1$ bld.append(Messages.PHPDocUtils_documentedType); bld.append("</b>"); //$NON-NLS-1$ bld.append(var.getDescription()); for (int a = 0; a < types.length; a++) { bld.append(types[a]); bld.append(' '); } } } } } } // Specifically look for HTML 'input' tags and change their open and close chars. The HTML rendering does // not // remove them when the hover is rendered, introducing form inputs in the hover popup. // @See https://aptana.lighthouseapp.com/projects/35272/tickets/1653 Matcher inputMatcher = INPUT_TAG_PATTERN.matcher(bld.toString()); int addedOffset = 0; while (inputMatcher.find()) { int start = inputMatcher.start(); int end = inputMatcher.end(); bld.replace(start + addedOffset, start + addedOffset + 1, "<"); //$NON-NLS-1$ addedOffset += 2; bld.replace(end + addedOffset, end + addedOffset + 1, ">"); //$NON-NLS-1$ addedOffset += 4; } additionalInfo = bld.toString(); return additionalInfo; } /** * Returns a list of {@link VarComment}s within a specified start and end offsets. * * @param comments * A sorted list of comments * @param start * @param end */ public static List<VarComment> findTypedVarComments(List<Comment> comments, int start, int end) { List<VarComment> result = new LinkedList<VarComment>(); // locate the last comment in the given list and create a result list from all the VarComments on top of it, // till we hit the start offset. // We use a linked list to append comments at the start of the result with better performance. int commentIndex = findUpperComment(comments, end); if (commentIndex < 0) { // The nearest comment we found should always have a negative value, as it should never overlap with the // given offset commentIndex = -commentIndex - 1; } if (commentIndex >= comments.size()) { // off bounds return result; } for (; commentIndex > -1; commentIndex--) { Comment comment = comments.get(commentIndex); if (comment.getStart() < start) { break; } if (comment instanceof VarComment) { result.add(0, (VarComment) comment); } } return result; } }