PY-9795 Use DocStringUpdater API to remove parameters from docstring in DocstringQuickfix and other places

This commit is contained in:
Mikhail Golubev
2015-09-02 14:35:23 +03:00
parent 0eccb1b873
commit e7767c4d6e
14 changed files with 180 additions and 96 deletions
@@ -16,6 +16,7 @@
package com.jetbrains.python.toolbox;
import com.intellij.openapi.util.TextRange;
import com.intellij.openapi.util.text.StringUtil;
import org.jetbrains.annotations.NotNull;
import java.util.ArrayList;
@@ -221,11 +222,31 @@ public class Substring implements CharSequence {
return new Substring(myString, start, end);
}
/**
* If both substrings share the same origin, returns new substring that includes both of them.
*/
@NotNull
public Substring union(@NotNull Substring other) {
if (!myString.equals(other.myString)) {
throw new IllegalArgumentException(String.format("Substrings '%s' and '%s' must belong to the same origin", this, other));
}
final TextRange unionRange = getTextRange().union(other.getTextRange());
return new Substring(getSuperString(), unionRange.getStartOffset(), unionRange.getEndOffset());
}
public int getStartOffset() {
return myStartOffset;
}
public int getStartLine() {
return StringUtil.offsetToLineNumber(myString, myStartOffset);
}
public int getEndOffset() {
return myEndOffset;
}
public int getEndLine() {
return StringUtil.offsetToLineNumber(myString, myEndOffset);
}
}
@@ -58,10 +58,6 @@ public abstract class DocStringLineParser {
return myLines.get(lineNum);
}
public int getLineByOffset(int offset) {
return StringUtil.countNewLines(myDocStringContent.getSuperString().subSequence(0, offset));
}
@Nullable
public Substring getLineOrNull(int lineNum) {
return lineNum >= 0 && lineNum < myLines.size() ? myLines.get(lineNum) : null;
@@ -80,4 +76,29 @@ public abstract class DocStringLineParser {
public Substring getDocStringContent() {
return myDocStringContent;
}
/**
* Finds line number of the <em>line next after the end of indented block</em>, i.e. if returned value equals startLine,
* it means that no block with requested indentation exist. Block can contain intermediate empty lines or lines that consists
* solely of spaces: their indentation is ignored, but it always ends with non-empty line. Non-empty lines in a block must
* have indentation greater than indentThreshold and for them {@link #isBlockEnd(int)} must return false.
*/
public int parseIndentedBlock(int startLine, int indentThreshold) {
int blockEnd = startLine - 1;
int lineNum = startLine;
while (!(lineNum >= getLineCount() || isBlockEnd(lineNum))) {
if (!isEmpty(lineNum)) {
if (getLineIndentSize(lineNum) > indentThreshold) {
blockEnd = lineNum;
}
else {
break;
}
}
lineNum++;
}
return blockEnd + 1;
}
protected abstract boolean isBlockEnd(int lineNum);
}
@@ -29,6 +29,10 @@ import com.intellij.psi.PsiWhiteSpace;
import com.intellij.psi.util.PsiTreeUtil;
import com.intellij.util.ArrayUtil;
import com.jetbrains.python.codeInsight.controlflow.ScopeOwner;
import com.jetbrains.python.documentation.docstrings.DocStringUpdater;
import com.jetbrains.python.documentation.docstrings.GoogleCodeStyleDocStringUpdater;
import com.jetbrains.python.documentation.docstrings.NumpyDocStringUpdater;
import com.jetbrains.python.documentation.docstrings.TagBasedDocStringUpdater;
import com.jetbrains.python.psi.*;
import com.jetbrains.python.psi.impl.PyPsiUtils;
import com.jetbrains.python.psi.impl.PyStringLiteralExpressionImpl;
@@ -262,4 +266,34 @@ public class DocStringUtil {
final PyDocumentationSettings settings = PyDocumentationSettings.getInstance(getModuleForElement(anchor));
return settings.getFormatForFile(anchor.getContainingFile());
}
public static void removeParamFromDocString(@NotNull PyStringLiteralExpression docString, @NotNull String paramName) {
final Module module = getModuleForElement(docString);
final DocStringFormat format = PyDocumentationSettings.getInstance(module).getFormatForFile(docString.getContainingFile());
DocStringUpdater updater;
switch (format) {
case EPYTEXT:
final EpydocString epyParsed = (EpydocString)parseDocString(format, docString);
updater = new TagBasedDocStringUpdater(epyParsed, "@", PyIndentUtil.getElementIndent(docString));
break;
case REST:
final SphinxDocString restParsed = (SphinxDocString)parseDocString(format, docString);
updater = new TagBasedDocStringUpdater(restParsed, ":", PyIndentUtil.getElementIndent(docString));
break;
case GOOGLE:
final GoogleCodeStyleDocString googleParsed = (GoogleCodeStyleDocString)parseDocString(format, docString);
updater = new GoogleCodeStyleDocStringUpdater(googleParsed, PyIndentUtil.getElementIndent(docString));
break;
case NUMPY:
final NumpyDocString numpyParsed = (NumpyDocString)parseDocString(format, docString);
updater = new NumpyDocStringUpdater(numpyParsed, PyIndentUtil.getElementIndent(docString));
break;
default:
return;
}
updater.removeParameter(paramName);
final String newText = updater.getDocStringText();
final PyExpressionStatement replacement = PyElementGenerator.getInstance(docString.getProject()).createDocstring(newText);
docString.replace(replacement.getExpression());
}
}
@@ -104,7 +104,7 @@ public class GoogleCodeStyleDocString extends SectionBasedDocString {
final List<Substring> nestedBlock = pair.getFirst();
if (!nestedBlock.isEmpty()) {
//noinspection ConstantConditions
description = mergeSubstrings(description, ContainerUtil.getLastItem(nestedBlock));
description = description.union(ContainerUtil.getLastItem(nestedBlock));
}
description = description.trim();
return Pair.create(new SectionField(name, type, description), pair.getSecond());
@@ -85,7 +85,7 @@ public class NumpyDocString extends SectionBasedDocString {
final Pair<List<Substring>, Integer> parsedDescription = parseIndentedBlock(lineNum + 1, getLineIndentSize(lineNum), sectionIndent);
final List<Substring> descriptionLines = parsedDescription.getFirst();
if (!descriptionLines.isEmpty()) {
description = mergeSubstrings(descriptionLines.get(0), descriptionLines.get(descriptionLines.size() - 1));
description = descriptionLines.get(0).union(descriptionLines.get(descriptionLines.size() - 1));
}
return Pair.create(new SectionField(name, type, description != null ? description.trim() : null), parsedDescription.getSecond());
}
@@ -96,7 +96,7 @@ public class NumpyDocString extends SectionBasedDocString {
}
@Override
protected boolean isSectionBreak(int lineNum, int curSectionIndent) {
return super.isSectionBreak(lineNum, curSectionIndent) || (isEmpty(lineNum) && isEmptyOrDoesNotExist(lineNum + 1));
protected boolean isBlockEnd(int lineNum) {
return super.isBlockEnd(lineNum) || (isEmpty(lineNum) && isEmptyOrDoesNotExist(lineNum + 1));
}
}
@@ -371,7 +371,7 @@ public class PyDocstringGenerator {
if (updater != null) {
for (DocstringParam param : myParams) {
if (param.isReturnValue()) {
updater.addReturnValue((param.getType()));
updater.addReturnValue(param.getType());
}
else {
updater.addParameter(param.getName(), param.getType());
@@ -120,7 +120,7 @@ public abstract class SectionBasedDocString extends DocStringLineParser implemen
lineNum = skipEmptyLines(lineNum);
}
//noinspection ConstantConditions
mySummary = summary.isEmpty() ? null : mergeSubstrings(summary.get(0), summary.get(summary.size() - 1)).trim();
mySummary = summary.isEmpty() ? null : summary.get(0).union(summary.get(summary.size() - 1)).trim();
}
@NotNull
@@ -197,7 +197,7 @@ public abstract class SectionBasedDocString extends DocStringLineParser implemen
final Substring firstLine = ContainerUtil.getFirstItem(pair.getFirst());
final Substring lastLine = ContainerUtil.getLastItem(pair.getFirst());
if (firstLine != null && lastLine != null) {
return Pair.create(new SectionField(null, null, mergeSubstrings(firstLine, lastLine).trim()), pair.getSecond());
return Pair.create(new SectionField(null, null, firstLine.union(lastLine).trim()), pair.getSecond());
}
return Pair.create(null, pair.getSecond());
}
@@ -218,9 +218,9 @@ public abstract class SectionBasedDocString extends DocStringLineParser implemen
}
protected boolean isSectionBreak(int lineNum, int curSectionIndent) {
return lineNum >= getLineCount() ||
isSectionStart(lineNum) ||
(!isEmpty(lineNum) && getLineIndentSize(lineNum) <= curSectionIndent);
return lineNum >= getLineCount() ||
(!isEmpty(lineNum) && getLineIndentSize(lineNum) <= curSectionIndent) ||
isBlockEnd(lineNum);
}
/**
@@ -231,22 +231,13 @@ public abstract class SectionBasedDocString extends DocStringLineParser implemen
*/
@NotNull
protected Pair<List<Substring>, Integer> parseIndentedBlock(int lineNum, int blockIndent, int sectionIndent) {
final List<Substring> result = new ArrayList<Substring>();
int lastNonEmpty = lineNum - 1;
while (!isSectionBreak(lineNum, sectionIndent)) {
if (getLineIndentSize(lineNum) > blockIndent) {
// copy all lines after the last non empty including the current one
for (int i = lastNonEmpty + 1; i <= lineNum; i++) {
result.add(getLine(i));
}
lastNonEmpty = lineNum;
}
else if (!isEmpty(lineNum)) {
break;
}
lineNum++;
}
return Pair.create(result, lineNum);
final int blockEnd = parseIndentedBlock(lineNum, blockIndent);
return Pair.create(myLines.subList(lineNum, blockEnd), blockEnd);
}
@Override
protected boolean isBlockEnd(int lineNum) {
return isSectionStart(lineNum);
}
/**
@@ -274,22 +265,6 @@ public abstract class SectionBasedDocString extends DocStringLineParser implemen
return line.split(":", 1);
}
/**
* If both substrings share the same origin, returns new substring that includes both of them. Otherwise return {@code null}.
*
* @param s2 substring to concat with
* @return new substring as described
*/
@NotNull
protected static Substring mergeSubstrings(@NotNull Substring s1, @NotNull Substring s2) {
if (!s1.getSuperString().equals(s2.getSuperString())) {
throw new IllegalArgumentException(String.format("Substrings '%s' and '%s' must belong to the same origin", s1, s2));
}
return new Substring(s1.getSuperString(),
Math.min(s1.getStartOffset(), s2.getStartOffset()),
Math.max(s1.getEndOffset(), s2.getEndOffset()));
}
// like Python's textwrap.dedent()
@NotNull
protected static String stripCommonIndent(@NotNull Substring text, boolean ignoreFirstStringIfNonEmpty) {
@@ -217,4 +217,9 @@ public abstract class TagBasedDocString extends DocStringLineParser implements S
results.addAll(getTagArguments(PARAM_TYPE_TAGS));
return results;
}
@Override
protected boolean isBlockEnd(int lineNum) {
return getLine(lineNum).trimLeft().startsWith(myTagPrefix);
}
}
@@ -59,6 +59,21 @@ public abstract class DocStringUpdater<T extends DocStringLineParser> {
insert(line.getEndOffset(), '\n' + text);
}
protected final void remove(int startOffset, int endOffset) {
replace(startOffset, endOffset, "");
}
protected final void removeLines(int startLine, int endLine) {
final List<Substring> lines = myOriginalDocString.getLines();
final int startOffset = lines.get(startLine).getStartOffset();
final int endOffset = endLine < lines.size() - 1 ? lines.get(endLine + 1).getStartOffset() : lines.get(endLine).getEndOffset();
remove(startOffset, endOffset);
}
protected final void removeLine(int line) {
removeLines(line, line);
}
protected final void insertBeforeLine(int lineNumber, @NotNull String text) {
final Substring line = myOriginalDocString.getLines().get(lineNumber);
insert(line.getStartOffset(), text + '\n');
@@ -100,6 +115,10 @@ public abstract class DocStringUpdater<T extends DocStringLineParser> {
return lastLineIndent;
}
protected int getLineIndentSize(int lineNum) {
return PyIndentUtil.getLineIndentSize(getLineIndent(lineNum));
}
protected int findLastNonEmptyLine() {
for (int i = myOriginalDocString.getLineCount() - 1; i >= 0; i--) {
if (!StringUtil.isEmptyOrSpaces(myOriginalDocString.getLine(i))) {
@@ -113,6 +132,8 @@ public abstract class DocStringUpdater<T extends DocStringLineParser> {
public abstract void addReturnValue(@Nullable String type);
public abstract void removeParameter(@NotNull String name);
private static class Modification implements Comparable<Modification> {
@NotNull final TextRange range;
@NotNull final String text;
@@ -116,6 +116,15 @@ public abstract class SectionBasedDocStringUpdater extends DocStringUpdater<Sect
}
}
@Override
public void removeParameter(@NotNull String name) {
for (SectionField param : myOriginalDocString.getParameterFields()) {
if (param.getName().equals(name)) {
removeLines(getFieldStartLine(param), getFieldEndLine(param));
}
}
}
abstract void updateParamDeclarationWithType(@NotNull Substring nameSubstring, @NotNull String type);
protected int getSectionLastTitleLine(@NotNull Section paramSection) {
@@ -195,12 +204,25 @@ public abstract class SectionBasedDocStringUpdater extends DocStringUpdater<Sect
}
protected int getSectionStartLine(@NotNull Section section) {
return myOriginalDocString.getLineByOffset(section.getTitleAsSubstring().getStartOffset());
return section.getTitleAsSubstring().getStartLine();
}
protected int getFieldStartLine(@NotNull SectionField field) {
final Substring anyFieldSub = ObjectUtils.chooseNotNull(field.getNameAsSubstring(), field.getTypeAsSubstring());
//noinspection ConstantConditions
return myOriginalDocString.getLineByOffset(anyFieldSub.getStartOffset());
return anyFieldSub.getStartLine();
}
protected int getFieldEndLine(@NotNull SectionField field) {
if (field.getDescriptionAsSubstring() != null) {
return field.getDescriptionAsSubstring().getEndLine();
}
else if (field.getTypeAsSubstring() != null) {
return field.getTypeAsSubstring().getEndLine();
}
else {
//noinspection ConstantConditions
return field.getNameAsSubstring().getEndLine();
}
}
}
@@ -20,14 +20,16 @@ import com.jetbrains.python.toolbox.Substring;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.util.List;
/**
* @author Mikhail Golubev
*/
public class TagBasedDocStringUpdater<T extends TagBasedDocString> extends DocStringUpdater<T>{
public class TagBasedDocStringUpdater extends DocStringUpdater<TagBasedDocString>{
private final String myTagPrefix;
public TagBasedDocStringUpdater(@NotNull T docString, @NotNull String tagPrefix, @NotNull String minContentIndent) {
public TagBasedDocStringUpdater(@NotNull TagBasedDocString docString, @NotNull String tagPrefix, @NotNull String minContentIndent) {
super(docString, minContentIndent);
myTagPrefix = tagPrefix;
}
@@ -57,6 +59,23 @@ public class TagBasedDocStringUpdater<T extends TagBasedDocString> extends DocSt
}
}
@Override
public void removeParameter(@NotNull String name) {
final List<Substring> nameSubs = myOriginalDocString.getParameterSubstrings();
for (Substring sub : nameSubs) {
if (sub.toString().equals(name)) {
final int startLine = sub.getStartLine();
final int nextAfterBlock = myOriginalDocString.parseIndentedBlock(startLine + 1, getLineIndentSize(startLine));
if (nextAfterBlock != startLine + 1) {
removeLines(startLine, nextAfterBlock - 1);
}
else {
removeLine(startLine);
}
}
}
}
private void insertTagLine(@NotNull DocStringBuilder lineBuilder) {
final int firstLineWithTag = findFirstLineWithTag();
if (firstLineWithTag >= 0) {
@@ -20,19 +20,18 @@ import com.intellij.codeInspection.ProblemDescriptor;
import com.intellij.openapi.editor.Document;
import com.intellij.openapi.editor.Editor;
import com.intellij.openapi.editor.EditorFactory;
import com.intellij.openapi.module.Module;
import com.intellij.openapi.module.ModuleUtilCore;
import com.intellij.openapi.project.Project;
import com.intellij.openapi.util.text.StringUtil;
import com.intellij.psi.PsiDocumentManager;
import com.intellij.psi.PsiElement;
import com.intellij.psi.util.PsiTreeUtil;
import com.jetbrains.python.PyBundle;
import com.jetbrains.python.codeInsight.intentions.PyGenerateDocstringIntention;
import com.jetbrains.python.documentation.DocStringUtil;
import com.jetbrains.python.documentation.PyDocstringGenerator;
import com.jetbrains.python.documentation.PyDocumentationSettings;
import com.jetbrains.python.editor.PythonDocCommentUtil;
import com.jetbrains.python.psi.*;
import com.jetbrains.python.psi.PyClass;
import com.jetbrains.python.psi.PyDocStringOwner;
import com.jetbrains.python.psi.PyFunction;
import com.jetbrains.python.psi.PyStringLiteralExpression;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
@@ -90,27 +89,11 @@ public class DocstringQuickFix implements LocalQuickFix {
return;
}
if (docStringExpression != null) {
PyElementGenerator elementGenerator = PyElementGenerator.getInstance(project);
final Module module = ModuleUtilCore.findModuleForPsiElement(docStringExpression);
if (module == null) return;
PyDocumentationSettings documentationSettings = PyDocumentationSettings.getInstance(module);
if (documentationSettings.isEpydocFormat(docStringExpression.getContainingFile())) {
myPrefix = "@";
}
else {
myPrefix = ":";
}
String replacement = docStringExpression.getText();
if (myMissingText != null) {
replacement = new PyDocstringGenerator(docStringOwner).withParam(myMissingText).buildDocString();
new PyDocstringGenerator(docStringOwner).withParam(myMissingText).buildAndInsert();
}
if (myUnexpected != null) {
replacement = PythonDocCommentUtil.removeParamFromDocstring(replacement, myPrefix, myUnexpected);
}
if (!replacement.equals(docStringExpression.getText()) && !StringUtil.isEmptyOrSpaces(replacement)) {
PyExpression str = elementGenerator.createDocstring(replacement).getExpression();
docStringExpression.replace(str);
else if (myUnexpected != null) {
DocStringUtil.removeParamFromDocString(docStringExpression, myUnexpected);
}
}
}
@@ -17,15 +17,12 @@ package com.jetbrains.python.inspections.quickfix;
import com.intellij.codeInspection.LocalQuickFix;
import com.intellij.codeInspection.ProblemDescriptor;
import com.intellij.openapi.module.Module;
import com.intellij.openapi.module.ModuleUtilCore;
import com.intellij.openapi.project.Project;
import com.intellij.psi.PsiElement;
import com.intellij.psi.util.PsiTreeUtil;
import com.intellij.usageView.UsageInfo;
import com.jetbrains.python.PyBundle;
import com.jetbrains.python.documentation.PyDocumentationSettings;
import com.jetbrains.python.editor.PythonDocCommentUtil;
import com.jetbrains.python.documentation.DocStringUtil;
import com.jetbrains.python.psi.*;
import com.jetbrains.python.psi.resolve.PyResolveContext;
import com.jetbrains.python.refactoring.PyRefactoringUtil;
@@ -76,14 +73,7 @@ public class PyRemoveParameterQuickFix implements LocalQuickFix {
final PyStringLiteralExpression expression = pyFunction.getDocStringExpression();
final String paramName = ((PyParameter)element).getName();
if (expression != null && paramName != null) {
final Module module = ModuleUtilCore.findModuleForPsiElement(pyFunction);
assert module != null;
PyDocumentationSettings documentationSettings = PyDocumentationSettings.getInstance(module);
String prefix = documentationSettings.isEpydocFormat(pyFunction.getContainingFile()) ? "@" : ":";
final String replacement = PythonDocCommentUtil.removeParamFromDocstring(expression.getText(), prefix, paramName);
PyExpression str =
PyElementGenerator.getInstance(project).createDocstring(replacement).getExpression();
expression.replace(str);
DocStringUtil.removeParamFromDocString(expression, paramName);
}
}
@@ -33,9 +33,8 @@ import com.intellij.util.containers.HashSet;
import com.intellij.util.containers.MultiMap;
import com.jetbrains.python.PyNames;
import com.jetbrains.python.PythonLanguage;
import com.jetbrains.python.documentation.DocStringUtil;
import com.jetbrains.python.documentation.PyDocstringGenerator;
import com.jetbrains.python.documentation.PyDocumentationSettings;
import com.jetbrains.python.editor.PythonDocCommentUtil;
import com.jetbrains.python.psi.*;
import com.jetbrains.python.psi.search.PyOverridingMethodsSearch;
import com.jetbrains.python.refactoring.PyRefactoringUtil;
@@ -340,13 +339,7 @@ public class PyChangeSignatureUsageProcessor implements ChangeSignatureUsageProc
for (PyParameter p : function.getParameterList().getParameters()) {
final String paramName = p.getName();
if (!names.contains(paramName) && paramName != null) {
PyDocumentationSettings documentationSettings = PyDocumentationSettings.getInstance(module);
String prefix = documentationSettings.isEpydocFormat(docStringExpression.getContainingFile()) ? "@" : ":";
final String replacement = PythonDocCommentUtil.removeParamFromDocstring(docStringExpression.getText(), prefix,
paramName);
PyExpression str =
PyElementGenerator.getInstance(function.getProject()).createDocstring(replacement).getExpression();
docStringExpression.replace(str);
DocStringUtil.removeParamFromDocString(docStringExpression, paramName);
}
}
}