EA-75111 Handle projects with no modules and, hence, no configured docstring format
[idea/community.git] / python / src / com / jetbrains / python / documentation / docstrings / DocStringUtil.java
1 /*
2  * Copyright 2000-2014 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.intellij.lang.ASTNode;
19 import com.intellij.openapi.module.Module;
20 import com.intellij.openapi.module.ModuleManager;
21 import com.intellij.openapi.module.ModuleUtilCore;
22 import com.intellij.openapi.ui.Messages;
23 import com.intellij.openapi.util.TextRange;
24 import com.intellij.openapi.util.text.StringUtil;
25 import com.intellij.psi.PsiComment;
26 import com.intellij.psi.PsiElement;
27 import com.intellij.psi.PsiFile;
28 import com.intellij.psi.util.PsiTreeUtil;
29 import com.intellij.util.ArrayUtil;
30 import com.intellij.util.ObjectUtils;
31 import com.jetbrains.python.codeInsight.controlflow.ScopeOwner;
32 import com.jetbrains.python.documentation.PyDocumentationSettings;
33 import com.jetbrains.python.psi.*;
34 import com.jetbrains.python.psi.impl.PyPsiUtils;
35 import com.jetbrains.python.psi.impl.PyStringLiteralExpressionImpl;
36 import com.jetbrains.python.toolbox.Substring;
37 import org.jetbrains.annotations.NonNls;
38 import org.jetbrains.annotations.NotNull;
39 import org.jetbrains.annotations.Nullable;
40
41 import java.util.List;
42
43 /**
44  * User: catherine
45  */
46 public class DocStringUtil {
47   private DocStringUtil() {
48   }
49
50   @Nullable
51   public static String getDocStringValue(@NotNull PyDocStringOwner owner) {
52     return PyPsiUtils.strValue(owner.getDocStringExpression());
53   }
54
55   /**
56    * Attempts to detect docstring format from given text and parses it into corresponding structured docstring.
57    * It's recommended to use more reliable {@link #parse(String, PsiElement)} that fallbacks to format specified in settings.
58    *
59    * @param text docstring text <em>with both quotes and string prefix stripped</em> 
60    * @return structured docstring for one of supported formats or instance of {@link PlainDocString} if none was recognized.
61    * @see #parse(String, PsiElement)
62    */
63   @NotNull
64   public static StructuredDocString parse(@NotNull String text) {
65     return parse(text, null);
66   }
67
68   /**
69    * Attempts to detects docstring format first from given text, next from settings and parses text into corresponding structured docstring.
70    *
71    * @param text   docstring text <em>with both quotes and string prefix stripped</em>
72    * @param anchor PSI element that will be used to retrieve docstring format from the containing file or the project module
73    * @return structured docstring for one of supported formats or instance of {@link PlainDocString} if none was recognized.
74    * @see DocStringFormat#ALL_NAMES_BUT_PLAIN
75    * @see #guessDocStringFormat(String, PsiElement)
76    */
77   @NotNull
78   public static StructuredDocString parse(@NotNull String text, @Nullable PsiElement anchor) {
79     final DocStringFormat format = guessDocStringFormat(text, anchor);
80     return parseDocStringContent(format, text);
81   }
82
83   /**
84    * Attempts to detects docstring format first from the text of given string node, next from settings using given expression as an anchor 
85    * and parses text into corresponding structured docstring.
86    *
87    * @param stringLiteral supposedly result of {@link PyDocStringOwner#getDocStringExpression()}
88    * @return structured docstring for one of supported formats or instance of {@link PlainDocString} if none was recognized.
89    */
90   @NotNull
91   public static StructuredDocString parseDocString(@NotNull PyStringLiteralExpression stringLiteral) {
92     return parseDocString(guessDocStringFormat(stringLiteral.getStringValue(), stringLiteral), stringLiteral);
93   }
94
95   @NotNull
96   public static StructuredDocString parseDocString(@NotNull DocStringFormat format, @NotNull PyStringLiteralExpression stringLiteral) {
97     return parseDocString(format, stringLiteral.getStringNodes().get(0));
98   }
99
100   @NotNull
101   public static StructuredDocString parseDocString(@NotNull DocStringFormat format, @NotNull ASTNode node) {
102     //Preconditions.checkArgument(node.getElementType() == PyTokenTypes.DOCSTRING);
103     return parseDocString(format, node.getText());
104   }
105
106   /**
107    * @param stringText docstring text with possible string prefix and quotes
108    */
109   @NotNull
110   public static StructuredDocString parseDocString(@NotNull DocStringFormat format, @NotNull String stringText) {
111     return parseDocString(format, stripPrefixAndQuotes(stringText));
112   }
113
114   /**
115    * @param stringContent docstring text without string prefix and quotes, but not escaped, otherwise ranges of {@link Substring} returned
116    *                      from {@link StructuredDocString} may be invalid
117    */
118   @NotNull
119   public static StructuredDocString parseDocStringContent(@NotNull DocStringFormat format, @NotNull String stringContent) {
120     return parseDocString(format, new Substring(stringContent));
121   }
122
123   @NotNull
124   public static StructuredDocString parseDocString(@NotNull DocStringFormat format, @NotNull Substring content) {
125     switch (format) {
126       case REST:
127         return new SphinxDocString(content);
128       case EPYTEXT:
129         return new EpydocString(content);
130       case GOOGLE:
131         return new GoogleCodeStyleDocString(content);
132       case NUMPY:
133         return new NumpyDocString(content);
134       default:
135         return new PlainDocString(content);
136     }
137   }
138
139   @NotNull
140   private static Substring stripPrefixAndQuotes(@NotNull String text) {
141     final TextRange contentRange = PyStringLiteralExpressionImpl.getNodeTextRange(text);
142     return new Substring(text, contentRange.getStartOffset(), contentRange.getEndOffset());
143   }
144   
145   /**
146    * @return docstring format inferred heuristically solely from its content. For more reliable result use anchored version 
147    * {@link #guessDocStringFormat(String, PsiElement)} of this method.
148    * @see #guessDocStringFormat(String, PsiElement) 
149    */
150   @NotNull
151   public static DocStringFormat guessDocStringFormat(@NotNull String text) {
152     if (isLikeEpydocDocString(text)) {
153       return DocStringFormat.EPYTEXT;
154     }
155     if (isLikeSphinxDocString(text)) {
156       return DocStringFormat.REST;
157     }
158     if (isLikeNumpyDocstring(text)) {
159       return DocStringFormat.NUMPY;
160     }
161     if (isLikeGoogleDocString(text)) {
162       return DocStringFormat.GOOGLE;
163     }
164     return DocStringFormat.PLAIN;
165   }
166
167   /**
168    * @param text   docstring text <em>with both quotes and string prefix stripped</em>
169    * @param anchor PSI element that will be used to retrieve docstring format from the containing file or the project module
170    * @return docstring inferred heuristically and if unsuccessful fallback to configured format retrieved from anchor PSI element
171    * @see #getConfiguredDocStringFormat(PsiElement)
172    */
173   @NotNull
174   public static DocStringFormat guessDocStringFormat(@NotNull String text, @Nullable PsiElement anchor) {
175     final DocStringFormat guessed = guessDocStringFormat(text);
176     return guessed == DocStringFormat.PLAIN && anchor != null ? getConfiguredDocStringFormatOrPlain(anchor) : guessed;
177   }
178
179   /**
180    * @param anchor PSI element that will be used to retrieve docstring format from the containing file or the project module
181    * @return docstring format configured for file or module containing given anchor PSI element
182    * @see PyDocumentationSettings#getFormatForFile(PsiFile)
183    */
184   @Nullable
185   public static DocStringFormat getConfiguredDocStringFormat(@NotNull PsiElement anchor) {
186     final Module module = getModuleForElement(anchor);
187     if (module == null) {
188       return null;
189     }
190
191     final PyDocumentationSettings settings = PyDocumentationSettings.getInstance(module);
192     return settings.getFormatForFile(anchor.getContainingFile());
193   }
194
195   @NotNull
196   public static DocStringFormat getConfiguredDocStringFormatOrPlain(@NotNull PsiElement anchor) {
197     return ObjectUtils.chooseNotNull(getConfiguredDocStringFormat(anchor), DocStringFormat.PLAIN);
198   }
199
200   public static boolean isLikeSphinxDocString(@NotNull String text) {
201     return text.contains(":param ") || 
202            text.contains(":return:") || text.contains(":returns:") || 
203            text.contains(":rtype") || text.contains(":type");
204   }
205
206   public static boolean isLikeEpydocDocString(@NotNull String text) {
207     return text.contains("@param ") || text.contains("@return:") || text.contains("@rtype") || text.contains("@type");
208   }
209
210   public static boolean isLikeGoogleDocString(@NotNull String text) {
211     for (@NonNls String title : StringUtil.findMatches(text, GoogleCodeStyleDocString.SECTION_HEADER, 1)) {
212       if (SectionBasedDocString.isValidSectionTitle(title)) {
213         return true;
214       }
215     }
216     return false;
217   }
218
219   public static boolean isLikeNumpyDocstring(@NotNull String text) {
220     final String[] lines = StringUtil.splitByLines(text, false);
221     for (int i = 0; i < lines.length; i++) {
222       final String line = lines[i];
223       if (NumpyDocString.SECTION_HEADER.matcher(line).matches() && i > 0) {
224         @NonNls final String lineBefore = lines[i - 1];
225         if (SectionBasedDocString.SECTION_NAMES.contains(lineBefore.trim().toLowerCase())) {
226           return true;
227         }
228       }
229     }
230     return false;
231   }
232
233   /**
234    * Looks for a doc string under given parent.
235    *
236    * @param parent where to look. For classes and functions, this would be PyStatementList, for modules, PyFile.
237    * @return the defining expression, or null.
238    */
239   @Nullable
240   public static PyStringLiteralExpression findDocStringExpression(@Nullable PyElement parent) {
241     if (parent != null) {
242       PsiElement seeker = PyPsiUtils.getNextNonCommentSibling(parent.getFirstChild(), false);
243       if (seeker instanceof PyExpressionStatement) seeker = PyPsiUtils.getNextNonCommentSibling(seeker.getFirstChild(), false);
244       if (seeker instanceof PyStringLiteralExpression) return (PyStringLiteralExpression)seeker;
245     }
246     return null;
247   }
248
249   @Nullable
250   public static StructuredDocString getStructuredDocString(@NotNull PyDocStringOwner owner) {
251     final String value = owner.getDocStringValue();
252     return value == null ? null : parse(value, owner);
253   }
254
255   /**
256    * Returns containing docstring expression of class definition, function definition or module. 
257    * Useful to test whether particular PSI element is or belongs to such docstring.
258    */
259   @Nullable
260   public static PyStringLiteralExpression getParentDefinitionDocString(@NotNull PsiElement element) {
261     final PyDocStringOwner docStringOwner = PsiTreeUtil.getParentOfType(element, PyDocStringOwner.class);
262     if (docStringOwner != null) {
263       final PyStringLiteralExpression docString = docStringOwner.getDocStringExpression();
264       if (PsiTreeUtil.isAncestor(docString, element, false)) {
265         return docString;
266       }
267     }
268     return null;
269   }
270
271   public static boolean isDocStringExpression(@NotNull PyExpression expression) {
272     if (getParentDefinitionDocString(expression) == expression) {
273       return true;
274     }
275     if (expression instanceof PyStringLiteralExpression) {
276       return isVariableDocString((PyStringLiteralExpression)expression);
277     }
278     return false;
279   }
280
281   @Nullable
282   public static String getAttributeDocComment(@NotNull PyTargetExpression attr) {
283     if (attr.getParent() instanceof PyAssignmentStatement) {
284       final PyAssignmentStatement assignment = (PyAssignmentStatement)attr.getParent();
285       final PsiElement prevSibling = PyPsiUtils.getPrevNonWhitespaceSibling(assignment);
286       if (prevSibling instanceof PsiComment && prevSibling.getText().startsWith("#:")) {
287         return prevSibling.getText().substring(2);
288       }
289     }
290     return null;
291   }
292
293   public static boolean isVariableDocString(@NotNull PyStringLiteralExpression expr) {
294     final PsiElement parent = expr.getParent();
295     if (!(parent instanceof PyExpressionStatement)) {
296       return false;
297     }
298     final PsiElement prevElement = PyPsiUtils.getPrevNonCommentSibling(parent, true);
299     if (prevElement instanceof PyAssignmentStatement) {
300       if (expr.getText().contains("type:")) return true;
301
302       final PyAssignmentStatement assignmentStatement = (PyAssignmentStatement)prevElement;
303       final ScopeOwner scope = PsiTreeUtil.getParentOfType(prevElement, ScopeOwner.class);
304       if (scope instanceof PyClass || scope instanceof PyFile) {
305         return true;
306       }
307       if (scope instanceof PyFunction) {
308         for (PyExpression target : assignmentStatement.getTargets()) {
309           if (PyUtil.isInstanceAttribute(target)) {
310             return true;
311           }
312         }
313       }
314     }
315     return false;
316   }
317
318   /**
319    * Checks that docstring format is set either via element module's {@link com.jetbrains.python.PyNames.DOCFORMAT} attribute or
320    * in module settings. If none of them applies, show standard choose dialog, asking user to pick one and updates module settings
321    * accordingly.
322    *
323    * @param anchor PSI element that will be used to locate containing file and project module
324    * @return false if no structured docstring format was specified initially and user didn't select any, true otherwise
325    */
326   public static boolean ensureNotPlainDocstringFormat(@NotNull PsiElement anchor) {
327     final Module module = getModuleForElement(anchor);
328     if (module == null) {
329       return false;
330     }
331
332     return ensureNotPlainDocstringFormatForFile(anchor.getContainingFile(), module);
333   }
334
335   // Might return {@code null} in some rare cases when PSI element doesn't have an associated module.
336   // For instance, an empty IDEA project with a Python scratch file.
337   @Nullable
338   private static Module getModuleForElement(@NotNull PsiElement element) {
339     final Module module = ModuleUtilCore.findModuleForPsiElement(element);
340     if (module != null) {
341       return module;
342     }
343     
344     return ArrayUtil.getFirstElement(ModuleManager.getInstance(element.getProject()).getModules());
345   }
346
347   private static boolean ensureNotPlainDocstringFormatForFile(@NotNull PsiFile file, @NotNull Module module) {
348     final PyDocumentationSettings settings = PyDocumentationSettings.getInstance(module);
349     if (settings.isPlain(file)) {
350       final List<String> values = DocStringFormat.ALL_NAMES_BUT_PLAIN;
351       final int i =
352         Messages.showChooseDialog("Docstring format:", "Select Docstring Type", ArrayUtil.toStringArray(values), values.get(0), null);
353       if (i < 0) {
354         return false;
355       }
356       settings.setFormat(DocStringFormat.fromNameOrPlain(values.get(i)));
357     }
358     return true;
359   }
360 }