Mostly finished updaters of Google Code Style and Numpy docstrings
authorMikhail Golubev <mikhail.golubev@jetbrains.com>
Mon, 24 Aug 2015 22:51:42 +0000 (01:51 +0300)
committerMikhail Golubev <mikhail.golubev@jetbrains.com>
Wed, 2 Sep 2015 11:35:14 +0000 (14:35 +0300)
This is probably the most tedious part of the whole process.

12 files changed:
python/psi-api/src/com/jetbrains/python/psi/StructuredDocString.java
python/psi-api/src/com/jetbrains/python/toolbox/Substring.java
python/src/com/jetbrains/python/documentation/GoogleCodeStyleDocString.java
python/src/com/jetbrains/python/documentation/NumpyDocString.java
python/src/com/jetbrains/python/documentation/SectionBasedDocString.java
python/src/com/jetbrains/python/documentation/docstrings/DocStringUpdater.java
python/src/com/jetbrains/python/documentation/docstrings/GoogleCodeStyleDocStringUpdater.java [new file with mode: 0644]
python/src/com/jetbrains/python/documentation/docstrings/NumpyDocStringUpdater.java [new file with mode: 0644]
python/src/com/jetbrains/python/documentation/docstrings/SectionBasedDocStringBuilder.java
python/src/com/jetbrains/python/documentation/docstrings/SectionBasedDocStringUpdater.java [new file with mode: 0644]
python/src/com/jetbrains/python/documentation/docstrings/TagBasedDocStringUpdater.java
python/testSrc/com/jetbrains/python/PySectionBasedDocStringTest.java

index 02c6bbe27d9cd5fbb4238e356a275f6d0bffae2f..9d27c478396b277ff7b78dc9e9891acd52a6659e 100644 (file)
@@ -91,4 +91,7 @@ public interface StructuredDocString {
 
   @Nullable
   Substring getParamByNameAndKind(@NotNull String name, String kind);
+
+  @Nullable
+  Substring getParamNameSubstring();
 }
index 336a54a919e2f540e38097eb995886844c8f059c..4b6c09fba01c2b51ec05468b17ba49107f681f45 100644 (file)
@@ -159,6 +159,10 @@ public class Substring implements CharSequence {
     return myEndOffset - myStartOffset;
   }
 
