mirror of
https://gitflic.ru/project/openide/openide.git
synced 2026-10-01 06:51:17 +07:00
Refactored Python docstring parsing, more careful handling of formatting inside type tags
This commit is contained in:
@@ -1,14 +1,14 @@
|
||||
package com.jetbrains.python.documentation;
|
||||
|
||||
import com.google.common.base.CharMatcher;
|
||||
import com.intellij.openapi.util.TextRange;
|
||||
import com.intellij.psi.PsiElement;
|
||||
import com.intellij.psi.PsiReference;
|
||||
import com.intellij.psi.PsiReferenceProvider;
|
||||
import com.intellij.psi.util.PsiTreeUtil;
|
||||
import com.intellij.util.ArrayUtil;
|
||||
import com.intellij.util.ProcessingContext;
|
||||
import com.jetbrains.python.PyNames;
|
||||
import com.jetbrains.python.psi.PyDocStringOwner;
|
||||
import com.jetbrains.python.psi.PyStringLiteralExpression;
|
||||
import com.jetbrains.python.psi.types.PyType;
|
||||
import com.jetbrains.python.psi.types.PyTypeParser;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
@@ -22,80 +22,48 @@ import java.util.Map;
|
||||
* @author yole
|
||||
*/
|
||||
public class DocStringReferenceProvider extends PsiReferenceProvider {
|
||||
private final String[] ALL_PARAM_TAGS;
|
||||
|
||||
public DocStringReferenceProvider() {
|
||||
List<String> allParamTags = new ArrayList<String>();
|
||||
for (String tag : EpydocString.PARAM_TAGS) {
|
||||
allParamTags.add("@" + tag);
|
||||
allParamTags.add(":" + tag);
|
||||
}
|
||||
for (String tag : EpydocString.RTYPE_TAGS) {
|
||||
allParamTags.add("@" + tag);
|
||||
allParamTags.add(":" + tag);
|
||||
}
|
||||
allParamTags.add("@type");
|
||||
allParamTags.add(":type");
|
||||
ALL_PARAM_TAGS = ArrayUtil.toStringArray(allParamTags);
|
||||
}
|
||||
|
||||
@NotNull
|
||||
@Override
|
||||
public PsiReference[] getReferencesByElement(@NotNull final PsiElement element, @NotNull ProcessingContext context) {
|
||||
final PyDocStringOwner docStringOwner = PsiTreeUtil.getParentOfType(element, PyDocStringOwner.class);
|
||||
if (docStringOwner != null && element == docStringOwner.getDocStringExpression()) {
|
||||
// TODO: Think about swithing to the StructuredDocString parser
|
||||
final List<PsiReference> result = new ArrayList<PsiReference>();
|
||||
String docString = element.getText();
|
||||
int pos = 0;
|
||||
while (pos < docString.length()) {
|
||||
final TextRange tagRange = findNextTag(docString, pos, ALL_PARAM_TAGS);
|
||||
if (tagRange == null) {
|
||||
break;
|
||||
}
|
||||
pos = CharMatcher.anyOf(" \t*").negate().indexIn(docString, tagRange.getEndOffset());
|
||||
CharMatcher identifierMatcher = new CharMatcher() {
|
||||
@Override public boolean matches(char c) {
|
||||
return Character.isLetterOrDigit(c) || c == '_';
|
||||
}}.negate();
|
||||
final String tagName = docString.substring(tagRange.getStartOffset(), tagRange.getEndOffset());
|
||||
boolean isRType = isReturnType(tagName);
|
||||
if (tagName.startsWith(":") && !isRType) { // if ReST parameter tag
|
||||
int ws = CharMatcher.anyOf(" \t*").indexIn(docString, pos+1);
|
||||
if (ws != -1) {
|
||||
int next = CharMatcher.anyOf(" \t*").negate().indexIn(docString, ws);
|
||||
if (next != -1 && !docString.substring(pos, next).contains(":")) {
|
||||
int endPos = identifierMatcher.indexIn(docString, pos);
|
||||
result.addAll(parseTypeReferences(element, docString.substring(pos, endPos), pos));
|
||||
pos = next;
|
||||
final PyStringLiteralExpression expr = (PyStringLiteralExpression)element;
|
||||
final List<TextRange> ranges = expr.getStringValueTextRanges();
|
||||
final String text = expr.getStringValue();
|
||||
if (!ranges.isEmpty()) {
|
||||
final List<PsiReference> result = new ArrayList<PsiReference>();
|
||||
final int offset = ranges.get(0).getStartOffset();
|
||||
// XXX: It does not work with multielement docstrings
|
||||
StructuredDocString docString = StructuredDocString.parse(text);
|
||||
if (docString != null) {
|
||||
final List<Substring> paramNames = new ArrayList<Substring>();
|
||||
paramNames.addAll(docString.getParameterSubstrings());
|
||||
paramNames.addAll(docString.getKeywordArgumentSubstrings());
|
||||
for (Substring name : paramNames) {
|
||||
final String s = name.toString();
|
||||
if (PyNames.isIdentifier(s)) {
|
||||
result.add(new DocStringParameterReference(element, name.getTextRange().shiftRight(offset)));
|
||||
}
|
||||
final Substring type = docString.getParamTypeSubstring(s);
|
||||
if (type != null) {
|
||||
result.addAll(parseTypeReferences(element, type, offset));
|
||||
}
|
||||
}
|
||||
}
|
||||
int endPos = identifierMatcher.indexIn(docString, pos);
|
||||
if (endPos < 0) {
|
||||
endPos = docString.length();
|
||||
}
|
||||
if (!isRType)
|
||||
result.add(new DocStringParameterReference(element, new TextRange(pos, endPos)));
|
||||
if (tagName.equals(":type") || tagName.equals("@type") || isRType) {
|
||||
pos = CharMatcher.anyOf(" \t*").negate().indexIn(docString, endPos+1);
|
||||
endPos = CharMatcher.anyOf("\n\r").indexIn(docString, pos+1);
|
||||
if (endPos == -1) {
|
||||
endPos = pos;
|
||||
final Substring rtype = docString.getReturnTypeSubstring();
|
||||
if (rtype != null) {
|
||||
result.addAll(parseTypeReferences(element, rtype, offset));
|
||||
}
|
||||
result.addAll(parseTypeReferences(element, docString.substring(pos, endPos), pos));
|
||||
}
|
||||
pos = endPos;
|
||||
return result.toArray(new PsiReference[result.size()]);
|
||||
}
|
||||
|
||||
return result.toArray(new PsiReference[result.size()]);
|
||||
}
|
||||
return PsiReference.EMPTY_ARRAY;
|
||||
}
|
||||
|
||||
private static List<PsiReference> parseTypeReferences(PsiElement anchor, String s, int offset) {
|
||||
private static List<PsiReference> parseTypeReferences(PsiElement anchor, Substring s, int offset) {
|
||||
final List<PsiReference> result = new ArrayList<PsiReference>();
|
||||
final PyTypeParser.ParseResult parseResult = PyTypeParser.parse(anchor, s);
|
||||
final PyTypeParser.ParseResult parseResult = PyTypeParser.parse(anchor, s.toString());
|
||||
offset = s.getTextRange().getStartOffset() + offset;
|
||||
final Map<TextRange, PyType> types = parseResult.getTypes();
|
||||
final Map<PyType, TextRange> fullRanges = parseResult.getFullRanges();
|
||||
for (Map.Entry<TextRange, PyType> pair : types.entrySet()) {
|
||||
@@ -107,10 +75,6 @@ public class DocStringReferenceProvider extends PsiReferenceProvider {
|
||||
return result;
|
||||
}
|
||||
|
||||
private static boolean isReturnType(String tagName) {
|
||||
return tagName.equals(":rtype") || tagName.equals("@rtype") || tagName.equals("@returntype");
|
||||
}
|
||||
|
||||
@Nullable
|
||||
public static TextRange findNextTag(String docString, int pos, String[] paramTags) {
|
||||
int result = Integer.MAX_VALUE;
|
||||
|
||||
@@ -9,9 +9,7 @@ public class DocStringUtil {
|
||||
|
||||
public static String trimDocString(String s) {
|
||||
return s.trim()
|
||||
.replaceFirst("^(:class:|:py:class:)", "")
|
||||
.trim()
|
||||
.replaceFirst("^`", "")
|
||||
.replaceFirst("(`|\\.)$", "");
|
||||
.replaceFirst("^(:class:`|:py:class:`|[A-Z]\\{)", "")
|
||||
.replaceFirst("(`|\\})?\\.?$", "");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -457,11 +457,12 @@ class DocumentationBuilder {
|
||||
result.append("<br/><br/><b>Additional:</b><br/>");
|
||||
result.append("<table>");
|
||||
for (String tagName : additionalTags) {
|
||||
final List<String> args = docString.getTagArguments(tagName);
|
||||
for(String arg : args) {
|
||||
final List<Substring> args = docString.getTagArguments(tagName);
|
||||
for(Substring arg : args) {
|
||||
final String s = arg.toString();
|
||||
result.append("<tr><td align=\"right\"><b>").append(tagName);
|
||||
result.append(" ").append(arg).append(":</b>");
|
||||
result.append("</td><td>").append(docString.getTagValue(tagName, arg)).append("</td></tr>");
|
||||
result.append(" ").append(s).append(":</b>");
|
||||
result.append("</td><td>").append(docString.getTagValue(tagName, s)).append("</td></tr>");
|
||||
}
|
||||
result.append("</table>");
|
||||
}
|
||||
|
||||
@@ -4,9 +4,7 @@ import com.intellij.openapi.util.text.StringUtil;
|
||||
import com.intellij.xml.util.XmlTagUtilBase;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.*;
|
||||
|
||||
/**
|
||||
* @author yole
|
||||
@@ -14,6 +12,7 @@ import java.util.Map;
|
||||
public class EpydocString extends StructuredDocString {
|
||||
public static String[] RAISES_TAGS = new String[] { "raises", "raise", "except", "exception" };
|
||||
public static String[] PARAM_TAGS = new String[] { "param", "parameter", "arg", "argument" };
|
||||
public static String[] PARAM_TYPE_TAGS = new String[] { "type" };
|
||||
public static String[] RETURN_TAGS = new String[] { "return", "returns" };
|
||||
public static String[] RTYPE_TAGS = new String[] { "rtype", "returntype" };
|
||||
public static String[] KEYWORD_ARGUMENT_TAGS = new String[] { "keyword", "kwarg", "kwparam" };
|
||||
@@ -45,19 +44,18 @@ public class EpydocString extends StructuredDocString {
|
||||
|
||||
@Override
|
||||
public List<String> getParameters() {
|
||||
return getTagArguments(PARAM_TAGS);
|
||||
return toUniqueStrings(getParameterSubstrings());
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<String> getKeywordArguments() {
|
||||
return getTagArguments(KEYWORD_ARGUMENT_TAGS);
|
||||
return toUniqueStrings(getKeywordArgumentSubstrings());
|
||||
}
|
||||
|
||||
@Override
|
||||
@Nullable
|
||||
public String getReturnType() {
|
||||
String value = getTagValue(RTYPE_TAGS);
|
||||
return removeInlineMarkup(value);
|
||||
return removeInlineMarkup(getReturnTypeSubstring());
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -68,14 +66,13 @@ public class EpydocString extends StructuredDocString {
|
||||
@Override
|
||||
@Nullable
|
||||
public String getParamType(@Nullable String paramName) {
|
||||
String value = paramName == null ? getTagValue("type") : getTagValue("type", paramName);
|
||||
return removeInlineMarkup(value);
|
||||
return removeInlineMarkup(getParamTypeSubstring(paramName));
|
||||
}
|
||||
|
||||
@Override
|
||||
@Nullable
|
||||
public String getParamDescription(String paramName) {
|
||||
String value = getTagValue(PARAM_TAGS, paramName);
|
||||
Substring value = getTagValue(PARAM_TAGS, paramName);
|
||||
if (value == null) {
|
||||
value = getTagValue(PARAM_TAGS, "*" + paramName);
|
||||
}
|
||||
@@ -92,7 +89,7 @@ public class EpydocString extends StructuredDocString {
|
||||
|
||||
@Override
|
||||
public List<String> getRaisedExceptions() {
|
||||
return getTagArguments(RAISES_TAGS);
|
||||
return toUniqueStrings(getTagArguments(RAISES_TAGS));
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -102,16 +99,22 @@ public class EpydocString extends StructuredDocString {
|
||||
|
||||
@Override
|
||||
public String getAttributeDescription() {
|
||||
return convertInlineMarkup(getTagValue(VARIABLE_TAGS), true);
|
||||
final Substring value = getTagValue(VARIABLE_TAGS);
|
||||
return convertInlineMarkup(value != null ? value.toString() : null, true);
|
||||
}
|
||||
|
||||
@Nullable
|
||||
public static String removeInlineMarkup(String s) {
|
||||
public static String removeInlineMarkup(@Nullable String s) {
|
||||
return convertInlineMarkup(s, false);
|
||||
}
|
||||
|
||||
@Nullable
|
||||
private static String convertInlineMarkup(String s, boolean toHTML) {
|
||||
private static String removeInlineMarkup(@Nullable Substring s) {
|
||||
return convertInlineMarkup(s != null ? s.concatTrimmedLines(" ") : null, false);
|
||||
}
|
||||
|
||||
@Nullable
|
||||
private static String convertInlineMarkup(@Nullable String s, boolean toHTML) {
|
||||
if (s == null) return null;
|
||||
MarkupConverter converter = toHTML ? new HTMLConverter() : new MarkupConverter();
|
||||
converter.appendWithMarkup(s);
|
||||
@@ -245,14 +248,19 @@ public class EpydocString extends StructuredDocString {
|
||||
}
|
||||
|
||||
@Nullable
|
||||
public static String inlineMarkupToHTML(String s) {
|
||||
public static String inlineMarkupToHTML(@Nullable String s) {
|
||||
return convertInlineMarkup(s, true);
|
||||
}
|
||||
|
||||
@Nullable
|
||||
private static String inlineMarkupToHTML(@Nullable Substring s) {
|
||||
return inlineMarkupToHTML(s != null ? s.concatTrimmedLines(" ") : null);
|
||||
}
|
||||
|
||||
public List<String> getAdditionalTags() {
|
||||
List<String> list = new ArrayList<String>();
|
||||
for (String tagName : ADDITIONAL) {
|
||||
final Map<String, String> map = myArgTagValues.get(tagName);
|
||||
final Map<Substring, Substring> map = myArgTagValues.get(tagName);
|
||||
if (map != null) {
|
||||
list.add(tagName);
|
||||
}
|
||||
@@ -260,4 +268,26 @@ public class EpydocString extends StructuredDocString {
|
||||
return list;
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<Substring> getParameterSubstrings() {
|
||||
final List<Substring> results = new ArrayList<Substring>();
|
||||
results.addAll(getTagArguments(PARAM_TAGS));
|
||||
results.addAll(getTagArguments(PARAM_TYPE_TAGS));
|
||||
return results;
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<Substring> getKeywordArgumentSubstrings() {
|
||||
return getTagArguments(KEYWORD_ARGUMENT_TAGS);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Substring getReturnTypeSubstring() {
|
||||
return getTagValue(RTYPE_TAGS);
|
||||
}
|
||||
|
||||
@Override
|
||||
Substring getParamTypeSubstring(@Nullable String paramName) {
|
||||
return paramName == null ? getTagValue("type") : getTagValue("type", paramName);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,14 +1,10 @@
|
||||
package com.jetbrains.python.documentation;
|
||||
|
||||
import com.google.common.collect.Maps;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.regex.Matcher;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
/**
|
||||
* @author yole
|
||||
@@ -23,54 +19,59 @@ public class SphinxDocString extends StructuredDocString {
|
||||
super(docstringText, ":");
|
||||
}
|
||||
|
||||
@Nullable
|
||||
protected static String concatTrimmedLines(@Nullable Substring s) {
|
||||
return s != null ? s.concatTrimmedLines(" ") : null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<String> getParameters() {
|
||||
return getTagArguments(EpydocString.PARAM_TAGS);
|
||||
return toUniqueStrings(getParameterSubstrings());
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<String> getKeywordArguments() {
|
||||
return getTagArguments(KEYWORD_ARGUMENT_TAGS);
|
||||
return toUniqueStrings(getKeywordArgumentSubstrings());
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getKeywordArgumentDescription(String paramName) {
|
||||
return getTagValue(KEYWORD_ARGUMENT_TAGS, paramName);
|
||||
return concatTrimmedLines(getTagValue(KEYWORD_ARGUMENT_TAGS, paramName));
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getReturnType() {
|
||||
return getTagValue("rtype");
|
||||
return concatTrimmedLines(getReturnTypeSubstring());
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getParamType(@Nullable String paramName) {
|
||||
return paramName == null ? getTagValue("type") : getTagValue("type", paramName);
|
||||
return concatTrimmedLines(getParamTypeSubstring(paramName));
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getParamDescription(String paramName) {
|
||||
return getTagValue("param", paramName);
|
||||
return concatTrimmedLines(getTagValue("param", paramName));
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getReturnDescription() {
|
||||
return getTagValue(EpydocString.RETURN_TAGS);
|
||||
return concatTrimmedLines(getTagValue(EpydocString.RETURN_TAGS));
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<String> getRaisedExceptions() {
|
||||
return getTagArguments(EpydocString.RAISES_TAGS);
|
||||
return toUniqueStrings(getTagArguments(EpydocString.RAISES_TAGS));
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getRaisedExceptionDescription(String exceptionName) {
|
||||
return getTagValue(EpydocString.RAISES_TAGS, exceptionName);
|
||||
return concatTrimmedLines(getTagValue(EpydocString.RAISES_TAGS, exceptionName));
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getAttributeDescription() {
|
||||
return getTagValue(EpydocString.VARIABLE_TAGS);
|
||||
return concatTrimmedLines(getTagValue(EpydocString.VARIABLE_TAGS));
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -78,54 +79,26 @@ public class SphinxDocString extends StructuredDocString {
|
||||
return Collections.emptyList();
|
||||
}
|
||||
|
||||
protected int parseTag(String[] lines, int index, String tagPrefix) {
|
||||
String line = lines[index].trim();
|
||||
if (line.startsWith(tagPrefix)) {
|
||||
line = line.substring(tagPrefix.length());
|
||||
final Pattern tagPattern = Pattern.compile("([a-z]+)(.*):([^:]*)");
|
||||
final Matcher tagMatcher = tagPattern.matcher(line);
|
||||
if (tagMatcher.matches()) {
|
||||
final String tagName = tagMatcher.group(1);
|
||||
final String argName = tagMatcher.group(2).trim();
|
||||
final StringBuilder builder = new StringBuilder();
|
||||
builder.append(tagMatcher.group(3).trim());
|
||||
for (index += 1; index < lines.length && !lines[index].trim().startsWith(tagPrefix); index++) {
|
||||
builder.append(" ");
|
||||
builder.append(lines[index].trim());
|
||||
}
|
||||
index--;
|
||||
final String argValue = builder.toString().trim();
|
||||
if (argName.isEmpty()) {
|
||||
mySimpleTagValues.put(tagName, argValue);
|
||||
}
|
||||
else {
|
||||
if ("param".equals(tagName) || "parameter".equals(tagName) ||
|
||||
"arg".equals(tagName) || "argument".equals(tagName)) {
|
||||
final Pattern argPattern = Pattern.compile("(.*) ([a-zA-Z_0-9]+)");
|
||||
final Matcher argMatcher = argPattern.matcher(argName);
|
||||
if (argMatcher.matches()) {
|
||||
final String type = argMatcher.group(1).trim();
|
||||
final String arg = argMatcher.group(2);
|
||||
getTagValuesMap("type").put(arg, type);
|
||||
getTagValuesMap(tagName).put(arg, argValue);
|
||||
}
|
||||
}
|
||||
else {
|
||||
getTagValuesMap(tagName).put(argName, argValue);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return index;
|
||||
@Override
|
||||
public List<Substring> getParameterSubstrings() {
|
||||
final List<Substring> results = new ArrayList<Substring>();
|
||||
results.addAll(getTagArguments(EpydocString.PARAM_TAGS));
|
||||
results.addAll(getTagArguments(EpydocString.PARAM_TYPE_TAGS));
|
||||
return results;
|
||||
}
|
||||
|
||||
@NotNull
|
||||
private Map<String, String> getTagValuesMap(String key) {
|
||||
Map<String, String> map = myArgTagValues.get(key);
|
||||
if (map == null) {
|
||||
map = Maps.newLinkedHashMap();
|
||||
myArgTagValues.put(key, map);
|
||||
}
|
||||
return map;
|
||||
@Override
|
||||
public List<Substring> getKeywordArgumentSubstrings() {
|
||||
return getTagArguments(KEYWORD_ARGUMENT_TAGS);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Substring getReturnTypeSubstring() {
|
||||
return getTagValue("rtype");
|
||||
}
|
||||
|
||||
@Override
|
||||
Substring getParamTypeSubstring(@Nullable String paramName) {
|
||||
return paramName == null ? getTagValue("type") : getTagValue("type", paramName);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,22 +1,29 @@
|
||||
package com.jetbrains.python.documentation;
|
||||
|
||||
import com.google.common.collect.Maps;
|
||||
import com.intellij.openapi.util.text.LineTokenizer;
|
||||
import com.intellij.openapi.util.text.StringUtil;
|
||||
import com.intellij.openapi.util.TextRange;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.regex.Matcher;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
/**
|
||||
* @author yole
|
||||
*/
|
||||
public abstract class StructuredDocString {
|
||||
protected final String myDescription;
|
||||
protected final Map<String, String> mySimpleTagValues = Maps.newHashMap();
|
||||
protected final Map<String, Map<String, String>> myArgTagValues = Maps.newHashMap();
|
||||
|
||||
protected final Map<String, Substring> mySimpleTagValues = Maps.newHashMap();
|
||||
protected final Map<String, Map<Substring, Substring>> myArgTagValues = Maps.newHashMap();
|
||||
|
||||
private static final Pattern RE_STRICT_TAG_LINE = Pattern.compile("([a-z]+)(.*): (.*)");
|
||||
private static final Pattern RE_LOOSE_TAG_LINE = Pattern.compile("([a-z]+) ([a-zA-Z_0-9]*):?([^:]*)");
|
||||
private static final Pattern RE_ARG_TYPE = Pattern.compile("(.*) ([a-zA-Z_0-9]+)");
|
||||
|
||||
@Nullable
|
||||
public static StructuredDocString parse(String text) {
|
||||
@@ -29,59 +36,110 @@ public abstract class StructuredDocString {
|
||||
return new EpydocString(text);
|
||||
}
|
||||
|
||||
protected StructuredDocString(String docstringText, String tagPrefix) {
|
||||
final String[] lines = LineTokenizer.tokenize(docstringText, false);
|
||||
int i = 0;
|
||||
StringBuilder descBuilder = new StringBuilder();
|
||||
while (i < lines.length) {
|
||||
String line = lines[i].trim();
|
||||
protected StructuredDocString(String docStringText, String tagPrefix) {
|
||||
final Substring docString = new Substring(docStringText);
|
||||
final List<Substring> lines = docString.splitLines();
|
||||
final int nlines = lines.size();
|
||||
final StringBuilder builder = new StringBuilder();
|
||||
int lineno = 0;
|
||||
while (lineno < nlines) {
|
||||
Substring line = lines.get(lineno).trim();
|
||||
if (line.startsWith(tagPrefix)) {
|
||||
i = parseTag(lines, i, tagPrefix);
|
||||
lineno = parseTag(lines, lineno, tagPrefix);
|
||||
}
|
||||
else {
|
||||
descBuilder.append(line).append("\n");
|
||||
builder.append(line.toString()).append("\n");
|
||||
}
|
||||
i++;
|
||||
lineno++;
|
||||
}
|
||||
myDescription = descBuilder.toString();
|
||||
myDescription = builder.toString();
|
||||
}
|
||||
|
||||
public String getDescription() {
|
||||
return myDescription;
|
||||
}
|
||||
|
||||
protected int parseTag(String[] lines, int index, String tagPrefix) {
|
||||
String line = lines[index].trim();
|
||||
int tagEnd = StringUtil.indexOfAny(line, " \t:", 1, line.length());
|
||||
if (tagEnd < 0) return index;
|
||||
String tagName = line.substring(1, tagEnd);
|
||||
String tagValue = line.substring(tagEnd).trim();
|
||||
int pos = tagValue.indexOf(':');
|
||||
if (pos < 0) return index;
|
||||
String value = tagValue.substring(pos+1).trim();
|
||||
while(index+1 < lines.length && !lines[index+1].trim().startsWith(tagPrefix)) {
|
||||
index++;
|
||||
value += " " + lines[index].trim();
|
||||
@NotNull
|
||||
private Map<Substring, Substring> getTagValuesMap(String key) {
|
||||
Map<Substring, Substring> map = myArgTagValues.get(key);
|
||||
if (map == null) {
|
||||
map = Maps.newLinkedHashMap();
|
||||
myArgTagValues.put(key, map);
|
||||
}
|
||||
if (pos == 0) {
|
||||
mySimpleTagValues.put(tagName, value);
|
||||
}
|
||||
else {
|
||||
String arg = tagValue.substring(0, pos).trim();
|
||||
Map<String, String> argValues = myArgTagValues.get(tagName);
|
||||
if (argValues == null) {
|
||||
argValues = Maps.newLinkedHashMap();
|
||||
myArgTagValues.put(tagName, argValues);
|
||||
return map;
|
||||
}
|
||||
|
||||
protected int parseTag(List<Substring> lines, int lineno, String tagPrefix) {
|
||||
final Substring lineWithPrefix = lines.get(lineno).trim();
|
||||
if (lineWithPrefix.startsWith(tagPrefix)) {
|
||||
final Substring line = lineWithPrefix.substring(tagPrefix.length());
|
||||
final Matcher strictTagMatcher = RE_STRICT_TAG_LINE.matcher(line);
|
||||
final Matcher looseTagMatcher = RE_LOOSE_TAG_LINE.matcher(line);
|
||||
Matcher tagMatcher = null;
|
||||
if (strictTagMatcher.matches()) {
|
||||
tagMatcher = strictTagMatcher;
|
||||
}
|
||||
else if (looseTagMatcher.matches()) {
|
||||
tagMatcher = looseTagMatcher;
|
||||
}
|
||||
if (tagMatcher != null) {
|
||||
final Substring tagName = line.getMatcherGroup(tagMatcher, 1);
|
||||
final Substring argName = line.getMatcherGroup(tagMatcher, 2).trim();
|
||||
final TextRange firstArgLineRange = line.getMatcherGroup(tagMatcher, 3).trim().getTextRange();
|
||||
final int linesCount = lines.size();
|
||||
final int argStart = firstArgLineRange.getStartOffset();
|
||||
int argEnd = firstArgLineRange.getEndOffset();
|
||||
while (lineno + 1 < linesCount) {
|
||||
final Substring nextLine = lines.get(lineno + 1).trim();
|
||||
if (nextLine.length() == 0 || nextLine.startsWith(tagPrefix)) {
|
||||
break;
|
||||
}
|
||||
argEnd = nextLine.getTextRange().getEndOffset();
|
||||
lineno++;
|
||||
}
|
||||
final Substring argValue = new Substring(argName.getSuperString(), argStart, argEnd);
|
||||
final String tagNameString = tagName.toString();
|
||||
if (argName.length() == 0) {
|
||||
mySimpleTagValues.put(tagNameString, argValue);
|
||||
}
|
||||
else {
|
||||
if ("param".equals(tagNameString) || "parameter".equals(tagNameString) ||
|
||||
"arg".equals(tagNameString) || "argument".equals(tagNameString)) {
|
||||
final Matcher argTypeMatcher = RE_ARG_TYPE.matcher(argName);
|
||||
if (argTypeMatcher.matches()) {
|
||||
final Substring type = argName.getMatcherGroup(argTypeMatcher, 1).trim();
|
||||
final Substring arg = argName.getMatcherGroup(argTypeMatcher, 2);
|
||||
getTagValuesMap("type").put(arg, type);
|
||||
getTagValuesMap(tagNameString).put(arg, argValue);
|
||||
}
|
||||
else {
|
||||
getTagValuesMap(tagNameString).put(argName, argValue);
|
||||
}
|
||||
}
|
||||
else {
|
||||
getTagValuesMap(tagNameString).put(argName, argValue);
|
||||
}
|
||||
}
|
||||
}
|
||||
argValues.put(arg, value);
|
||||
}
|
||||
return index;
|
||||
return lineno;
|
||||
}
|
||||
|
||||
protected static List<String> toUniqueStrings(List<?> objects) {
|
||||
final List<String> result = new ArrayList<String>(objects.size());
|
||||
for (Object o : objects) {
|
||||
final String s = o.toString();
|
||||
if (!result.contains(s)) {
|
||||
result.add(s);
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
@Nullable
|
||||
public String getTagValue(String... tagNames) {
|
||||
public Substring getTagValue(String... tagNames) {
|
||||
for (String tagName : tagNames) {
|
||||
final String value = mySimpleTagValues.get(tagName);
|
||||
final Substring value = mySimpleTagValues.get(tagName);
|
||||
if (value != null) {
|
||||
return value;
|
||||
}
|
||||
@@ -90,27 +148,27 @@ public abstract class StructuredDocString {
|
||||
}
|
||||
|
||||
@Nullable
|
||||
public String getTagValue(String tagName, String argName) {
|
||||
Map<String, String> argValues = myArgTagValues.get(tagName);
|
||||
return argValues == null ? null : argValues.get(argName);
|
||||
public Substring getTagValue(String tagName, String argName) {
|
||||
final Map<Substring, Substring> argValues = myArgTagValues.get(tagName);
|
||||
return argValues != null ? argValues.get(new Substring(argName)) : null;
|
||||
}
|
||||
|
||||
@Nullable
|
||||
public String getTagValue(String[] tagNames, String argName) {
|
||||
public Substring getTagValue(String[] tagNames, String argName) {
|
||||
for (String tagName : tagNames) {
|
||||
Map<String, String> argValues = myArgTagValues.get(tagName);
|
||||
Map<Substring, Substring> argValues = myArgTagValues.get(tagName);
|
||||
if (argValues != null) {
|
||||
return argValues.get(argName);
|
||||
return argValues.get(new Substring(argName));
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
public List<String> getTagArguments(String... tagNames) {
|
||||
public List<Substring> getTagArguments(String... tagNames) {
|
||||
for (String tagName : tagNames) {
|
||||
final Map<String, String> map = myArgTagValues.get(tagName);
|
||||
final Map<Substring, Substring> map = myArgTagValues.get(tagName);
|
||||
if (map != null) {
|
||||
return new ArrayList<String>(map.keySet());
|
||||
return new ArrayList<Substring>(map.keySet());
|
||||
}
|
||||
}
|
||||
return Collections.emptyList();
|
||||
@@ -139,4 +197,13 @@ public abstract class StructuredDocString {
|
||||
@Nullable
|
||||
public abstract String getAttributeDescription();
|
||||
public abstract List<String> getAdditionalTags();
|
||||
|
||||
public abstract List<Substring> getParameterSubstrings();
|
||||
public abstract List<Substring> getKeywordArgumentSubstrings();
|
||||
|
||||
@Nullable
|
||||
public abstract Substring getReturnTypeSubstring();
|
||||
|
||||
@Nullable
|
||||
abstract Substring getParamTypeSubstring(@Nullable String paramName);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,158 @@
|
||||
package com.jetbrains.python.documentation;
|
||||
|
||||
import com.intellij.openapi.util.TextRange;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.regex.Matcher;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
/**
|
||||
* @author vlan
|
||||
*/
|
||||
|
||||
/**
|
||||
* Substring with explicit offsets within its parent string.
|
||||
* <p>
|
||||
* Regular java.lang.String objects share a single char buffer for results of substring(), trim(), etc., but the offset and count
|
||||
* fields of Strings are unfortunately private.
|
||||
*/
|
||||
public class Substring implements CharSequence {
|
||||
private static final Pattern RE_NL = Pattern.compile("(\\r?\\n)");
|
||||
|
||||
private final String myString;
|
||||
private final int myStartOffset;
|
||||
private final int myEndOffset;
|
||||
|
||||
public Substring(String s) {
|
||||
this(s, 0, s.length());
|
||||
}
|
||||
|
||||
public Substring(String s, int start, int end) {
|
||||
myString = s;
|
||||
myStartOffset = start;
|
||||
myEndOffset = end;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(Object o) {
|
||||
if (o instanceof String) {
|
||||
return toString().equals(o);
|
||||
}
|
||||
else if (o instanceof Substring) {
|
||||
return toString().equals(o.toString());
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
return toString().hashCode();
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return getTextRange().substring(myString);
|
||||
}
|
||||
|
||||
public String getSuperString() {
|
||||
return myString;
|
||||
}
|
||||
|
||||
public TextRange getTextRange() {
|
||||
return TextRange.create(myStartOffset, myEndOffset);
|
||||
}
|
||||
|
||||
public List<Substring> split(String regex) {
|
||||
return split(Pattern.compile(regex));
|
||||
}
|
||||
|
||||
public List<Substring> split(Pattern pattern) {
|
||||
final List<Substring> result = new ArrayList<Substring>();
|
||||
final Matcher m = pattern.matcher(myString);
|
||||
int start = myStartOffset;
|
||||
int end = myEndOffset;
|
||||
if (m.find(start)) {
|
||||
do {
|
||||
end = m.start();
|
||||
result.add(createAnotherSubstring(start, Math.min(end, myEndOffset)));
|
||||
start = m.end();
|
||||
} while (end < myEndOffset && m.find());
|
||||
if (start < myEndOffset) {
|
||||
result.add(createAnotherSubstring(start, myEndOffset));
|
||||
}
|
||||
} else {
|
||||
result.add(createAnotherSubstring(start, end));
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
public List<Substring> splitLines() {
|
||||
return split(RE_NL);
|
||||
}
|
||||
|
||||
public Substring trim() {
|
||||
int start;
|
||||
int end;
|
||||
for (start = myStartOffset; start < myEndOffset && myString.charAt(start) <= '\u0020'; start++) {
|
||||
}
|
||||
for (end = myEndOffset - 1; end > start && myString.charAt(end) <= '\u0020'; end--) {
|
||||
}
|
||||
return createAnotherSubstring(start, end + 1);
|
||||
}
|
||||
|
||||
public Substring getMatcherGroup(Matcher m, int group) {
|
||||
return substring(m.start(group), m.end(group));
|
||||
}
|
||||
|
||||
@Override
|
||||
public int length() {
|
||||
return myEndOffset - myStartOffset;
|
||||
}
|
||||
|
||||
@Override
|
||||
public char charAt(int i) {
|
||||
return myString.charAt(myStartOffset + i);
|
||||
}
|
||||
|
||||
@Override
|
||||
public CharSequence subSequence(int start, int end) {
|
||||
return substring(start, end);
|
||||
}
|
||||
|
||||
public boolean startsWith(String prefix) {
|
||||
return indexOf(prefix) == 0;
|
||||
}
|
||||
|
||||
public int indexOf(String s) {
|
||||
int n = myString.indexOf(s, myStartOffset);
|
||||
return n < myEndOffset ? n - myStartOffset : -1;
|
||||
}
|
||||
|
||||
@SuppressWarnings({"MethodNamesDifferingOnlyByCase"})
|
||||
public Substring substring(int start) {
|
||||
return substring(start, length());
|
||||
}
|
||||
|
||||
@SuppressWarnings({"MethodNamesDifferingOnlyByCase"})
|
||||
public Substring substring(int start, int end) {
|
||||
return createAnotherSubstring(myStartOffset + start, myStartOffset + end);
|
||||
}
|
||||
|
||||
public String concatTrimmedLines(String separator) {
|
||||
final StringBuilder b = new StringBuilder();
|
||||
List<Substring> lines = splitLines();
|
||||
final int n = lines.size();
|
||||
for (int i = 0; i < n; i++) {
|
||||
b.append(lines.get(i).trim().toString());
|
||||
if (i < n - 1) {
|
||||
b.append(separator);
|
||||
}
|
||||
}
|
||||
return b.toString();
|
||||
}
|
||||
|
||||
private Substring createAnotherSubstring(int start, int end) {
|
||||
return new Substring(myString, start, end);
|
||||
}
|
||||
}
|
||||
@@ -1 +1 @@
|
||||
<html><body><code>name: str</code><br>The name to convert to a label. This must be a string which could be used as a Python identifier. Strings which do not take this form will result in unpredictable behavior. </body></html>
|
||||
<html><body><code>name: str</code><br>The name to convert to a label. This must be a string which could be used as a Python identifier. Strings which do not take this form will result in unpredictable behavior.</body></html>
|
||||
@@ -2,6 +2,7 @@ package com.jetbrains.python;
|
||||
|
||||
import com.intellij.testFramework.UsefulTestCase;
|
||||
import com.jetbrains.python.documentation.EpydocString;
|
||||
import com.jetbrains.python.documentation.Substring;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
@@ -11,19 +12,25 @@ import java.util.List;
|
||||
public class EpydocStringTest extends UsefulTestCase {
|
||||
public void testTagValue() {
|
||||
EpydocString docString = new EpydocString("@rtype: C{str}");
|
||||
assertEquals("C{str}", docString.getTagValue("rtype"));
|
||||
Substring s = docString.getTagValue("rtype");
|
||||
assertNotNull(s);
|
||||
assertEquals("C{str}", s.toString());
|
||||
}
|
||||
|
||||
public void testTagWithParamValue() {
|
||||
EpydocString docString = new EpydocString("@type m: number");
|
||||
assertEquals("number", docString.getTagValue("type", "m"));
|
||||
final Substring s = docString.getTagValue("type", "m");
|
||||
assertNotNull(s);
|
||||
assertEquals("number", s.toString());
|
||||
}
|
||||
|
||||
public void testMultilineTag() {
|
||||
EpydocString docString = new EpydocString(" @param b: The y intercept of the line. The X{y intercept} of a\n" +
|
||||
" line is the point at which it crosses the y axis (M{x=0}).");
|
||||
final Substring s = docString.getTagValue("param", "b");
|
||||
assertNotNull(s);
|
||||
assertEquals("The y intercept of the line. The X{y intercept} of a line is the point at which it crosses the y axis (M{x=0}).",
|
||||
docString.getTagValue("param", "b"));
|
||||
s.concatTrimmedLines(" "));
|
||||
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user