/* * Copyright 2000-2013 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.intellij.codeInsight.documentation; import com.intellij.codeInspection.InspectionManager; import com.intellij.codeInspection.ProblemDescriptor; import com.intellij.codeInspection.QuickFix; import com.intellij.codeInspection.javaDoc.JavaDocLocalInspection; import com.intellij.codeInspection.javaDoc.JavaDocReferenceInspection; import com.intellij.javadoc.JavadocNavigationDelegate; import com.intellij.openapi.editor.Document; import com.intellij.openapi.editor.Editor; import com.intellij.openapi.project.Project; import com.intellij.openapi.util.Pair; import com.intellij.openapi.util.TextRange; import com.intellij.openapi.util.text.StringUtil; import com.intellij.psi.*; import com.intellij.psi.javadoc.PsiDocComment; import com.intellij.psi.javadoc.PsiDocTag; import com.intellij.psi.javadoc.PsiDocTagValue; import com.intellij.psi.javadoc.PsiDocToken; import com.intellij.util.containers.ContainerUtilRt; import com.intellij.util.text.CharArrayUtil; import org.jetbrains.annotations.NotNull; import java.util.*; /** * @author Denis Zhdanov * @since 9/20/12 8:44 PM */ public class JavaDocCommentFixer implements DocCommentFixer { @NotNull private static final String PARAM_TAG = "@param"; /** * Lists tags eligible for moving caret to after javadoc fixing. The main idea is that we want to locate caret at the * incomplete tag description after fixing the doc comment. *

* Example: *

   *   class Test {
   *     /**
   *      * Method description
   *      *
   *      * @param i    'i' argument
   *      * @param j    [we want to move the caret here because j's description is missing]
   *      */
   *     void test(int i, int j) {
   *     }
   *   }
   * 