+  public boolean isEmpty() {
+    return length() <= 0;
+  }
+
   @Override
   public char charAt(int i) {
     return myString.charAt(myStartOffset + i);
index b4c1ab518d18f7163cde266702c1e6f0d3dbcbdd..e168c8c01801cdc937d91531dd7552e9498fce8f 100644 (file)
@@ -113,11 +113,12 @@ public class GoogleCodeStyleDocString extends SectionBasedDocString {
 
   @NotNull
   @Override
-  protected Pair<String, Integer> parseSectionHeader(int lineNum) {
-    final Matcher matcher = SECTION_HEADER_RE.matcher(getLine(lineNum));
+  protected Pair<Substring, Integer> parseSectionHeader(int lineNum) {
+    final Substring line = getLine(lineNum);
+    final Matcher matcher = SECTION_HEADER_RE.matcher(line);
     if (matcher.matches()) {
-      @NonNls final String title = matcher.group(1).trim();
-      if (SECTION_NAMES.contains(title.toLowerCase())) {
+      @NonNls final Substring title = line.getMatcherGroup(matcher, 1).trim();
+      if (SECTION_NAMES.contains(title.toString().toLowerCase())) {
         return Pair.create(title, lineNum + 1);
       }
     }
index 7c9237dfb8f858b614807b32ec04a941266b57cc..590ff46f0548c229691f44184ad0a6a4ae66855f 100644 (file)
@@ -50,12 +50,12 @@ public class NumpyDocString extends SectionBasedDocString {
 
   @NotNull
   @Override
-  protected Pair<String, Integer> parseSectionHeader(int lineNum) {
+  protected Pair<Substring, Integer> parseSectionHeader(int lineNum) {
     @NonNls final String title = getLine(lineNum).trim().toString();
     if (SECTION_NAMES.contains(title.toLowerCase())) {
       final Substring nextLine = getLineOrNull(lineNum + 1);
       if (nextLine != null && SECTION_HEADER.matcher(nextLine).matches()) {
-        return Pair.create(getLine(lineNum).trim().toString(), lineNum + 2);
+        return Pair.create(getLine(lineNum).trim(), lineNum + 2);
       }
     }
     return Pair.create(null, lineNum);
index 909469f426cf5efa8ed0d6424907bf1fe46e3446..d12b040ab692e0dbd38ea0040eadc7833f8fa443 100644 (file)
@@ -15,7 +15,6 @@
  */
 package com.jetbrains.python.documentation;
 
-import com.google.common.annotations.VisibleForTesting;
 import com.google.common.collect.ImmutableMap;
 import com.google.common.collect.ImmutableSet;
 import com.intellij.openapi.util.Condition;
@@ -44,14 +43,14 @@ public abstract class SectionBasedDocString extends DocStringLineParser implemen
   /**
    * Frequently used section types
    */
-  @NonNls protected static final String RETURNS_SECTION = "returns";
-  @NonNls protected static final String RAISES_SECTION = "raises";
-  @NonNls protected static final String KEYWORD_ARGUMENTS_SECTION = "keyword arguments";
-  @NonNls protected static final String PARAMETERS_SECTION = "parameters";
-  @NonNls protected static final String ATTRIBUTES_SECTION = "attributes";
-  @NonNls protected static final String METHODS_SECTION = "methods";
-  @NonNls protected static final String OTHER_PARAMETERS_SECTION = "other parameters";
-  @NonNls protected static final String YIELDS_SECTION = "yields";
+  @NonNls public static final String RETURNS_SECTION = "returns";
+  @NonNls public static final String RAISES_SECTION = "raises";
+  @NonNls public static final String KEYWORD_ARGUMENTS_SECTION = "keyword arguments";
+  @NonNls public static final String PARAMETERS_SECTION = "parameters";
+  @NonNls public static final String ATTRIBUTES_SECTION = "attributes";
+  @NonNls public static final String METHODS_SECTION = "methods";
+  @NonNls public static final String OTHER_PARAMETERS_SECTION = "other parameters";
+  @NonNls public static final String YIELDS_SECTION = "yields";
 
   protected static final Map<String, String> SECTION_ALIASES =
     ImmutableMap.<String, String>builder()
@@ -90,8 +89,8 @@ public abstract class SectionBasedDocString extends DocStringLineParser implemen
   private static final ImmutableSet<String> SECTIONS_WITH_NAME = ImmutableSet.of(METHODS_SECTION);
 
   @Nullable
-  protected static String normalizeSectionTitle(@Nullable @NonNls String title) {
-    return title == null ? null : SECTION_ALIASES.get(title.toLowerCase());
+  protected static String normalizeSectionTitle(@NotNull @NonNls String title) {
+    return SECTION_ALIASES.get(title.toLowerCase());
   }
 
   private final Substring mySummary;
@@ -145,9 +144,12 @@ public abstract class SectionBasedDocString extends DocStringLineParser implemen
 
   @NotNull
   protected Pair<Section, Integer> parseSection(int sectionStartLine) {
-    final Pair<String, Integer> pair = parseSectionHeader(sectionStartLine);
-    final String title = normalizeSectionTitle(pair.getFirst());
-    if (title == null) {
+    final Pair<Substring, Integer> pair = parseSectionHeader(sectionStartLine);
+    if (pair.getFirst() == null) {
+      return Pair.create(null, sectionStartLine);
+    }
+    final String normalized = normalizeSectionTitle(pair.getFirst().toString());
+    if (normalized == null) {
       return Pair.create(null, sectionStartLine);
     }
     int lineNum = skipEmptyLines(pair.getSecond());
@@ -155,7 +157,7 @@ public abstract class SectionBasedDocString extends DocStringLineParser implemen
     final int sectionIndent = getLineIndentSize(sectionStartLine);
     while (!isSectionBreak(lineNum, sectionIndent)) {
       if (!isEmpty(lineNum)) {
-        final Pair<SectionField, Integer> result = parseSectionField(lineNum, title, sectionIndent);
+        final Pair<SectionField, Integer> result = parseSectionField(lineNum, normalized, sectionIndent);
         if (result.getFirst() != null) {
           fields.add(result.getFirst());
           lineNum = result.getSecond();
@@ -164,7 +166,7 @@ public abstract class SectionBasedDocString extends DocStringLineParser implemen
       }
       lineNum++;
     }
-    return Pair.create(new Section(title, fields), lineNum);
+    return Pair.create(new Section(pair.getFirst(), fields), lineNum);
   }
 
   @NotNull
@@ -201,7 +203,7 @@ public abstract class SectionBasedDocString extends DocStringLineParser implemen
   }
 
   @NotNull
-  protected abstract Pair<String, Integer> parseSectionHeader(int lineNum);
+  protected abstract Pair<Substring, Integer> parseSectionHeader(int lineNum);
 
   protected int skipEmptyLines(int lineNum) {
     while (lineNum < getLineCount() && isEmpty(lineNum)) {
@@ -211,7 +213,7 @@ public abstract class SectionBasedDocString extends DocStringLineParser implemen
   }
 
   protected boolean isSectionStart(int lineNum) {
-    final Pair<String, Integer> pair = parseSectionHeader(lineNum);
+    final Pair<Substring, Integer> pair = parseSectionHeader(lineNum);
     return pair.getFirst() != null;
   }
 
@@ -315,7 +317,7 @@ public abstract class SectionBasedDocString extends DocStringLineParser implemen
     return StringUtil.join(skipFirstLine ? ContainerUtil.prepend(dedentedLines, firstLine) : dedentedLines, "\n");
   }
 
-  @VisibleForTesting
+  @NotNull
   public List<Section> getSections() {
     return Collections.unmodifiableList(mySections);
   }
@@ -396,7 +398,7 @@ public abstract class SectionBasedDocString extends DocStringLineParser implemen
   private List<SectionField> getParameterFields() {
     final List<SectionField> result = new ArrayList<SectionField>();
     for (Section section : mySections) {
-      if (section.getTitle().equals(PARAMETERS_SECTION)) {
+      if (section.getNormalizedTitle().equals(PARAMETERS_SECTION)) {
         for (SectionField field : section.getFields()) {
           result.add(field);
         }
@@ -441,7 +443,7 @@ public abstract class SectionBasedDocString extends DocStringLineParser implemen
   private List<SectionField> getKeywordArgumentFields() {
     final List<SectionField> result = new ArrayList<SectionField>();
     for (Section section : mySections) {
-      if (section.getTitle().equals(KEYWORD_ARGUMENTS_SECTION)) {
+      if (section.getNormalizedTitle().equals(KEYWORD_ARGUMENTS_SECTION)) {
         for (SectionField field : section.getFields()) {
           result.add(field);
         }
@@ -484,7 +486,7 @@ public abstract class SectionBasedDocString extends DocStringLineParser implemen
   @Nullable
   private SectionField getFirstReturnField() {
     for (Section section : mySections) {
-      if (section.getTitle().equals(RETURNS_SECTION) && !section.getFields().isEmpty()) {
+      if (section.getNormalizedTitle().equals(RETURNS_SECTION) && !section.getFields().isEmpty()) {
         return section.getFields().get(0);
       }
     }
@@ -517,7 +519,7 @@ public abstract class SectionBasedDocString extends DocStringLineParser implemen
   private List<SectionField> getExceptionFields() {
     final List<SectionField> result = new ArrayList<SectionField>();
     for (Section section : mySections) {
-      if (section.getTitle().equals(RAISES_SECTION)) {
+      if (section.getNormalizedTitle().equals(RAISES_SECTION)) {
         result.addAll(section.getFields());
       }
     }
@@ -547,19 +549,30 @@ public abstract class SectionBasedDocString extends DocStringLineParser implemen
   }
 
   public static class Section {
-    private final String myTitle;
+    private final Substring myTitle;
     private final List<SectionField> myFields;
 
-    public Section(@NotNull String title, @NotNull List<SectionField> fields) {
+    public Section(@NotNull Substring title, @NotNull List<SectionField> fields) {
       myTitle = title;
       myFields = new ArrayList<SectionField>(fields);
     }
 
     @NotNull
-    public String getTitle() {
+    public Substring getTitleAsSubstring() {
       return myTitle;
     }
 
+    @NotNull
+    public String getTitle() {
+      return myTitle.toString();
+    }
+    
+    @NotNull
+    public String getNormalizedTitle() {
+      //noinspection ConstantConditions
+      return normalizeSectionTitle(getTitle());
+    }
+
     @NotNull
     public List<SectionField> getFields() {
       return Collections.unmodifiableList(myFields);
index 83868dd53734c543ab35f910a23b94f98d2a8530..5724882d013d03a3aad82ba44d4ff4211b4f8cb1 100644 (file)
@@ -16,6 +16,7 @@
 package com.jetbrains.python.documentation.docstrings;
 
 import com.intellij.openapi.util.TextRange;
+import com.intellij.openapi.util.text.StringUtil;
 import com.jetbrains.python.documentation.DocStringLineParser;
 import com.jetbrains.python.psi.PyIndentUtil;
 import com.jetbrains.python.toolbox.Substring;
@@ -32,7 +33,7 @@ public abstract class DocStringUpdater<T extends DocStringLineParser> {
   protected final T myOriginalDocString;
   private final StringBuilder myBuilder;
   private final List<Modification> myUpdates = new ArrayList<Modification>();
-  private final String myMinContentIndent;
+  protected final String myMinContentIndent;
 
   public DocStringUpdater(@NotNull T docString, @NotNull String minContentIndent) {
     myBuilder = new StringBuilder(docString.getDocStringContent().getSuperString());
@@ -93,12 +94,21 @@ public abstract class DocStringUpdater<T extends DocStringLineParser> {
   }
 
   @NotNull
-  protected String getLineIndent(int lastNonEmptyLine) {
-    final String indent = myOriginalDocString.getLineIndent(lastNonEmptyLine);
-    if (PyIndentUtil.getLineIndentSize(indent) < PyIndentUtil.getLineIndentSize(myMinContentIndent)) {
+  protected String getLineIndent(int lineNum) {
+    final String lastLineIndent = myOriginalDocString.getLineIndent(lineNum);
+    if (PyIndentUtil.getLineIndentSize(lastLineIndent) < PyIndentUtil.getLineIndentSize(myMinContentIndent)) {
       return myMinContentIndent;
     }
-    return indent;
+    return lastLineIndent;
+  }
+
+  protected int findLastNonEmptyLine() {
+    for (int i = myOriginalDocString.getLineCount() - 1; i >= 0; i--) {
+      if (StringUtil.isEmptyOrSpaces(myOriginalDocString.getLine(i))) {
+        return i;
+      }
+    }
+    return 0;
   }
 
   private static class Modification implements Comparable<Modification> {
diff --git a/python/src/com/jetbrains/python/documentation/docstrings/GoogleCodeStyleDocStringUpdater.java b/python/src/com/jetbrains/python/documentation/docstrings/GoogleCodeStyleDocStringUpdater.java
new file mode 100644 (file)
index 0000000..e1826d9
--- /dev/null
@@ -0,0 +1,39 @@
+/*
+ * Copyright 2000-2015 JetBrains s.r.o.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.jetbrains.python.documentation.docstrings;
+
+import com.jetbrains.python.documentation.SectionBasedDocString;
+import com.jetbrains.python.toolbox.Substring;
+import org.jetbrains.annotations.NotNull;
+
+/**
+ * @author Mikhail Golubev
+ */
+public class GoogleCodeStyleDocStringUpdater extends SectionBasedDocStringUpdater {
+  public GoogleCodeStyleDocStringUpdater(@NotNull SectionBasedDocString docString, @NotNull String minContentIndent) {
+    super(docString, minContentIndent);
+  }
+
+  @Override
+  void updateParamDeclarationWithType(@NotNull Substring nameSubstring, @NotNull String type) {
+    insert(nameSubstring.getEndOffset(), "(" + type + ")");
+  }
+
+  @Override
+  protected SectionBasedDocStringBuilder createBuilder() {
+    return new GoogleCodeStyleDocStringBuilder();
+  }
+}
diff --git a/python/src/com/jetbrains/python/documentation/docstrings/NumpyDocStringUpdater.java b/python/src/com/jetbrains/python/documentation/docstrings/NumpyDocStringUpdater.java
new file mode 100644 (file)
index 0000000..065ba0c
--- /dev/null
@@ -0,0 +1,46 @@
+/*
+ * Copyright 2000-2015 JetBrains s.r.o.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.jetbrains.python.documentation.docstrings;
+
+import com.jetbrains.python.documentation.SectionBasedDocString;
+import com.jetbrains.python.documentation.SectionBasedDocString.Section;
+import com.jetbrains.python.toolbox.Substring;
+import org.jetbrains.annotations.NotNull;
+
+/**
+ * @author Mikhail Golubev
+ */
+public class NumpyDocStringUpdater extends SectionBasedDocStringUpdater {
+  public NumpyDocStringUpdater(@NotNull SectionBasedDocString docString,
+                               @NotNull String minContentIndent) {
+    super(docString, minContentIndent);
+  }
+
+  @Override
+  void updateParamDeclarationWithType(@NotNull Substring nameSubstring, @NotNull String type) {
+    insert(nameSubstring.getEndOffset(), " : " + type);
+  }
+
+  @Override
+  protected int getSectionLastTitleLine(@NotNull Section section) {
+    return getSectionStartLine(section) + 1;
+  }
+
+  @Override
+  protected SectionBasedDocStringBuilder createBuilder() {
+    return new NumpyDocStringBuilder();
+  }
+}
index a43860e76cd069f6030b4f76fae34a12a7781872..1d2812a445ead8ddbb05f25c9dd48d202de4c39e 100644 (file)
@@ -26,7 +26,7 @@ public abstract class SectionBasedDocStringBuilder extends DocStringBuilder {
   protected static final String DEFAULT_SECTION_INDENT = StringUtil.repeatSymbol(' ', 4);
   protected static final String DEFAULT_CONTINUATION_INDENT = StringUtil.repeatSymbol(' ', 4);
 
-  protected final String mySectionIndent = DEFAULT_SECTION_INDENT;
+  protected String mySectionIndent = DEFAULT_SECTION_INDENT;
   protected final String myContinuationIndent = DEFAULT_CONTINUATION_INDENT;
 
   private String myCurSectionTitle = null;
@@ -67,4 +67,10 @@ public abstract class SectionBasedDocStringBuilder extends DocStringBuilder {
   protected SectionBasedDocStringBuilder addSectionLine(@NotNull String line) {
     return (SectionBasedDocStringBuilder)addLine(mySectionIndent + line);
   }
+
+  @NotNull
+  protected SectionBasedDocStringBuilder withSectionIndent(@NotNull String indent) {
+    mySectionIndent = indent;
+    return this;
+  }
 }
diff --git a/python/src/com/jetbrains/python/documentation/docstrings/SectionBasedDocStringUpdater.java b/python/src/com/jetbrains/python/documentation/docstrings/SectionBasedDocStringUpdater.java
new file mode 100644 (file)
index 0000000..d4f7f55
--- /dev/null
@@ -0,0 +1,198 @@
+/*
+ * Copyright 2000-2015 JetBrains s.r.o.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.jetbrains.python.documentation.docstrings;
+
+import com.intellij.openapi.util.Condition;
+import com.intellij.openapi.util.text.StringUtil;
+import com.intellij.util.ObjectUtils;
+import com.intellij.util.containers.ContainerUtil;
+import com.jetbrains.python.documentation.SectionBasedDocString;
+import com.jetbrains.python.documentation.SectionBasedDocString.Section;
+import com.jetbrains.python.documentation.SectionBasedDocString.SectionField;
+import com.jetbrains.python.psi.PyIndentUtil;
+import com.jetbrains.python.toolbox.Substring;
+import org.jetbrains.annotations.NotNull;
+import org.jetbrains.annotations.Nullable;
+
+import java.util.List;
+
+/**
+ * @author Mikhail Golubev
+ */
+public abstract class SectionBasedDocStringUpdater extends DocStringUpdater<SectionBasedDocString> {
+  public SectionBasedDocStringUpdater(@NotNull SectionBasedDocString docString, @NotNull String minContentIndent) {
+    super(docString, minContentIndent);
+  }
+
+  public final void addParameter(@NotNull String name, @Nullable String type) {
+    if (type != null) {
+      final Substring typeSub = myOriginalDocString.getParamTypeSubstring(name);
+      if (typeSub != null) {
+        replace(typeSub.getTextRange(), type);
+        return;
+      }
+      final Substring nameSub = myOriginalDocString.getParamNameSubstring();
+      if (nameSub != null) {
+        updateParamDeclarationWithType(nameSub, type);
+        return;
+      }
+    }
+    final Section paramSection = findFirstParametersSection();
+    if (paramSection != null) {
+      final List<SectionField> fields = paramSection.getFields();
+      if (!fields.isEmpty()) {
+        final SectionField firstField = fields.get(0);
+        final String newLine = createParamLine(name, type, getSectionIndent(paramSection), getFieldIndent(paramSection, firstField));
+        insertBeforeLine(getFieldStartLine(firstField), newLine);
+      }
+      else {
+        final String newLine = createParamLine(name, type, getSectionIndent(paramSection), getExpectedFieldIndent());
+        insertAfterLine(getSectionStartLine(paramSection), newLine);
+      }
+    }
+    else {
+      final int line = findLastNonEmptyLine();
+      final String newSection = createBuilder()
+        .withSectionIndent(getExpectedFieldIndent())
+        .startParametersSection()
+        .addParameter(name, type, "")
+        .buildContent(getExpectedSectionIndent(), true);
+      insertAfterLine(line, newSection);
+    }
+  }
+
+  public final void addReturnValue(@NotNull String type) {
+    final Substring typeSub = myOriginalDocString.getReturnTypeSubstring();
+    if (typeSub != null) {
+      replace(typeSub.getTextRange(), type);
+      return;
+    }
+    final Section paramSection = findFirstReturnSection();
+    if (paramSection != null) {
+      final List<SectionField> fields = paramSection.getFields();
+      if (!fields.isEmpty()) {
+        final SectionField firstField = fields.get(0);
+        final String newLine = createReturnLine(type, getSectionIndent(paramSection), getFieldIndent(paramSection, firstField));
+        insertBeforeLine(getFieldStartLine(firstField), newLine);
+      }
+      else {
+        final String newLine = createReturnLine(type, getSectionIndent(paramSection), getExpectedFieldIndent());
+        insertAfterLine(getSectionLastTitleLine(paramSection), newLine);
+      }
+    }
+    else {
+      final int line = findLastNonEmptyLine();
+      final String newSection = createBuilder()
+        .withSectionIndent(getExpectedFieldIndent())
+        .startReturnsSection()
+        .addReturnValue(null, type, "")
+        .buildContent(getExpectedSectionIndent(), true);
+      insertAfterLine(line, newSection);
+    }
+  }
+
+  abstract void updateParamDeclarationWithType(@NotNull Substring nameSubstring, @NotNull String type);
+
+  protected int getSectionLastTitleLine(@NotNull Section paramSection) {
+    return getSectionStartLine(paramSection);
+  }
+
+  @Override
+  protected void scheduleUpdates() {
+  }
+
+  protected abstract SectionBasedDocStringBuilder createBuilder();
+
+  protected String createParamLine(@NotNull String name,
+                                   @Nullable String type,
+                                   @NotNull String docStringIndent,
+                                   @NotNull String sectionIndent) {
+    return createBuilder()
+      .withSectionIndent(sectionIndent)
+      .addParameter(name, type, "")
+      .buildContent(docStringIndent, true);
+  }
+
+  protected String createReturnLine(@NotNull String type,
+                                    @NotNull String docStringIndent,
+                                    @NotNull String sectionIndent) {
+    return createBuilder()
+      .withSectionIndent(sectionIndent)
+      .addReturnValue("", type, "")
+      .buildContent(docStringIndent, true);
+  }
+
+  @Nullable
+  protected Section findFirstParametersSection() {
+    return ContainerUtil.find(myOriginalDocString.getSections(), new Condition<Section>() {
+      @Override
+      public boolean value(Section section) {
+        return section.getNormalizedTitle().equals(SectionBasedDocString.PARAMETERS_SECTION);
+      }
+    });
+  }
+
+  @Nullable
+  protected Section findFirstReturnSection() {
+    return ContainerUtil.find(myOriginalDocString.getSections(), new Condition<Section>() {
+      @Override
+      public boolean value(Section section) {
+        return section.getNormalizedTitle().equals(SectionBasedDocString.RETURNS_SECTION);
+      }
+    });
+  }
+
+  @NotNull
+  protected String getExpectedSectionIndent() {
+    final Section first = ContainerUtil.getFirstItem(myOriginalDocString.getSections());
+    return first != null ? getSectionIndent(first) : myMinContentIndent;
+  }
+
+  @NotNull
+  protected String getExpectedFieldIndent() {
+    for (Section section : myOriginalDocString.getSections()) {
+      final List<SectionField> fields = section.getFields();
+      if (fields.isEmpty()) {
+        continue;
+      }
+      return getFieldIndent(section, fields.get(0));
+    }
+    return createBuilder().mySectionIndent;
+  }
+
+  @NotNull
+  protected String getFieldIndent(@NotNull Section section, @NotNull SectionField field) {
+    final String titleIndent = getSectionIndent(section);
+    final String fieldIndent = getLineIndent(getFieldStartLine(field));
+    final int diffSize = Math.max(1, PyIndentUtil.getLineIndentSize(titleIndent) - PyIndentUtil.getLineIndentSize(fieldIndent));
+    return StringUtil.repeatSymbol(' ', diffSize);
+  }
+
+  @NotNull
+  protected String getSectionIndent(@NotNull Section section) {
+    return getLineIndent(getSectionStartLine(section));
+  }
+
+  protected int getSectionStartLine(@NotNull Section section) {
+    return myOriginalDocString.getLineByOffset(section.getTitleAsSubstring().getStartOffset());
+  }
+
+  protected int getFieldStartLine(@NotNull SectionField field) {
+    final Substring anyFieldSub = ObjectUtils.chooseNotNull(field.getNameAsSubstring(), field.getTypeAsSubstring());
+    //noinspection ConstantConditions
+    return myOriginalDocString.getLineByOffset(anyFieldSub.getStartOffset());
+  }
+}
index 273d10929d10e2b4e8d233df67f0bbb20881c05b..27fe1f066f38ca42763b9633be5f9c45fbe306a9 100644 (file)
@@ -15,7 +15,6 @@
  */
 package com.jetbrains.python.documentation.docstrings;
 
-import com.intellij.openapi.util.text.StringUtil;
 import com.jetbrains.python.documentation.TagBasedDocString;
 import com.jetbrains.python.toolbox.Substring;
 import org.jetbrains.annotations.NotNull;
@@ -72,15 +71,6 @@ public class TagBasedDocStringUpdater<T extends TagBasedDocString> extends DocSt
     insertAfterLine(lastNonEmptyLine, lineBuilder.buildContent(indent, true));
   }
 
-  private int findLastNonEmptyLine() {
-    for (int i = myOriginalDocString.getLineCount() - 1; i >= 0; i--) {
-      if (StringUtil.isEmptyOrSpaces(myOriginalDocString.getLine(i))) {
-        return i;
-      }
-    }
-    return 0;
-  }
-
   private int findFirstLineWithTag() {
     for (int i = 0; i < myOriginalDocString.getLineCount(); i++) {
       final Substring line = myOriginalDocString.getLine(i);
index 7e1e07730abda9d84c10acc9aed903f39d52cc08..da64bb1bd227f0846a0975ed0aff829b25ce1921 100644 (file)
@@ -49,7 +49,7 @@ public class PySectionBasedDocStringTest extends PyTestCase {
     final List<Section> sections = docString.getSections();
     assertSize(3, sections);
 
-    assertEquals("parameters", sections.get(0).getTitle());
+    assertEquals("parameters", sections.get(0).getNormalizedTitle());
     final List<SectionField> paramFields = sections.get(0).getFields();
     assertSize(2, paramFields);
     final SectionField param1 = paramFields.get(0);
@@ -63,7 +63,7 @@ public class PySectionBasedDocStringTest extends PyTestCase {
     assertEquals("second parameter\n" +
                  "with longer description", param2.getDescription());
 
-    assertEquals("raises", sections.get(1).getTitle());
+    assertEquals("raises", sections.get(1).getNormalizedTitle());
     final List<SectionField> exceptionFields = sections.get(1).getFields();
     assertSize(1, exceptionFields);
     final SectionField exception1 = exceptionFields.get(0);
@@ -71,7 +71,7 @@ public class PySectionBasedDocStringTest extends PyTestCase {
     assertEquals("Exception", exception1.getType());
     assertEquals("if anything bad happens", exception1.getDescription());
 
-    assertEquals("returns", sections.get(2).getTitle());
+    assertEquals("returns", sections.get(2).getNormalizedTitle());
     final List<SectionField> returnFields = sections.get(2).getFields();
     assertSize(1, returnFields);
     final SectionField return1 = returnFields.get(0);
@@ -126,7 +126,7 @@ public class PySectionBasedDocStringTest extends PyTestCase {
     assertSize(2, docString.getSections());
 
     final Section examplesSection = docString.getSections().get(0);
-    assertEquals("examples", examplesSection.getTitle());
+    assertEquals("examples", examplesSection.getNormalizedTitle());
     assertSize(1, examplesSection.getFields());
     final SectionField example1 = examplesSection.getFields().get(0);
     assertEmpty(example1.getName());
@@ -135,7 +135,7 @@ public class PySectionBasedDocStringTest extends PyTestCase {
                  "func() == func()", example1.getDescription());
 
     final Section notesSection = docString.getSections().get(1);
-    assertEquals("notes", notesSection.getTitle());
+    assertEquals("notes", notesSection.getNormalizedTitle());
     assertSize(1, notesSection.getFields());
     final SectionField note1 = notesSection.getFields().get(0);
     assertEmpty(note1.getName());
@@ -168,7 +168,7 @@ public class PySectionBasedDocStringTest extends PyTestCase {
     final GoogleCodeStyleDocString docString = findAndParseGoogleStyleDocString();
     assertSize(1, docString.getSections());
     final Section section1 = docString.getSections().get(0);
-    assertEquals("parameters", section1.getTitle());
+    assertEquals("parameters", section1.getNormalizedTitle());
     assertSize(1, section1.getFields());
     final SectionField param1 = section1.getFields().get(0);
     assertEquals("x", param1.getName());
@@ -226,7 +226,7 @@ public class PySectionBasedDocStringTest extends PyTestCase {
     final NumpyDocString docString = findAndParseNumpyStyleDocString();
     assertSize(1, docString.getSections());
     final Section paramSection = docString.getSections().get(0);
-    assertEquals("parameters", paramSection.getTitle());
+    assertEquals("parameters", paramSection.getNormalizedTitle());
     assertSize(1, paramSection.getFields());
     final SectionField param1 = paramSection.getFields().get(0);
     assertEquals("x", param1.getName());
@@ -241,7 +241,7 @@ public class PySectionBasedDocStringTest extends PyTestCase {
     final GoogleCodeStyleDocString docString = findAndParseGoogleStyleDocString();
     assertSize(1, docString.getSections());
     final Section paramSection = docString.getSections().get(0);
-    assertEquals("parameters", paramSection.getTitle());
+    assertEquals("parameters", paramSection.getNormalizedTitle());
     assertSize(1, paramSection.getFields());
     final SectionField param1 = paramSection.getFields().get(0);
     assertEquals("x", param1.getName());
@@ -256,7 +256,7 @@ public class PySectionBasedDocStringTest extends PyTestCase {
     final GoogleCodeStyleDocString docString = findAndParseGoogleStyleDocString();
     assertSize(1, docString.getSections());
     final Section returnSection = docString.getSections().get(0);
-    assertEquals("returns", returnSection.getTitle());
+    assertEquals("returns", returnSection.getNormalizedTitle());
     assertSize(1, returnSection.getFields());
     final SectionField return1 = returnSection.getFields().get(0);
     assertEmpty(return1.getName());