2 * Copyright 2000-2014 JetBrains s.r.o.
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
8 * http://www.apache.org/licenses/LICENSE-2.0
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.
16 package com.jetbrains.python.documentation.docstrings;
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.jetbrains.python.codeInsight.controlflow.ScopeOwner;
31 import com.jetbrains.python.documentation.PyDocumentationSettings;
32 import com.jetbrains.python.psi.*;
33 import com.jetbrains.python.psi.impl.PyPsiUtils;
34 import com.jetbrains.python.psi.impl.PyStringLiteralExpressionImpl;
35 import com.jetbrains.python.toolbox.Substring;
36 import org.jetbrains.annotations.NonNls;
37 import org.jetbrains.annotations.NotNull;
38 import org.jetbrains.annotations.Nullable;
40 import java.util.List;
45 public class DocStringUtil {
46 private DocStringUtil() {
50 public static String getDocStringValue(@NotNull PyDocStringOwner owner) {
51 return PyPsiUtils.strValue(owner.getDocStringExpression());
55 * Attempts to detect docstring format from given text and parses it into corresponding structured docstring.
56 * It's recommended to use more reliable {@link #parse(String, PsiElement)} that fallbacks to format specified in settings.
58 * @param text docstring text <em>with both quotes and string prefix stripped</em>
59 * @return structured docstring for one of supported formats or instance of {@link PlainDocString} if none was recognized.
60 * @see #parse(String, PsiElement)
63 public static StructuredDocString parse(@NotNull String text) {
64 return parse(text, null);
68 * Attempts to detects docstring format first from given text, next from settings and parses text into corresponding structured docstring.
70 * @param text docstring text <em>with both quotes and string prefix stripped</em>
71 * @param anchor PSI element that will be used to retrieve docstring format from the containing file or the project module
72 * @return structured docstring for one of supported formats or instance of {@link PlainDocString} if none was recognized.
73 * @see DocStringFormat#ALL_NAMES_BUT_PLAIN
74 * @see #guessDocStringFormat(String, PsiElement)
77 public static StructuredDocString parse(@NotNull String text, @Nullable PsiElement anchor) {
78 final DocStringFormat format = guessDocStringFormat(text, anchor);
79 return parseDocStringContent(format, text);
83 * Attempts to detects docstring format first from the text of given string node, next from settings using given expression as an anchor
84 * and parses text into corresponding structured docstring.
86 * @param stringLiteral supposedly result of {@link PyDocStringOwner#getDocStringExpression()}
87 * @return structured docstring for one of supported formats or instance of {@link PlainDocString} if none was recognized.
90 public static StructuredDocString parseDocString(@NotNull PyStringLiteralExpression stringLiteral) {
91 return parseDocString(guessDocStringFormat(stringLiteral.getStringValue(), stringLiteral), stringLiteral);
95 public static StructuredDocString parseDocString(@NotNull DocStringFormat format, @NotNull PyStringLiteralExpression stringLiteral) {
96 return parseDocString(format, stringLiteral.getStringNodes().get(0));
100 public static StructuredDocString parseDocString(@NotNull DocStringFormat format, @NotNull ASTNode node) {
101 //Preconditions.checkArgument(node.getElementType() == PyTokenTypes.DOCSTRING);
102 return parseDocString(format, node.getText());
106 * @param stringText docstring text with possible string prefix and quotes
109 public static StructuredDocString parseDocString(@NotNull DocStringFormat format, @NotNull String stringText) {
110 return parseDocString(format, stripPrefixAndQuotes(stringText));
114 * @param stringContent docstring text without string prefix and quotes, but not escaped, otherwise ranges of {@link Substring} returned
115 * from {@link StructuredDocString} may be invalid
118 public static StructuredDocString parseDocStringContent(@NotNull DocStringFormat format, @NotNull String stringContent) {
119 return parseDocString(format, new Substring(stringContent));
123 public static StructuredDocString parseDocString(@NotNull DocStringFormat format, @NotNull Substring content) {
126 return new SphinxDocString(content);
128 return new EpydocString(content);
130 return new GoogleCodeStyleDocString(content);
132 return new NumpyDocString(content);
134 return new PlainDocString(content);
139 private static Substring stripPrefixAndQuotes(@NotNull String text) {
140 final TextRange contentRange = PyStringLiteralExpressionImpl.getNodeTextRange(text);
141 return new Substring(text, contentRange.getStartOffset(), contentRange.getEndOffset());
145 * @return docstring format inferred heuristically solely from its content. For more reliable result use anchored version
146 * {@link #guessDocStringFormat(String, PsiElement)} of this method.
147 * @see #guessDocStringFormat(String, PsiElement)
150 public static DocStringFormat guessDocStringFormat(@NotNull String text) {
151 if (isLikeNumpyDocstring(text)) {
152 return DocStringFormat.NUMPY;
154 if (isLikeGoogleDocString(text)) {
155 return DocStringFormat.GOOGLE;
157 if (isLikeEpydocDocString(text)) {
158 return DocStringFormat.EPYTEXT;
160 if (isLikeSphinxDocString(text)) {
161 return DocStringFormat.REST;
163 return DocStringFormat.PLAIN;
167 * @param text docstring text <em>with both quotes and string prefix stripped</em>
168 * @param anchor PSI element that will be used to retrieve docstring format from the containing file or the project module
169 * @return docstring inferred heuristically and if unsuccessful fallback to configured format retrieved from anchor PSI element
170 * @see #getConfiguredDocStringFormat(PsiElement)
173 public static DocStringFormat guessDocStringFormat(@NotNull String text, @Nullable PsiElement anchor) {
174 final DocStringFormat guessed = guessDocStringFormat(text);
175 return guessed == DocStringFormat.PLAIN && anchor != null ? getConfiguredDocStringFormat(anchor) : guessed;
179 * @param anchor PSI element that will be used to retrieve docstring format from the containing file or the project module
180 * @return docstring format configured for file or module containing given anchor PSI element
181 * @see PyDocumentationSettings#getFormatForFile(PsiFile)
184 public static DocStringFormat getConfiguredDocStringFormat(@NotNull PsiElement anchor) {
185 final PyDocumentationSettings settings = PyDocumentationSettings.getInstance(getModuleForElement(anchor));
186 return settings.getFormatForFile(anchor.getContainingFile());
189 public static boolean isLikeSphinxDocString(@NotNull String text) {
190 return text.contains(":param ") ||
191 text.contains(":return:") || text.contains(":returns:") ||
192 text.contains(":rtype") || text.contains(":type");
195 public static boolean isLikeEpydocDocString(@NotNull String text) {
196 return text.contains("@param ") || text.contains("@return:") || text.contains("@rtype") || text.contains("@type");
199 public static boolean isLikeGoogleDocString(@NotNull String text) {
200 for (@NonNls String title : StringUtil.findMatches(text, GoogleCodeStyleDocString.SECTION_HEADER, 1)) {
201 if (SectionBasedDocString.isValidSectionTitle(title)) {
208 public static boolean isLikeNumpyDocstring(@NotNull String text) {
209 final String[] lines = StringUtil.splitByLines(text, false);
210 for (int i = 0; i < lines.length; i++) {
211 final String line = lines[i];
212 if (NumpyDocString.SECTION_HEADER.matcher(line).matches() && i > 0) {
213 @NonNls final String lineBefore = lines[i - 1];
214 if (SectionBasedDocString.SECTION_NAMES.contains(lineBefore.trim().toLowerCase())) {
223 * Looks for a doc string under given parent.
225 * @param parent where to look. For classes and functions, this would be PyStatementList, for modules, PyFile.
226 * @return the defining expression, or null.
229 public static PyStringLiteralExpression findDocStringExpression(@Nullable PyElement parent) {
230 if (parent != null) {
231 PsiElement seeker = PyPsiUtils.getNextNonCommentSibling(parent.getFirstChild(), false);
232 if (seeker instanceof PyExpressionStatement) seeker = PyPsiUtils.getNextNonCommentSibling(seeker.getFirstChild(), false);
233 if (seeker instanceof PyStringLiteralExpression) return (PyStringLiteralExpression)seeker;
239 public static StructuredDocString getStructuredDocString(@NotNull PyDocStringOwner owner) {
240 final String value = owner.getDocStringValue();
241 return value == null ? null : parse(value, owner);
245 * Returns containing docstring expression of class definition, function definition or module.
246 * Useful to test whether particular PSI element is or belongs to such docstring.
249 public static PyStringLiteralExpression getParentDefinitionDocString(@NotNull PsiElement element) {
250 final PyDocStringOwner docStringOwner = PsiTreeUtil.getParentOfType(element, PyDocStringOwner.class);
251 if (docStringOwner != null) {
252 final PyStringLiteralExpression docString = docStringOwner.getDocStringExpression();
253 if (PsiTreeUtil.isAncestor(docString, element, false)) {
260 public static boolean isDocStringExpression(@NotNull PyExpression expression) {
261 if (getParentDefinitionDocString(expression) == expression) {
264 if (expression instanceof PyStringLiteralExpression) {
265 return isVariableDocString((PyStringLiteralExpression)expression);
271 public static String getAttributeDocComment(@NotNull PyTargetExpression attr) {
272 if (attr.getParent() instanceof PyAssignmentStatement) {
273 final PyAssignmentStatement assignment = (PyAssignmentStatement)attr.getParent();
274 final PsiElement prevSibling = PyPsiUtils.getPrevNonWhitespaceSibling(assignment);
275 if (prevSibling instanceof PsiComment && prevSibling.getText().startsWith("#:")) {
276 return prevSibling.getText().substring(2);
282 public static boolean isVariableDocString(@NotNull PyStringLiteralExpression expr) {
283 final PsiElement parent = expr.getParent();
284 if (!(parent instanceof PyExpressionStatement)) {
287 final PsiElement prevElement = PyPsiUtils.getPrevNonCommentSibling(parent, true);
288 if (prevElement instanceof PyAssignmentStatement) {
289 if (expr.getText().contains("type:")) return true;
291 final PyAssignmentStatement assignmentStatement = (PyAssignmentStatement)prevElement;
292 final ScopeOwner scope = PsiTreeUtil.getParentOfType(prevElement, ScopeOwner.class);
293 if (scope instanceof PyClass || scope instanceof PyFile) {
296 if (scope instanceof PyFunction) {
297 for (PyExpression target : assignmentStatement.getTargets()) {
298 if (PyUtil.isInstanceAttribute(target)) {
308 * Checks that docstring format is set either via element module's {@link com.jetbrains.python.PyNames.DOCFORMAT} attribute or
309 * in module settings. If none of them applies, show standard choose dialog, asking user to pick one and updates module settings
312 * @param anchor PSI element that will be used to locate containing file and project module
313 * @return false if no structured docstring format was specified initially and user didn't select any, true otherwise
315 public static boolean ensureNotPlainDocstringFormat(@NotNull PsiElement anchor) {
316 return ensureNotPlainDocstringFormatForFile(anchor.getContainingFile(), getModuleForElement(anchor));
320 private static Module getModuleForElement(@NotNull PsiElement element) {
321 Module module = ModuleUtilCore.findModuleForPsiElement(element);
322 if (module == null) {
323 module = ModuleManager.getInstance(element.getProject()).getModules()[0];
328 private static boolean ensureNotPlainDocstringFormatForFile(@NotNull PsiFile file, @NotNull Module module) {
329 final PyDocumentationSettings settings = PyDocumentationSettings.getInstance(module);
330 if (settings.isPlain(file)) {
331 final List<String> values = DocStringFormat.ALL_NAMES_BUT_PLAIN;
333 Messages.showChooseDialog("Docstring format:", "Select Docstring Type", ArrayUtil.toStringArray(values), values.get(0), null);
337 settings.setFormat(DocStringFormat.fromNameOrPlain(values.get(i)));