PY-16987 Google and Numpy docstrings return null as parameter type if it wasn't speci...
[idea/community.git] / python / src / com / jetbrains / python / documentation / docstrings / SectionBasedDocString.java
1 /*
2  * Copyright 2000-2015 JetBrains s.r.o.
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  * http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 package com.jetbrains.python.documentation.docstrings;
17
18 import com.google.common.collect.ImmutableMap;
19 import com.google.common.collect.ImmutableSet;
20 import com.intellij.openapi.util.Condition;
21 import com.intellij.openapi.util.Pair;
22 import com.intellij.openapi.util.text.StringUtil;
23 import com.intellij.util.Function;
24 import com.intellij.util.containers.ContainerUtil;
25 import com.jetbrains.python.PyNames;
26 import com.jetbrains.python.psi.PyIndentUtil;
27 import com.jetbrains.python.psi.StructuredDocString;
28 import com.jetbrains.python.toolbox.Substring;
29 import org.jetbrains.annotations.NonNls;
30 import org.jetbrains.annotations.NotNull;
31 import org.jetbrains.annotations.Nullable;
32
33 import java.util.*;
34 import java.util.regex.Pattern;
35
36 /**
37  * Common base class for docstring styles supported by Napoleon Sphinx extension.
38  *
39  * @author Mikhail Golubev
40  * @see <a href="http://sphinxcontrib-napoleon.readthedocs.org/en/latest/index.html">Napoleon</a>
41  */
42 public abstract class SectionBasedDocString extends DocStringLineParser implements StructuredDocString {
43
44   /**
45    * Frequently used section types
46    */
47   @NonNls public static final String RETURNS_SECTION = "returns";
48   @NonNls public static final String RAISES_SECTION = "raises";
49   @NonNls public static final String KEYWORD_ARGUMENTS_SECTION = "keyword arguments";
50   @NonNls public static final String PARAMETERS_SECTION = "parameters";
51   @NonNls public static final String ATTRIBUTES_SECTION = "attributes";
52   @NonNls public static final String METHODS_SECTION = "methods";
53   @NonNls public static final String OTHER_PARAMETERS_SECTION = "other parameters";
54   @NonNls public static final String YIELDS_SECTION = "yields";
55   
56   private static final Pattern PLAIN_TEXT = Pattern.compile("\\w+(\\s+\\w+){2}"); // dumb heuristic - consecutive words
57
58   protected static final Map<String, String> SECTION_ALIASES =
59     ImmutableMap.<String, String>builder()
60                 .put("arguments", PARAMETERS_SECTION)
61                 .put("args", PARAMETERS_SECTION)
62                 .put("parameters", PARAMETERS_SECTION)
63                 .put("params", PARAMETERS_SECTION)
64                 .put("keyword args", KEYWORD_ARGUMENTS_SECTION)
65                 .put("keyword arguments", KEYWORD_ARGUMENTS_SECTION)
66                 .put("other parameters", OTHER_PARAMETERS_SECTION)
67                 .put("attributes", ATTRIBUTES_SECTION)
68                 .put("methods", METHODS_SECTION)
69                 .put("note", "notes")
70                 .put("notes", "notes")
71                 .put("example", "examples")
72                 .put("examples", "examples")
73                 .put("return", RETURNS_SECTION)
74                 .put("returns", RETURNS_SECTION)
75                 .put("yield", YIELDS_SECTION)
76                 .put("yields", "yields")
77                 .put("raises", RAISES_SECTION)
78                 .put("references", "references")
79                 .put("see also", "see also")
80                 .put("warning", "warnings")
81                 .put("warns", "warnings")
82                 .put("warnings", "warnings")
83                 .build();
84   private static final Pattern SPHINX_REFERENCE_RE = Pattern.compile("(:\\w+:\\S+:`.+?`|:\\S+:`.+?`|`.+?`)");
85
86   public static Set<String> SECTION_NAMES = SECTION_ALIASES.keySet();
87   private static final ImmutableSet<String> SECTIONS_WITH_NAME_AND_OPTIONAL_TYPE = ImmutableSet.of(ATTRIBUTES_SECTION,
88                                                                                                    PARAMETERS_SECTION,
89                                                                                                    KEYWORD_ARGUMENTS_SECTION,
90                                                                                                    OTHER_PARAMETERS_SECTION);
91   private static final ImmutableSet<String> SECTIONS_WITH_TYPE_AND_OPTIONAL_NAME = ImmutableSet.of(RETURNS_SECTION, YIELDS_SECTION);
92   private static final ImmutableSet<String> SECTIONS_WITH_TYPE = ImmutableSet.of(RAISES_SECTION);
93   private static final ImmutableSet<String> SECTIONS_WITH_NAME = ImmutableSet.of(METHODS_SECTION);
94
95   @Nullable
96   public static String getNormalizedSectionTitle(@NotNull @NonNls String title) {
97     return SECTION_ALIASES.get(title.toLowerCase());
98   }
99   
100   public static boolean isValidSectionTitle(@NotNull @NonNls String title) {
101     return StringUtil.isCapitalized(title) && getNormalizedSectionTitle(title) != null;
102   }
103
104   private final Substring mySummary;
105   private final List<Section> mySections = new ArrayList<Section>();
106   private final List<Substring> myOtherContent = new ArrayList<Substring>();
107
108   protected SectionBasedDocString(@NotNull Substring text) {
109     super(text);
110     List<Substring> summary = Collections.emptyList();
111     int startLine = consumeEmptyLines(parseHeader(0));
112     int lineNum = startLine;
113     while (lineNum < getLineCount()) {
114       final Pair<Section, Integer> parsedSection = parseSection(lineNum);
115       if (parsedSection.getFirst() != null) {
116         mySections.add(parsedSection.getFirst());
117         lineNum = parsedSection.getSecond();
118       }
119       else if (lineNum == startLine) {
120         final Pair<List<Substring>, Integer> parsedSummary = parseSummary(lineNum);
121         summary = parsedSummary.getFirst();
122         lineNum = parsedSummary.getSecond();
123       }
124       else {
125         myOtherContent.add(getLine(lineNum));
126         lineNum++;
127       }
128       lineNum = consumeEmptyLines(lineNum);
129     }
130     //noinspection ConstantConditions
131     mySummary = summary.isEmpty() ? null : summary.get(0).union(summary.get(summary.size() - 1)).trim();
132   }
133
134   @NotNull
135   private Pair<List<Substring>, Integer> parseSummary(int lineNum) {
136     final List<Substring> result = new ArrayList<Substring>();
137     while (!(isEmptyOrDoesNotExist(lineNum) || isBlockEnd(lineNum))) {
138       result.add(getLine(lineNum));
139       lineNum++;
140     }
141     return Pair.create(result, lineNum);
142   }
143
144   /**
145    * Used to parse e.g. optional function signature at the beginning of NumPy-style docstring
146    *
147    * @return first line from which to star parsing remaining sections
148    */
149   protected int parseHeader(int startLine) {
150     return startLine;
151   }
152
153   @NotNull
154   protected Pair<Section, Integer> parseSection(int sectionStartLine) {
155     final Pair<Substring, Integer> parsedHeader = parseSectionHeader(sectionStartLine);
156     if (parsedHeader.getFirst() == null) {
157       return Pair.create(null, sectionStartLine);
158     }
159     final String normalized = getNormalizedSectionTitle(parsedHeader.getFirst().toString());
160     if (normalized == null) {
161       return Pair.create(null, sectionStartLine);
162     }
163     final List<SectionField> fields = new ArrayList<SectionField>();
164     final int sectionIndent = getLineIndentSize(sectionStartLine);
165     int lineNum = consumeEmptyLines(parsedHeader.getSecond());
166     while (!isSectionBreak(lineNum, sectionIndent)) {
167       if (!isEmpty(lineNum)) {
168         final Pair<SectionField, Integer> parsedField = parseSectionField(lineNum, normalized, sectionIndent);
169         if (parsedField.getFirst() != null) {
170           fields.add(parsedField.getFirst());
171           lineNum = parsedField.getSecond();
172           continue;
173         }
174         else {
175           myOtherContent.add(getLine(lineNum));
176         }
177       }
178       lineNum++;
179     }
180     return Pair.create(new Section(parsedHeader.getFirst(), fields), lineNum);
181   }
182
183   @NotNull
184   protected Pair<SectionField, Integer> parseSectionField(int lineNum, @NotNull String normalizedSectionTitle, int sectionIndent) {
185     if (SECTIONS_WITH_NAME_AND_OPTIONAL_TYPE.contains(normalizedSectionTitle)) {
186       return parseSectionField(lineNum, sectionIndent, true, false);
187     }
188     if (SECTIONS_WITH_TYPE_AND_OPTIONAL_NAME.contains(normalizedSectionTitle)) {
189       return parseSectionField(lineNum, sectionIndent, true, true);
190     }
191     if (SECTIONS_WITH_NAME.contains(normalizedSectionTitle)) {
192       return parseSectionField(lineNum, sectionIndent, false, false);
193     }
194     if (SECTIONS_WITH_TYPE.contains(normalizedSectionTitle)) {
195       return parseSectionField(lineNum, sectionIndent, false, true);
196     }
197     return parseGenericField(lineNum, sectionIndent);
198   }
199
200   protected abstract Pair<SectionField, Integer> parseSectionField(int lineNum,
201                                                                    int sectionIndent,
202                                                                    boolean mayHaveType,
203                                                                    boolean preferType);
204
205   @NotNull
206   protected Pair<SectionField, Integer> parseGenericField(int lineNum, int sectionIndent) {
207     // We want to let section content has the same indent as section header, in particular for Numpy
208     final Pair<List<Substring>, Integer> pair = parseIndentedBlock(lineNum, Math.max(sectionIndent - 1, 0));
209     final Substring firstLine = ContainerUtil.getFirstItem(pair.getFirst());
210     final Substring lastLine = ContainerUtil.getLastItem(pair.getFirst());
211     if (firstLine != null && lastLine != null) {
212       return Pair.create(new SectionField((Substring)null, null, firstLine.union(lastLine).trim()), pair.getSecond());
213     }
214     return Pair.create(null, pair.getSecond());
215   }
216
217   @NotNull
218   protected abstract Pair<Substring, Integer> parseSectionHeader(int lineNum);
219
220   protected boolean isSectionStart(int lineNum) {
221     final Pair<Substring, Integer> pair = parseSectionHeader(lineNum);
222     return pair.getFirst() != null;
223   }
224
225   protected boolean isSectionBreak(int lineNum, int curSectionIndent) {
226     return lineNum >= getLineCount() || 
227            // note that field may have the same indent as its containing section
228            (!isEmpty(lineNum) && getLineIndentSize(lineNum) < curSectionIndent) || 
229            isSectionStart(lineNum);
230   }
231
232   /**
233    * Consumes all lines that are indented more than {@code blockIndent} and don't contain start of a new section.
234    * Trailing empty lines (e.g. due to indentation of closing triple quotes) are omitted in result.
235    *
236    * @param blockIndent indentation threshold, block ends with a line that has greater indentation
237    */
238   @NotNull
239   protected Pair<List<Substring>, Integer> parseIndentedBlock(int lineNum, int blockIndent) {
240     final int blockEnd = consumeIndentedBlock(lineNum, blockIndent);
241     return Pair.create(myLines.subList(lineNum, blockEnd), blockEnd);
242   }
243
244   @Override
245   protected boolean isBlockEnd(int lineNum) {
246     return isSectionStart(lineNum);
247   }
248
249   protected boolean isValidType(@NotNull String type) {
250     return !type.isEmpty() && !PLAIN_TEXT.matcher(type).find();
251   }
252
253   protected boolean isValidName(@NotNull String name) {
254     return PyNames.isIdentifierString(name.toString());
255   }
256
257   /**
258    * Properly partitions line by first colon taking into account possible Sphinx references inside
259    * <p/>
260    * <h3>Example</h3>
261    * <pre><code>
262    *   runtime (:class:`Runtime`): Use it to access the environment.
263    * </code></pre>
264    */
265   @NotNull
266   protected static List<Substring> splitByFirstColon(@NotNull Substring line) {
267     final List<Substring> parts = line.split(SPHINX_REFERENCE_RE);
268     if (parts.size() > 1) {
269       for (Substring part : parts) {
270         final int i = part.indexOf(":");
271         if (i >= 0) {
272           final Substring beforeColon = new Substring(line.getSuperString(), line.getStartOffset(), part.getStartOffset() + i);
273           final Substring afterColon = new Substring(line.getSuperString(), part.getStartOffset() + i + 1, line.getEndOffset());
274           return Arrays.asList(beforeColon, afterColon);
275         }
276       }
277       return Collections.singletonList(line);
278     }
279     return line.split(":", 1);
280   }
281
282   @NotNull
283   public List<Section> getSections() {
284     return Collections.unmodifiableList(mySections);
285   }
286
287   @Override
288   public String getSummary() {
289     return mySummary != null ? mySummary.concatTrimmedLines("\n") : "";
290   }
291
292   @Override
293   public String getDescription() {
294     return null;
295   }
296
297   @NotNull
298   @Override
299   public List<String> getParameters() {
300     return ContainerUtil.map(getParameterSubstrings(), new Function<Substring, String>() {
301       @Override
302       public String fun(Substring substring) {
303         return substring.toString();
304       }
305     });
306   }
307
308   @NotNull
309   @Override
310   public List<Substring> getParameterSubstrings() {
311     final List<Substring> result = new ArrayList<Substring>();
312     for (SectionField field : getParameterFields()) {
313       ContainerUtil.addAllNotNull(result, field.getNamesAsSubstrings());
314     }
315     return result;
316   }
317
318   @Nullable
319   @Override
320   public String getParamType(@Nullable String paramName) {
321     final Substring sub = getParamTypeSubstring(paramName);
322     return sub != null ? sub.toString() : null;
323   }
324
325   @Nullable
326   @Override
327   public Substring getParamTypeSubstring(@Nullable String paramName) {
328     if (paramName != null) {
329       final SectionField field = getFirstFieldForParameter(paramName);
330       if (field != null) {
331         return field.getTypeAsSubstring();
332       }
333     }
334     return null;
335   }
336
337   @Nullable
338   @Override
339   public String getParamDescription(@Nullable String paramName) {
340     if (paramName != null) {
341       final SectionField field = getFirstFieldForParameter(paramName);
342       if (field != null) {
343         return field.getDescription();
344       }
345     }
346     return null;
347   }
348
349   @Nullable
350   public SectionField getFirstFieldForParameter(@NotNull final String name) {
351     return ContainerUtil.find(getParameterFields(), new Condition<SectionField>() {
352       @Override
353       public boolean value(SectionField field) {
354         return field.getNames().contains(name);
355       }
356     });
357   }
358
359   @NotNull
360   public List<SectionField> getParameterFields() {
361     final List<SectionField> result = new ArrayList<SectionField>();
362     for (Section section : getParameterSections()) {
363       result.addAll(section.getFields());
364     }
365     return result;
366   }
367
368   @NotNull
369   public List<Section> getParameterSections() {
370     return getSectionsWithNormalizedTitle(PARAMETERS_SECTION);
371   }
372
373   @NotNull
374   @Override
375   public List<String> getKeywordArguments() {
376     final List<String> result = new ArrayList<String>();
377     for (SectionField field : getKeywordArgumentFields()) {
378       result.addAll(field.getNames());
379     }
380     return result;
381   }
382
383   @NotNull
384   @Override
385   public List<Substring> getKeywordArgumentSubstrings() {
386     final List<Substring> result = new ArrayList<Substring>();
387     for (SectionField field : getKeywordArgumentFields()) {
388       ContainerUtil.addAllNotNull(field.getNamesAsSubstrings());
389     }
390     return result;
391   }
392   
393   @Nullable
394   @Override
395   public String getKeywordArgumentDescription(@Nullable String paramName) {
396     if (paramName != null) {
397       final SectionField argument = getFirstFieldForKeywordArgument(paramName);
398       if (argument != null) {
399         return argument.getDescription();
400       }
401     }
402     return null;
403   }
404
405   @NotNull
406   public List<SectionField> getKeywordArgumentFields() {
407     final List<SectionField> result = new ArrayList<SectionField>();
408     for (Section section : getSectionsWithNormalizedTitle(KEYWORD_ARGUMENTS_SECTION)) {
409       result.addAll(section.getFields());
410     }
411     return result;
412   }
413
414   @Nullable
415   private SectionField getFirstFieldForKeywordArgument(@NotNull final String name) {
416     return ContainerUtil.find(getKeywordArgumentFields(), new Condition<SectionField>() {
417       @Override
418       public boolean value(SectionField field) {
419         return field.getNames().contains(name);
420       }
421     });
422   }
423
424   @Nullable
425   @Override
426   public String getReturnType() {
427     final Substring sub = getReturnTypeSubstring();
428     return sub != null ? sub.toString() : null;
429   }
430
431   @Nullable
432   @Override
433   public Substring getReturnTypeSubstring() {
434     final SectionField field = getFirstReturnField();
435     return field != null ? field.getTypeAsSubstring() : null;
436   }
437
438   @Nullable
439   @Override
440   public String getReturnDescription() {
441     final SectionField field = getFirstReturnField();
442     return field != null ? field.getDescription() : null;
443   }
444
445
446   @NotNull
447   public List<SectionField> getReturnFields() {
448     final List<SectionField> result = new ArrayList<SectionField>();
449     for (Section section : getSectionsWithNormalizedTitle(RETURNS_SECTION)) {
450       result.addAll(section.getFields());
451     }
452     return result;
453   }
454   
455   @Nullable
456   private SectionField getFirstReturnField() {
457     return ContainerUtil.getFirstItem(getReturnFields());
458   }
459
460   @NotNull
461   @Override
462   public List<String> getRaisedExceptions() {
463     return ContainerUtil.mapNotNull(getExceptionFields(), new Function<SectionField, String>() {
464       @Override
465       public String fun(SectionField field) {
466         return field.getType();
467       }
468     });
469   }
470
471   @Nullable
472   @Override
473   public String getRaisedExceptionDescription(@Nullable String exceptionName) {
474     if (exceptionName != null) {
475       final SectionField exception = getFirstFieldForException(exceptionName);
476       if (exception != null) {
477         return exception.getDescription();
478       }
479     }
480     return null;
481   }
482
483   @NotNull
484   public List<SectionField> getExceptionFields() {
485     final List<SectionField> result = new ArrayList<SectionField>();
486     for (Section section : getSectionsWithNormalizedTitle(RAISES_SECTION)) {
487       result.addAll(section.getFields());
488     }
489     return result;
490   }
491
492   @Nullable
493   private SectionField getFirstFieldForException(@NotNull final String exceptionType) {
494     return ContainerUtil.find(getExceptionFields(), new Condition<SectionField>() {
495       @Override
496       public boolean value(SectionField field) {
497         return exceptionType.equals(field.getType());
498       }
499     });
500   }
501
502   @NotNull
503   public List<SectionField> getAttributeFields() {
504     final List<SectionField> result = new ArrayList<SectionField>();
505     for (Section section : getSectionsWithNormalizedTitle(ATTRIBUTES_SECTION)) {
506       result.addAll(section.getFields());
507     }
508     return result;
509   }
510
511   @NotNull
512   public List<Section> getSectionsWithNormalizedTitle(@NotNull final String title) {
513     return ContainerUtil.mapNotNull(mySections, new Function<Section, Section>() {
514       @Override
515       public Section fun(Section section) {
516         return section.getNormalizedTitle().equals(getNormalizedSectionTitle(title)) ? section : null;
517       }
518     });
519   }
520
521   @Nullable
522   public Section getFirstSectionWithNormalizedTitle(@NotNull String title) {
523     return ContainerUtil.getFirstItem(getSectionsWithNormalizedTitle(title));
524   }
525
526   @Nullable
527   @Override
528   public String getAttributeDescription() {
529     return null;
530   }
531
532   @NotNull
533   protected static Substring cleanUpName(@NotNull Substring name) {
534     int firstNotStar = 0;
535     while (firstNotStar < name.length() && name.charAt(firstNotStar) == '*') {
536       firstNotStar++;
537     }
538     return name.substring(firstNotStar).trimLeft();
539   }
540
541   public static class Section {
542     private final Substring myTitle;
543     private final List<SectionField> myFields;
544
545     public Section(@NotNull Substring title, @NotNull List<SectionField> fields) {
546       myTitle = title;
547       myFields = new ArrayList<SectionField>(fields);
548     }
549
550     @NotNull
551     public Substring getTitleAsSubstring() {
552       return myTitle;
553     }
554
555     @NotNull
556     public String getTitle() {
557       return myTitle.toString();
558     }
559     
560     @NotNull
561     public String getNormalizedTitle() {
562       //noinspection ConstantConditions
563       return getNormalizedSectionTitle(getTitle());
564     }
565
566     @NotNull
567     public List<SectionField> getFields() {
568       return Collections.unmodifiableList(myFields);
569     }
570
571     @Override
572     public boolean equals(Object o) {
573       if (this == o) return true;
574       if (o == null || getClass() != o.getClass()) return false;
575
576       Section section = (Section)o;
577
578       if (!myTitle.equals(section.myTitle)) return false;
579       if (!myFields.equals(section.myFields)) return false;
580
581       return true;
582     }
583
584     @Override
585     public int hashCode() {
586       int result = myTitle.hashCode();
587       result = 31 * result + myFields.hashCode();
588       return result;
589     }
590   }
591
592   public static class SectionField {
593     private final List<Substring> myNames;
594     private final Substring myType;
595     private final Substring myDescription;
596
597     public SectionField(@Nullable Substring name, @Nullable Substring type, @Nullable Substring description) {
598       this(name == null ? Collections.<Substring>emptyList() : Collections.singletonList(name), type, description);
599     }
600
601     public SectionField(@NotNull List<Substring> names, @Nullable Substring type, @Nullable Substring description) {
602       myNames = names;
603       myType = type;
604       myDescription = description;
605     }
606
607     @NotNull
608     public String getName() {
609       return myNames.isEmpty() ? "" : myNames.get(0).toString();
610     }
611
612     @Nullable
613     public Substring getNameAsSubstring() {
614       return myNames.isEmpty() ? null : myNames.get(0);
615     }
616
617     @NotNull
618     public List<Substring> getNamesAsSubstrings() {
619       return myNames;
620     }
621
622     @NotNull
623     public List<String> getNames() {
624       return ContainerUtil.map(myNames, new Function<Substring, String>() {
625         @Override
626         public String fun(Substring substring) {
627           return substring.toString();
628         }
629       });
630     }
631
632     @NotNull
633     public String getType() {
634       return myType == null ? "" : myType.toString();
635     }
636
637     @Nullable
638     public Substring getTypeAsSubstring() {
639       return myType;
640     }
641
642     @NotNull 
643     public String getDescription() {
644       return myDescription == null ? "" : PyIndentUtil.removeCommonIndent(myDescription.getValue(), true);
645     }
646
647     @Nullable
648     public Substring getDescriptionAsSubstring() {
649       return myDescription;
650     }
651
652     @Override
653     public boolean equals(Object o) {
654       if (this == o) return true;
655       if (o == null || getClass() != o.getClass()) return false;
656
657       SectionField field = (SectionField)o;
658
659       if (myNames != null ? !myNames.equals(field.myNames) : field.myNames != null) return false;
660       if (myType != null ? !myType.equals(field.myType) : field.myType != null) return false;
661       if (myDescription != null ? !myDescription.equals(field.myDescription) : field.myDescription != null) return false;
662
663       return true;
664     }
665
666     @Override
667     public int hashCode() {
668       int result = myNames != null ? myNames.hashCode() : 0;
669       result = 31 * result + (myType != null ? myType.hashCode() : 0);
670       result = 31 * result + (myDescription != null ? myDescription.hashCode() : 0);
671       return result;
672     }
673   }
674 }