PY-16991 Don't mix up section headers and parameter references in Google docstrings
authorMikhail Golubev <mikhail.golubev@jetbrains.com>
Tue, 22 Sep 2015 13:44:02 +0000 (16:44 +0300)
committerMikhail Golubev <mikhail.golubev@jetbrains.com>
Tue, 22 Sep 2015 15:44:10 +0000 (18:44 +0300)
To avoid completion of headers instead of parameters we require at
least one space indentation inside sections in Google code style
docstrings. Thus parameters references are not inserted falsely and
we still can use the rule "if there is reference under caret don't
suggest section headers".

For Numpy we have nothing to do than to let headers be suggested
under sections (where parameters normally go), since it's not really
obvious how to distinguish incomplete parameter references from
incomplete headers there (thanks to its zero section indentation).

python/src/com/jetbrains/python/documentation/docstrings/DocStringSectionHeaderCompletionContributor.java
python/src/com/jetbrains/python/documentation/docstrings/NumpyDocString.java
python/src/com/jetbrains/python/documentation/docstrings/SectionBasedDocString.java
python/testData/completion/secondSectionNameInGoogleDocstring.py [new file with mode: 0644]
python/testData/docstrings/googleMandatoryIndentationInsideSection.py [new file with mode: 0644]
python/testSrc/com/jetbrains/python/PySectionBasedDocStringTest.java
python/testSrc/com/jetbrains/python/PythonCompletionTest.java

index 86405b29158b4540fe204883404e501085af8689..8f302299e77232800870c2a86da5de2cc6089668 100644 (file)
@@ -42,13 +42,15 @@ public class DocStringSectionHeaderCompletionContributor extends CompletionContr
                final PsiElement stringNode = parameters.getOriginalPosition();
                assert stringNode != null;
                final int offset = parameters.getOffset();
-               if (file.findReferenceAt(offset) != null) {
-                 return;
-               }
                final DocStringFormat format = DocStringUtil.getConfiguredDocStringFormat(file);
                if (!(format == DocStringFormat.GOOGLE || format == DocStringFormat.NUMPY)) {
                  return;
                }
+               // Numpy docstring format is ambiguous. Because parameters have the same indentation as section headers,
+               // beginning of section header can be parsed as parameter reference
+               if (format == DocStringFormat.GOOGLE && file.findReferenceAt(offset) != null) {
+                 return;
+               }
                final Document document = parameters.getEditor().getDocument();
                final TextRange linePrefixRange = new TextRange(document.getLineStartOffset(document.getLineNumber(offset)), offset);
                final String prefix = StringUtil.trimLeading(document.getText(linePrefixRange));
index 12ee80b2f65b32a992c5d10c5d654d7760266312..2402f1e45e5472373ee9c257037de8389db15e7e 100644 (file)
@@ -63,6 +63,12 @@ public class NumpyDocString extends SectionBasedDocString {
     return Pair.create(null, lineNum);
   }
 