*/ @NotNull private static final Set CARET_ANCHOR_TAGS = ContainerUtilRt.newHashSet(PARAM_TAG, "@throws", "@return"); @NotNull private static final Comparator COMPARATOR = new Comparator() { @Override public int compare(PsiElement e1, PsiElement e2) { return e2.getTextRange().getEndOffset() - e1.getTextRange().getEndOffset(); } }; @NotNull private static final String PARAM_TAG_NAME = "param"; @Override public void fixComment(@NotNull Project project, @NotNull Editor editor, @NotNull PsiComment comment) { if (!(comment instanceof PsiDocComment)) { return; } PsiDocComment docComment = (PsiDocComment)comment; PsiDocCommentOwner owner = docComment.getOwner(); if (owner == null) { return; } PsiFile file = comment.getContainingFile(); if (file == null) { return; } JavaDocReferenceInspection referenceInspection = new JavaDocReferenceInspection(); JavaDocLocalInspection localInspection = getDocLocalInspection(); InspectionManager inspectionManager = InspectionManager.getInstance(project); ProblemDescriptor[] referenceProblems = null; ProblemDescriptor[] otherProblems = null; if (owner instanceof PsiClass) { referenceProblems = referenceInspection.checkClass(((PsiClass)owner), inspectionManager, false); otherProblems = localInspection.checkClass(((PsiClass)owner), inspectionManager, false); } else if (owner instanceof PsiField) { referenceProblems = referenceInspection.checkField(((PsiField)owner), inspectionManager, false); otherProblems = localInspection.checkField(((PsiField)owner), inspectionManager, false); } else if (owner instanceof PsiMethod) { referenceProblems = referenceInspection.checkMethod((PsiMethod)owner, inspectionManager, false); otherProblems = localInspection.checkMethod((PsiMethod)owner, inspectionManager, false); } if (referenceProblems != null) { fixReferenceProblems(referenceProblems, project); } if (otherProblems != null) { fixCommonProblems(otherProblems, comment, editor.getDocument(), project); } PsiDocumentManager.getInstance(project).doPostponedOperationsAndUnblockDocument(editor.getDocument()); ensureContentOrdered(docComment, editor.getDocument()); locateCaret(docComment, editor, file); } @NotNull private static JavaDocLocalInspection getDocLocalInspection() { JavaDocLocalInspection localInspection = new JavaDocLocalInspection(); //region visibility localInspection.TOP_LEVEL_CLASS_OPTIONS.ACCESS_JAVADOC_REQUIRED_FOR = PsiModifier.PRIVATE; localInspection.INNER_CLASS_OPTIONS.ACCESS_JAVADOC_REQUIRED_FOR = PsiModifier.PRIVATE; localInspection.FIELD_OPTIONS.ACCESS_JAVADOC_REQUIRED_FOR = PsiModifier.PRIVATE; localInspection.METHOD_OPTIONS.ACCESS_JAVADOC_REQUIRED_FOR = PsiModifier.PRIVATE; //endregion localInspection.setIgnoreEmptyDescriptions(true); //region class type arguments if (!localInspection.TOP_LEVEL_CLASS_OPTIONS.REQUIRED_TAGS.contains(PARAM_TAG)) { localInspection.TOP_LEVEL_CLASS_OPTIONS.REQUIRED_TAGS += PARAM_TAG; } if (!localInspection.INNER_CLASS_OPTIONS.REQUIRED_TAGS.contains(PARAM_TAG)) { localInspection.INNER_CLASS_OPTIONS.REQUIRED_TAGS += PARAM_TAG; } //endregion return localInspection; } @SuppressWarnings("unchecked") private static void fixReferenceProblems(@NotNull ProblemDescriptor[] problems, @NotNull Project project) { for (ProblemDescriptor problem : problems) { QuickFix[] fixes = problem.getFixes(); if (fixes != null) { fixes[0].applyFix(project, problem); } } } /** * This fixer is based on existing javadoc inspections - there are two of them. One detects invalid references (to unexisted * method parameter or non-declared checked exception). Another one handles all other cases (parameter documentation is missing; * parameter doesn't have a description etc). This method handles result of the second exception * * @param problems detected problems * @param comment target comment to fix * @param document target document which contains text of the commen being fixed * @param project current project */ @SuppressWarnings("unchecked") private static void fixCommonProblems(@NotNull ProblemDescriptor[] problems, @NotNull PsiComment comment, @NotNull final Document document, @NotNull Project project) { List toRemove = new ArrayList(); for (ProblemDescriptor problem : problems) { PsiElement element = problem.getPsiElement(); if (element == null) { continue; } if ((!(element instanceof PsiDocToken) || !JavaDocTokenType.DOC_COMMENT_START.equals(((PsiDocToken)element).getTokenType())) && comment.getTextRange().contains(element.getTextRange())) { // Unnecessary element like '@return' at the void method's javadoc. for (PsiElement e = element; e != null; e = e.getParent()) { if (e instanceof PsiDocTag) { toRemove.add(e); break; } } } else { // Problems like 'missing @param'. QuickFix[] fixes = problem.getFixes(); if (fixes != null && fixes.length > 0) { fixes[0].applyFix(project, problem); } } } if (toRemove.isEmpty()) { return; } if (toRemove.size() > 1) { Collections.sort(toRemove, COMPARATOR); } PsiDocumentManager psiDocumentManager = PsiDocumentManager.getInstance(project); psiDocumentManager.doPostponedOperationsAndUnblockDocument(document); CharSequence text = document.getCharsSequence(); for (PsiElement element : toRemove) { int startOffset = element.getTextRange().getStartOffset(); int startLine = document.getLineNumber(startOffset); int i = CharArrayUtil.shiftBackward(text, startOffset - 1, " \t"); if (i >= 0) { char c = text.charAt(i); if (c == '*') { i = CharArrayUtil.shiftBackward(text, i - 1, " \t"); } } if (i >= 0 && text.charAt(i) == '\n') { startOffset = Math.max(i, document.getLineStartOffset(startLine) - 1); } int endOffset = element.getTextRange().getEndOffset(); // Javadoc PSI is awkward, it includes next line text before the next tag. That's why we need to strip it. i = CharArrayUtil.shiftBackward(text, endOffset - 1, " \t*"); if (i > 0 && text.charAt(i) == '\n') { endOffset = i; } document.deleteString(startOffset, endOffset); } psiDocumentManager.commitDocument(document); } private static void ensureContentOrdered(@NotNull PsiDocComment comment, @NotNull Document document) { //region Parse existing doc comment parameters. List current = new ArrayList(); Map> tagInfoByName = new HashMap>(); for (PsiDocTag tag : comment.getTags()) { if (!PARAM_TAG_NAME.equals(tag.getName())) { continue; } PsiDocTagValue valueElement = tag.getValueElement(); if (valueElement == null) { continue; } String paramName = valueElement.getText(); if (paramName != null) { current.add(paramName); tagInfoByName.put(paramName, parseTagValue(tag, document)); } } //endregion //region Calculate desired parameters order List ordered = new ArrayList(); PsiDocCommentOwner owner = comment.getOwner(); if ((owner instanceof PsiMethod)) { PsiParameter[] parameters = ((PsiMethod)owner).getParameterList().getParameters(); for (PsiParameter parameter : parameters) { ordered.add(parameter.getName()); } } if (owner instanceof PsiTypeParameterListOwner) { PsiTypeParameter[] typeParameters = ((PsiTypeParameterListOwner)owner).getTypeParameters(); for (PsiTypeParameter parameter : typeParameters) { ordered.add(String.format("<%s>", parameter.getName())); } } //endregion //region Fix order if necessary. if (current.size() != ordered.size()) { // Something is wrong, stop the processing. return; } boolean changed = false; for (int i = current.size() - 1; i >= 0; i--) { String newTag = ordered.get(i); String oldTag = current.get(i); if (newTag.equals(oldTag)) { continue; } TextRange range = tagInfoByName.get(oldTag).first; document.replaceString(range.getStartOffset(), range.getEndOffset(), tagInfoByName.get(newTag).second); changed = true; } if (changed) { PsiDocumentManager manager = PsiDocumentManager.getInstance(comment.getProject()); manager.commitDocument(document); } //endregion } @NotNull private static Pair parseTagValue(@NotNull PsiDocTag tag, @NotNull Document document) { PsiDocTagValue valueElement = tag.getValueElement(); assert valueElement != null; int startOffset = valueElement.getTextRange().getStartOffset(); int endOffset = tag.getTextRange().getEndOffset(); // Javadoc PSI is rather weird... CharSequence text = document.getCharsSequence(); int i = CharArrayUtil.shiftBackward(text, endOffset - 1, " \t*"); if (i > 0 && text.charAt(i) == '\n') { endOffset = i; } return Pair.create(TextRange.create(startOffset, endOffset), text.subSequence(startOffset, endOffset).toString()); } private static void locateCaret(@NotNull PsiDocComment comment, @NotNull Editor editor, @NotNull PsiFile file) { Document document = editor.getDocument(); int lineToNavigate = -1; for (PsiDocTag tag : comment.getTags()) { PsiElement nameElement = tag.getNameElement(); if (nameElement == null || !CARET_ANCHOR_TAGS.contains(nameElement.getText())) { continue; } boolean good = false; PsiElement[] dataElements = tag.getDataElements(); if (dataElements != null) { PsiDocTagValue valueElement = tag.getValueElement(); for (PsiElement element : dataElements) { if (element == valueElement) { continue; } if (!StringUtil.isEmptyOrSpaces(element.getText())) { good = true; break; } } } if (!good) { int offset = tag.getTextRange().getEndOffset(); CharSequence text = document.getCharsSequence(); int i = CharArrayUtil.shiftBackward(text, offset - 1, " \t*"); if (i > 0 && text.charAt(i) == '\n') { offset = i - 1; } lineToNavigate = document.getLineNumber(offset); break; } } if (lineToNavigate >= 0) { editor.getCaretModel().moveToOffset(document.getLineEndOffset(lineToNavigate)); JavadocNavigationDelegate.navigateToLineEnd(editor, file); } } }