+  @Override
+  protected int getSectionIndentationThreshold(int sectionIndent) {
+    // For Numpy we want to let section content has the same indent as section header
+    return sectionIndent - 1;
+  }
+
   @Override
   protected Pair<SectionField, Integer> parseSectionField(int lineNum,
                                                           int sectionIndent,
index f022499f163b1b748c5d8a4c4b79b56ce9107301..7e6e8efd171d0e914f940d281488c19cf7c99999 100644 (file)
@@ -204,8 +204,7 @@ public abstract class SectionBasedDocString extends DocStringLineParser implemen
 
   @NotNull
   protected Pair<SectionField, Integer> parseGenericField(int lineNum, int sectionIndent) {
-    // We want to let section content has the same indent as section header, in particular for Numpy
-    final Pair<List<Substring>, Integer> pair = parseIndentedBlock(lineNum, Math.max(sectionIndent - 1, 0));
+    final Pair<List<Substring>, Integer> pair = parseIndentedBlock(lineNum, getSectionIndentationThreshold(sectionIndent));
     final Substring firstLine = ContainerUtil.getFirstItem(pair.getFirst());
     final Substring lastLine = ContainerUtil.getLastItem(pair.getFirst());
     if (firstLine != null && lastLine != null) {
@@ -225,7 +224,7 @@ public abstract class SectionBasedDocString extends DocStringLineParser implemen
   protected boolean isSectionBreak(int lineNum, int curSectionIndent) {
     return lineNum >= getLineCount() || 
            // note that field may have the same indent as its containing section
-           (!isEmpty(lineNum) && getLineIndentSize(lineNum) < curSectionIndent) || 
+           (!isEmpty(lineNum) && getLineIndentSize(lineNum) <= getSectionIndentationThreshold(curSectionIndent)) || 
            isSectionStart(lineNum);
   }
 
@@ -241,6 +240,17 @@ public abstract class SectionBasedDocString extends DocStringLineParser implemen
     return Pair.create(myLines.subList(lineNum, blockEnd), blockEnd);
   }
 
+  /**
+   * Inside section any indentation that is equal or smaller to returned one signals about section break.
+   * It's safe to return negative value, because it's used only for comparisons.
+   *
+   * @see #isSectionBreak(int, int)
+   * @see #parseGenericField(int, int)
+   */
+  protected int getSectionIndentationThreshold(int sectionIndent) {
+    return sectionIndent;
+  }
+
   @Override
   protected boolean isBlockEnd(int lineNum) {
     return isSectionStart(lineNum);
diff --git a/python/testData/completion/secondSectionNameInGoogleDocstring.py b/python/testData/completion/secondSectionNameInGoogleDocstring.py
new file mode 100644 (file)
index 0000000..16e3b39
--- /dev/null
@@ -0,0 +1,7 @@
+def f(x):
+    """
+    Args:
+      x:
+
+    Ret<caret>:
+    """
\ No newline at end of file
diff --git a/python/testData/docstrings/googleMandatoryIndentationInsideSection.py b/python/testData/docstrings/googleMandatoryIndentationInsideSection.py
new file mode 100644 (file)
index 0000000..8d49ac0
--- /dev/null
@@ -0,0 +1,13 @@
+def f(x, y):
+    """
+    Args:
+    x (int): foo
+
+    Args:
+     y (int): bar
+
+    Examples:
+        first line
+         second line
+    third line
+    """
\ No newline at end of file
index 97df4fca34d26c449f853bab6b40785e891d86f2..9488be14829e04ad2d83cb7358b963c794d5c06d 100644 (file)
@@ -363,6 +363,21 @@ public class PySectionBasedDocStringTest extends PyTestCase {
     assertEquals(firstField.getDescription(), "description");
   }
 
+  // PY-16991
+  public void testGoogleMandatoryIndentationInsideSection() {
+    final GoogleCodeStyleDocString docString = findAndParseGoogleStyleDocString();
+    assertSize(3, docString.getSections());
+    assertEmpty(docString.getSections().get(0).getFields());
+    assertSize(1, docString.getSections().get(1).getFields());
+    final Section thirdSection = docString.getSections().get(2);
+    assertSize(1, thirdSection.getFields());
+    final SectionField firstExample = thirdSection.getFields().get(0);
+    assertEmpty(firstExample.getName());
+    assertEmpty(firstExample.getType());
+    assertEquals("first line\n" +
+                 "second line", firstExample.getDescription());
+  }
+
   @Override
   protected String getTestDataPath() {
     return super.getTestDataPath() + "/docstrings";
index f0fade3aebab789940951e1265a3629abf14fe20..ad48594360a60489e42b8e88c32d5efc411d8c71 100644 (file)
@@ -416,6 +416,18 @@ public class PythonCompletionTest extends PyTestCase {
     });
   }
 
+  // PY-16991
+  public void testSecondSectionNameInGoogleDocstring() {
+    runWithDocStringFormat(DocStringFormat.GOOGLE, new Runnable() {
+      @Override
+      public void run() {
+        final List<String> variants = doTestByFile();
+        assertNotNull(variants);
+        assertContainsElements(variants, "Return", "Returns");
+      }
+    });
+  }
+
   // PY-16877
   public void testTwoWordsSectionNameInGoogleDocstring() throws Exception {
     runWithDocStringFormat(DocStringFormat.GOOGLE, new Runnable() {