/* * Copyright 2000-2014 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.codeInspection.javaDoc; import com.intellij.ToolExtensionPoints; import com.intellij.codeInsight.FileModificationService; import com.intellij.codeInsight.daemon.QuickFixBundle; import com.intellij.codeInspection.*; import com.intellij.codeInspection.reference.RefJavaUtil; import com.intellij.lang.ASTNode; import com.intellij.openapi.diagnostic.Logger; import com.intellij.openapi.extensions.ExtensionPoint; import com.intellij.openapi.extensions.Extensions; import com.intellij.openapi.project.Project; import com.intellij.openapi.util.*; import com.intellij.pom.Navigatable; import com.intellij.profile.codeInspection.InspectionProfileManager; import com.intellij.profile.codeInspection.InspectionProjectProfileManager; import com.intellij.psi.*; import com.intellij.psi.impl.source.javadoc.PsiDocParamRef; import com.intellij.psi.javadoc.*; import com.intellij.psi.util.InheritanceUtil; import com.intellij.psi.util.PropertyUtil; import com.intellij.psi.util.PsiTreeUtil; import com.intellij.util.IncorrectOperationException; import com.intellij.util.containers.ContainerUtil; import org.jdom.Element; import org.jetbrains.annotations.NonNls; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; import java.util.*; public class JavaDocLocalInspectionBase extends BaseJavaBatchLocalInspectionTool { private static final Logger LOG = Logger.getInstance("com.intellij.codeInspection.javaDoc.JavaDocLocalInspectionBase"); @NonNls protected static final String NONE = "none"; @NonNls protected static final String PUBLIC = "public"; @NonNls protected static final String PROTECTED = "protected"; @NonNls protected static final String PACKAGE_LOCAL = "package"; @NonNls protected static final String PRIVATE = "private"; private static final String REQUIRED_JAVADOC_IS_ABSENT = InspectionsBundle.message("inspection.javadoc.problem.descriptor"); @NonNls private static final Set ourUniqueTags = new HashSet(); @NonNls public static final String SHORT_NAME = "JavaDoc"; static { ourUniqueTags.add("return"); ourUniqueTags.add("deprecated"); ourUniqueTags.add("serial"); ourUniqueTags.add("serialData"); } @NonNls private static final String IGNORE_ACCESSORS_ATTR_NAME = "IGNORE_ACCESSORS"; @NonNls private static final String IGNORE_DUPLICATED_THROWS_TAGS_ATTR_NAME = "IGNORE_DUPLICATED_THROWS_TAGS"; public static class Options implements JDOMExternalizable { @NonNls public String ACCESS_JAVADOC_REQUIRED_FOR = NONE; @NonNls public String REQUIRED_TAGS = ""; public Options() {} public Options(String ACCESS_JAVADOC_REQUIRED_FOR, String REQUIRED_TAGS) { this.ACCESS_JAVADOC_REQUIRED_FOR = ACCESS_JAVADOC_REQUIRED_FOR; this.REQUIRED_TAGS = REQUIRED_TAGS; } @Override public void readExternal(Element element) throws InvalidDataException { DefaultJDOMExternalizer.readExternal(this, element); } @Override public void writeExternal(Element element) throws WriteExternalException { DefaultJDOMExternalizer.writeExternal(this, element); } } @NonNls protected final Options PACKAGE_OPTIONS = new Options("none", ""); @NonNls public Options TOP_LEVEL_CLASS_OPTIONS = new Options("none", ""); @NonNls public Options INNER_CLASS_OPTIONS = new Options("none", ""); @NonNls public Options METHOD_OPTIONS = new Options("none", "@return@param@throws or @exception"); @NonNls public Options FIELD_OPTIONS = new Options("none", ""); public boolean IGNORE_DEPRECATED = false; public boolean IGNORE_JAVADOC_PERIOD = true; @Deprecated public boolean IGNORE_DUPLICATED_THROWS = false; private boolean myIgnoreDuplicatedThrows = true; public boolean getIgnoreDuplicatedThrows() { return myIgnoreDuplicatedThrows; } public void setIgnoreDuplicatedThrows(boolean ignoreDuplicatedThrows) { myIgnoreDuplicatedThrows = ignoreDuplicatedThrows; } public boolean IGNORE_POINT_TO_ITSELF = false; public String myAdditionalJavadocTags = ""; private boolean myIgnoreEmptyDescriptions = false; protected boolean myIgnoreSimpleAccessors = false; @Override public boolean isEnabledByDefault() { return true; } public void setIgnoreSimpleAccessors(boolean ignoreSimpleAccessors) { myIgnoreSimpleAccessors = ignoreSimpleAccessors; } public void setPackageOption(@NonNls String modifier, @NonNls String tags) { PACKAGE_OPTIONS.ACCESS_JAVADOC_REQUIRED_FOR = modifier; PACKAGE_OPTIONS.REQUIRED_TAGS = tags; } @Override public void writeSettings(@NotNull Element node) throws WriteExternalException { super.writeSettings(node); if (myIgnoreSimpleAccessors) { final Element option = new Element(IGNORE_ACCESSORS_ATTR_NAME); option.setAttribute("value", String.valueOf(true)); node.addContent(option); } if (!myIgnoreDuplicatedThrows) { node.addContent(new Element(IGNORE_DUPLICATED_THROWS_TAGS_ATTR_NAME).setAttribute("value", String.valueOf(false))); } if (!PACKAGE_OPTIONS.ACCESS_JAVADOC_REQUIRED_FOR.equals("none") || !PACKAGE_OPTIONS.REQUIRED_TAGS.isEmpty()) { PACKAGE_OPTIONS.writeExternal(node); } } @Override public void readSettings(@NotNull Element node) throws InvalidDataException { super.readSettings(node); final Element ignoreAccessorsTag = node.getChild(IGNORE_ACCESSORS_ATTR_NAME); if (ignoreAccessorsTag != null) { myIgnoreSimpleAccessors = Boolean.parseBoolean(ignoreAccessorsTag.getAttributeValue("value")); } Element ignoreDupThrowsTag = node.getChild(IGNORE_DUPLICATED_THROWS_TAGS_ATTR_NAME); if (ignoreDupThrowsTag != null) { myIgnoreDuplicatedThrows = Boolean.parseBoolean(ignoreDupThrowsTag.getAttributeValue("value")); } PACKAGE_OPTIONS.readExternal(node); } private static ProblemDescriptor createDescriptor(@NotNull PsiElement element, String template, InspectionManager manager, boolean onTheFly) { return manager.createProblemDescriptor(element, template, onTheFly, null, ProblemHighlightType.GENERIC_ERROR_OR_WARNING); } private static ProblemDescriptor createDescriptor(@NotNull PsiElement element, String template, @NotNull LocalQuickFix fix, InspectionManager manager, boolean onTheFly) { return manager.createProblemDescriptor(element, template, fix, ProblemHighlightType.GENERIC_ERROR_OR_WARNING, onTheFly); } private static class AddMissingTagFix implements LocalQuickFix { private final String myTag; private final String myValue; public AddMissingTagFix(@NonNls @NotNull String tag, @NotNull String value) { myTag = tag; myValue = value; } public AddMissingTagFix(@NotNull String tag) { this(tag, ""); } @Override @NotNull public String getName() { return InspectionsBundle.message("inspection.javadoc.problem.add.tag", myTag, myValue); } @Override public void applyFix(@NotNull Project project, @NotNull ProblemDescriptor descriptor) { final PsiElementFactory factory = JavaPsiFacade.getInstance(project).getElementFactory(); try { final PsiDocComment docComment = PsiTreeUtil.getParentOfType(descriptor.getEndElement(), PsiDocComment.class); if (docComment != null) { if (!FileModificationService.getInstance().preparePsiElementsForWrite(docComment)) return; final PsiDocTag tag = factory.createDocTagFromText("@" + myTag + " " + myValue); PsiElement addedTag; final PsiElement anchor = getAnchor(descriptor); if (anchor != null) { addedTag = docComment.addBefore(tag, anchor); } else { addedTag = docComment.add(tag); } moveCaretAfter(addedTag); } } catch (IncorrectOperationException e) { LOG.error(e); } } @Nullable protected PsiElement getAnchor(ProblemDescriptor descriptor) { return null; } private static void moveCaretAfter(final PsiElement newCaretPosition) { PsiElement sibling = newCaretPosition.getNextSibling(); if (sibling != null) { ((Navigatable)sibling).navigate(true); } } @Override @NotNull public String getFamilyName() { return InspectionsBundle.message("inspection.javadoc.problem.add.tag.family"); } } @Nullable @Override public ProblemDescriptor[] checkFile(@NotNull PsiFile file, @NotNull InspectionManager manager, boolean isOnTheFly) { if (!PsiPackage.PACKAGE_INFO_FILE.equals(file.getName()) || !(file instanceof PsiJavaFile)) { return null; } final PsiDocComment docComment = PsiTreeUtil.getChildOfType(file, PsiDocComment.class); final JavaDirectoryService directoryService = JavaDirectoryService.getInstance(); final PsiDirectory directory = file.getContainingDirectory(); final PsiPackage aPackage = directoryService.getPackage(directory); if (IGNORE_DEPRECATED && aPackage != null) { final PsiModifierList modifierList = aPackage.getModifierList(); if (modifierList != null && modifierList.findAnnotation("java.lang.Deprecated") != null) { return null; } } final PsiJavaFile javaFile = (PsiJavaFile)file; final PsiPackageStatement packageStatement = javaFile.getPackageStatement(); final PsiElement elementToHighlight = packageStatement != null ? packageStatement : file; final boolean required = isJavaDocRequired(aPackage); if (docComment != null) { if (IGNORE_DEPRECATED && docComment.findTagByName("deprecated") != null) { return null; } } else { return required ? new ProblemDescriptor[]{createDescriptor(elementToHighlight, REQUIRED_JAVADOC_IS_ABSENT, manager, isOnTheFly)} : null; } final PsiDocTag[] tags = docComment.getTags(); final List problems = getRequiredTagProblems(aPackage, docComment.getFirstChild(), tags, manager, isOnTheFly, required); final List tagProblems = getTagValuesProblems(aPackage, tags, manager, isOnTheFly); if (tagProblems != null) { problems.addAll(tagProblems); } checkInlineTags(manager, problems, docComment.getDescriptionElements(), JavadocManager.SERVICE.getInstance(docComment.getProject()), isOnTheFly); checkForPeriodInDoc(aPackage, docComment, problems, manager, isOnTheFly); checkForBadCharacters(docComment, problems, manager, isOnTheFly); return problems.isEmpty() ? null : problems.toArray(new ProblemDescriptor[problems.size()]); } @Override @Nullable public ProblemDescriptor[] checkClass(@NotNull PsiClass psiClass, @NotNull InspectionManager manager, boolean isOnTheFly) { if (psiClass instanceof PsiAnonymousClass) return null; if (psiClass instanceof PsiSyntheticClass) return null; if (psiClass instanceof PsiTypeParameter) return null; if (IGNORE_DEPRECATED && psiClass.isDeprecated()) { return null; } PsiDocComment docComment = psiClass.getDocComment(); final PsiIdentifier nameIdentifier = psiClass.getNameIdentifier(); final PsiElement elementToHighlight = nameIdentifier != null ? nameIdentifier : psiClass; final boolean required = isJavaDocRequired(psiClass); if (docComment == null) { return required ? new ProblemDescriptor[]{createDescriptor(elementToHighlight, REQUIRED_JAVADOC_IS_ABSENT, manager, isOnTheFly)} : null; } PsiDocTag[] tags = docComment.getTags(); final List problems = getRequiredTagProblems(psiClass, docComment.getFirstChild(), tags, manager, isOnTheFly, required); List tagProblems = getTagValuesProblems(psiClass, tags, manager, isOnTheFly); if (tagProblems != null) { problems.addAll(tagProblems); } checkForPeriodInDoc(psiClass, docComment, problems, manager, isOnTheFly); checkInlineTags(manager, problems, docComment.getDescriptionElements(), JavadocManager.SERVICE.getInstance(docComment.getProject()), isOnTheFly); checkForBadCharacters(docComment, problems, manager, isOnTheFly); checkDuplicateTags(tags, problems, manager, isOnTheFly); if (required && isTagRequired(psiClass, "param") && psiClass.hasTypeParameters() && nameIdentifier != null) { ArrayList absentParameters = null; final PsiTypeParameter[] typeParameters = psiClass.getTypeParameters(); for (PsiTypeParameter typeParameter : typeParameters) { if (!isFound(tags, typeParameter)) { if (absentParameters == null) absentParameters = new ArrayList(1); absentParameters.add(typeParameter); } } if (absentParameters != null) { for (PsiTypeParameter psiTypeParameter : absentParameters) { problems.add(createMissingParamTagDescriptor(docComment.getFirstChild(), psiTypeParameter, manager, isOnTheFly)); } } } return problems.isEmpty() ? null : problems.toArray(new ProblemDescriptor[problems.size()]); } private List getRequiredTagProblems(PsiElement context, PsiElement elementToHighlight, PsiDocTag[] tags, InspectionManager manager, boolean isOnTheFly, boolean required) { @NonNls String[] tagsToCheck = {"author", "version", "since"}; @NonNls String[] absentDescriptionKeys = { "inspection.javadoc.problem.missing.author.description", "inspection.javadoc.problem.missing.version.description", "inspection.javadoc.problem.missing.since.description"}; final ArrayList problems = new ArrayList(2); if (required) { boolean[] isTagRequired = new boolean[tagsToCheck.length]; boolean[] isTagPresent = new boolean[tagsToCheck.length]; boolean someTagsAreRequired = false; for (int i = 0; i < tagsToCheck.length; i++) { final String tag = tagsToCheck[i]; someTagsAreRequired |= isTagRequired[i] = isTagRequired(context, tag); } if (someTagsAreRequired) { for (PsiDocTag tag : tags) { String tagName = tag.getName(); for (int i = 0; i < tagsToCheck.length; i++) { final String tagToCheck = tagsToCheck[i]; if (tagToCheck.equals(tagName)) { isTagPresent[i] = true; } } } } for (int i = 0; i < tagsToCheck.length; i++) { final String tagToCheck = tagsToCheck[i]; if (isTagRequired[i] && !isTagPresent[i]) { problems.add(createMissingTagDescriptor(elementToHighlight, tagToCheck, manager, isOnTheFly)); } } } for (PsiDocTag tag : tags) { for (int i = 0; i < tagsToCheck.length; i++) { final String tagToCheck = tagsToCheck[i]; if (tagToCheck.equals(tag.getName()) && extractTagDescription(tag).isEmpty()) { problems.add(createDescriptor(tag.getNameElement(), InspectionsBundle.message(absentDescriptionKeys[i]), manager, isOnTheFly)); } } } return problems; } private static ProblemDescriptor createMissingParamTagDescriptor(final PsiElement elementToHighlight, final PsiTypeParameter psiTypeParameter, final InspectionManager manager, boolean isOnTheFly) { String message = InspectionsBundle.message("inspection.javadoc.problem.missing.tag", "@param"); return createDescriptor(elementToHighlight, message, new AddMissingTagFix("param", "<" + psiTypeParameter.getName() + ">"), manager, isOnTheFly); } @Override @Nullable public ProblemDescriptor[] checkField(@NotNull PsiField psiField, @NotNull InspectionManager manager, boolean isOnTheFly) { if (IGNORE_DEPRECATED && (psiField.isDeprecated() || psiField.getContainingClass().isDeprecated())) { return null; } PsiDocComment docComment = psiField.getDocComment(); if (docComment == null) { return isJavaDocRequired(psiField) ? new ProblemDescriptor[]{createDescriptor(psiField.getNameIdentifier(), REQUIRED_JAVADOC_IS_ABSENT, manager, isOnTheFly)} : null; } final ArrayList problems = new ArrayList(2); ArrayList tagProblems = getTagValuesProblems(psiField, docComment.getTags(), manager, isOnTheFly); if (tagProblems != null) { problems.addAll(tagProblems); } checkInlineTags(manager, problems, docComment.getDescriptionElements(), JavadocManager.SERVICE.getInstance(docComment.getProject()), isOnTheFly); checkForPeriodInDoc(psiField, docComment, problems, manager, isOnTheFly); checkDuplicateTags(docComment.getTags(), problems, manager, isOnTheFly); checkForBadCharacters(docComment, problems, manager, isOnTheFly); return problems.isEmpty() ? null : problems.toArray(new ProblemDescriptor[problems.size()]); } @Override @Nullable public ProblemDescriptor[] checkMethod(@NotNull PsiMethod psiMethod, @NotNull InspectionManager manager, boolean isOnTheFly) { if (psiMethod instanceof SyntheticElement) return null; if (IGNORE_DEPRECATED && (psiMethod.isDeprecated() || psiMethod.getContainingClass().isDeprecated())) { return null; } if (myIgnoreSimpleAccessors && PropertyUtil.isSimplePropertyAccessor(psiMethod)) { return null; } PsiDocComment docComment = psiMethod.getDocComment(); final PsiMethod[] superMethods = psiMethod.findSuperMethods(); final boolean required = isJavaDocRequired(psiMethod); if (docComment == null) { if (required) { if (superMethods.length > 0) return null; ExtensionPoint> point = Extensions.getRootArea().getExtensionPoint(ToolExtensionPoints.JAVADOC_LOCAL); final Condition[] addins = point.getExtensions(); for (Condition addin : addins) { if (addin.value(psiMethod)) return null; } if (superMethods.length == 0) { final PsiIdentifier nameIdentifier = psiMethod.getNameIdentifier(); return nameIdentifier != null ? new ProblemDescriptor[] { createDescriptor(nameIdentifier, REQUIRED_JAVADOC_IS_ABSENT, manager, isOnTheFly)} : null; } else { return null; } } else { return null; } } final PsiElement[] descriptionElements = docComment.getDescriptionElements(); for (PsiElement descriptionElement : descriptionElements) { if (descriptionElement instanceof PsiInlineDocTag) { if ("inheritDoc".equals(((PsiInlineDocTag)descriptionElement).getName())) return null; } } List problems = new ArrayList(2); checkInlineTags(manager, problems, descriptionElements, JavadocManager.SERVICE.getInstance(docComment.getProject()), isOnTheFly); final PsiDocTag tagByName = docComment.findTagByName("inheritDoc"); if (tagByName != null) { final String tagName = tagByName.getName(); final JavadocTagInfo tagInfo = JavadocManager.SERVICE.getInstance(tagByName.getProject()).getTagInfo(tagName); if (tagInfo != null && tagInfo.isValidInContext(psiMethod)){ return null; } } PsiDocTag[] tags = docComment.getTags(); boolean isReturnRequired = false; boolean isReturnAbsent = true; if (superMethods.length == 0 && !psiMethod.isConstructor() && !PsiType.VOID.equals(psiMethod.getReturnType()) && isTagRequired(psiMethod, "return")) { isReturnRequired = true; for (PsiDocTag tag : tags) { if ("return".equals(tag.getName())) { isReturnAbsent = false; break; } } } ArrayList absentParameters = null; if (required && superMethods.length == 0 && isTagRequired(psiMethod, "param") ) { PsiParameter[] params = psiMethod.getParameterList().getParameters(); for (PsiParameter param : params) { if (!isFound(tags, param)) { if (absentParameters == null) absentParameters = new ArrayList(2); absentParameters.add(param); } } } if (required && isReturnRequired && isReturnAbsent) { problems.add(createMissingTagDescriptor(docComment.getFirstChild(), "return", manager, isOnTheFly)); } if (absentParameters != null) { for (PsiParameter psiParameter : absentParameters) { problems.add(createMissingParamTagDescriptor(docComment.getFirstChild(), psiParameter, manager, isOnTheFly)); } } if (!myIgnoreEmptyDescriptions) { for (PsiDocTag tag : tags) { if ("param".equals(tag.getName())) { final PsiElement[] dataElements = tag.getDataElements(); final PsiDocTagValue valueElement = tag.getValueElement(); boolean hasProblemsWithTag = dataElements.length < 2; if (!hasProblemsWithTag) { final StringBuilder buf = new StringBuilder(); for (PsiElement element : dataElements) { if (element != valueElement){ buf.append(element.getText()); } } hasProblemsWithTag = buf.toString().trim().isEmpty(); } if (hasProblemsWithTag) { if (valueElement != null) { problems.add(createDescriptor(valueElement, InspectionsBundle.message("inspection.javadoc.method.problem.missing.tag.description", "@param " + valueElement.getText() + ""), manager, isOnTheFly)); } } } } } if (required && superMethods.length == 0 && isTagRequired(psiMethod, "@throws") && psiMethod.getThrowsList().getReferencedTypes().length > 0) { final Map declaredExceptions = new LinkedHashMap(); final PsiClassType[] classTypes = psiMethod.getThrowsList().getReferencedTypes(); for (PsiClassType classType : classTypes) { final PsiClass psiClass = classType.resolve(); if (psiClass != null){ declaredExceptions.put(classType, psiClass); } } processThrowsTags(tags, declaredExceptions, manager, problems, isOnTheFly); if (!declaredExceptions.isEmpty()) { for (PsiClassType declaredException : declaredExceptions.keySet()) { problems.add(createMissingThrowsTagDescriptor(docComment.getFirstChild(), manager, declaredException, isOnTheFly)); } } } ArrayList tagProblems = getTagValuesProblems(psiMethod, tags, manager, isOnTheFly); if (tagProblems != null) { problems.addAll(tagProblems); } checkForPeriodInDoc(psiMethod, docComment, problems, manager, isOnTheFly); checkForBadCharacters(docComment, problems, manager, isOnTheFly); for (PsiDocTag tag : tags) { if ("param".equals(tag.getName())) { if (extractTagDescription(tag).isEmpty()) { PsiDocTagValue value = tag.getValueElement(); if (value instanceof PsiDocParamRef) { PsiDocParamRef paramRef = (PsiDocParamRef)value; PsiParameter[] params = psiMethod.getParameterList().getParameters(); for (PsiParameter param : params) { if (paramRef.getReference().isReferenceTo(param)) { problems.add(createDescriptor(value, InspectionsBundle.message("inspection.javadoc.method.problem.descriptor", "@param", "" + param.getName() + ""), manager, isOnTheFly)); } } } } } else if ("return".equals(tag.getName()) && !myIgnoreEmptyDescriptions) { if (extractTagDescription(tag).isEmpty()) { String message = InspectionsBundle.message("inspection.javadoc.method.problem.missing.tag.description", "@return"); ProblemDescriptor descriptor = manager.createProblemDescriptor(tag.getNameElement(), message, (LocalQuickFix)null, ProblemHighlightType.GENERIC_ERROR_OR_WARNING, isOnTheFly); problems.add(descriptor); } } } checkDuplicateTags(tags, problems, manager, isOnTheFly); return problems.isEmpty() ? null : problems.toArray(new ProblemDescriptor[problems.size()]); } public static boolean isFound(final PsiDocTag[] tags, final PsiElement param) { for (PsiDocTag tag : tags) { if ("param".equals(tag.getName())) { PsiDocTagValue value = tag.getValueElement(); if (value instanceof PsiDocParamRef) { PsiDocParamRef paramRef = (PsiDocParamRef)value; final PsiReference psiReference = paramRef.getReference(); if (psiReference != null && psiReference.isReferenceTo(param)) { return true; } } } } return false; } private void processThrowsTags(@NotNull PsiDocTag[] tags, @NotNull Map declaredExceptions, @NotNull InspectionManager manager, @NotNull final List problems, boolean isOnTheFly) { for (PsiDocTag tag : tags) { if ("throws".equals(tag.getName()) || "exception".equals(tag.getName())) { final PsiDocTagValue value = tag.getValueElement(); if (value == null) continue; final PsiElement firstChild = value.getFirstChild(); if (firstChild == null) continue; final PsiElement psiElement = firstChild.getFirstChild(); if (!(psiElement instanceof PsiJavaCodeReferenceElement)) continue; final PsiJavaCodeReferenceElement ref = (PsiJavaCodeReferenceElement)psiElement; final PsiElement element = ref.resolve(); if (element instanceof PsiClass){ final PsiClass exceptionClass = (PsiClass)element; for (Iterator it = declaredExceptions.keySet().iterator(); it.hasNext();) { PsiClassType classType = it.next(); final PsiClass psiClass = declaredExceptions.get(classType); if (InheritanceUtil.isInheritorOrSelf(exceptionClass, psiClass, true)) { if (!myIgnoreEmptyDescriptions && extractThrowsTagDescription(tag).isEmpty()) { problems.add(createDescriptor(tag.getNameElement(), InspectionsBundle.message("inspection.javadoc.method.problem.missing.tag.description", "" + tag.getName() + ""), manager, isOnTheFly)); } it.remove(); } } } } } } @Nullable private static ProblemDescriptor createMissingThrowsTagDescriptor(final PsiElement elementToHighlight, final InspectionManager manager, final PsiClassType exceptionClassType, boolean isOnTheFly) { @NonNls String tag = "throws"; String message = InspectionsBundle.message("inspection.javadoc.problem.missing.tag", "@" + tag + " " + exceptionClassType.getCanonicalText()); final String firstDeclaredException = exceptionClassType.getCanonicalText(); return createDescriptor(elementToHighlight, message, new AddMissingTagFix(tag, firstDeclaredException), manager, isOnTheFly); } private static ProblemDescriptor createMissingTagDescriptor(PsiElement elementToHighlight, @NonNls String tag, final InspectionManager manager, boolean isOnTheFly) { String message = InspectionsBundle.message("inspection.javadoc.problem.missing.tag", "@" + tag + ""); return createDescriptor(elementToHighlight, message, new AddMissingTagFix(tag), manager, isOnTheFly); } private static ProblemDescriptor createMissingParamTagDescriptor(PsiElement elementToHighlight, PsiParameter param, final InspectionManager manager, boolean isOnTheFly) { String message = InspectionsBundle.message("inspection.javadoc.method.problem.missing.param.tag", "@param", "" + param.getName() + ""); return createDescriptor(elementToHighlight, message, new AddMissingParamTagFix(param.getName()), manager, isOnTheFly); } private static class AddMissingParamTagFix extends AddMissingTagFix { private final String myName; public AddMissingParamTagFix(String name) { super("param", name); myName = name; } @Override @NotNull public String getName() { return InspectionsBundle.message("inspection.javadoc.problem.add.param.tag", myName); } @Override @Nullable protected PsiElement getAnchor(ProblemDescriptor descriptor) { PsiElement element = descriptor.getPsiElement(); PsiElement parent = element == null ? null : element.getParent(); if (!(parent instanceof PsiDocComment)) return null; final PsiDocComment docComment = (PsiDocComment)parent; final PsiDocCommentOwner owner = docComment.getOwner(); if (!(owner instanceof PsiMethod)) return null; PsiParameter[] parameters = ((PsiMethod)owner).getParameterList().getParameters(); PsiParameter myParam = ContainerUtil.find(parameters, new Condition() { @Override public boolean value(PsiParameter psiParameter) { return myName.equals(psiParameter.getName()); } }); if (myParam == null) return null; PsiDocTag[] tags = docComment.findTagsByName("param"); if (tags.length == 0) { //insert as first tag or append to description tags = docComment.getTags(); if (tags.length == 0) return null; return tags[0]; } PsiParameter nextParam = PsiTreeUtil.getNextSiblingOfType(myParam, PsiParameter.class); while (nextParam != null) { for (PsiDocTag tag : tags) { if (matches(nextParam, tag)) { return tag; } } nextParam = PsiTreeUtil.getNextSiblingOfType(nextParam, PsiParameter.class); } PsiParameter prevParam = PsiTreeUtil.getPrevSiblingOfType(myParam, PsiParameter.class); while (prevParam != null) { for (PsiDocTag tag : tags) { if (matches(prevParam, tag)) { return PsiTreeUtil.getNextSiblingOfType(tag, PsiDocTag.class); } } prevParam = PsiTreeUtil.getPrevSiblingOfType(prevParam, PsiParameter.class); } return null; } private static boolean matches(final PsiParameter param, final PsiDocTag tag) { final PsiDocTagValue valueElement = tag.getValueElement(); return valueElement != null && valueElement.getText().trim().startsWith(param.getName()); } } private static String extractTagDescription(PsiDocTag tag) { StringBuilder buf = new StringBuilder(); PsiElement[] children = tag.getChildren(); for (PsiElement child : children) { if (child instanceof PsiDocToken) { PsiDocToken token = (PsiDocToken)child; if (token.getTokenType() == JavaDocTokenType.DOC_COMMENT_DATA) { buf.append(token.getText()); } } else if (child instanceof PsiDocTagValue) { buf.append(child.getText()); } else if (child instanceof PsiInlineDocTag) { buf.append(child.getText()); } } String s = buf.toString(); return s.trim(); } private static String extractThrowsTagDescription(PsiDocTag tag) { StringBuilder buf = new StringBuilder(); PsiElement[] children = tag.getChildren(); for (PsiElement child : children) { if (child instanceof PsiDocToken) { PsiDocToken token = (PsiDocToken)child; if (token.getTokenType() == JavaDocTokenType.DOC_COMMENT_DATA) { buf.append(token.getText()); } } } return buf.toString().trim(); } private static void checkForBadCharacters(PsiDocComment docComment, final List problems, final InspectionManager manager, final boolean onTheFly) { docComment.accept(new PsiRecursiveElementVisitor(){ @Override public void visitElement(PsiElement element) { super.visitElement(element); final ASTNode node = element.getNode(); if (node != null) { if (node.getElementType() == JavaDocTokenType.DOC_COMMENT_BAD_CHARACTER) { problems.add(manager.createProblemDescriptor(element, "Illegal character", (LocalQuickFix)null, ProblemHighlightType.GENERIC_ERROR_OR_WARNING, onTheFly)); } } } }); } private void checkForPeriodInDoc(PsiElement context, PsiDocComment docComment, List problems, InspectionManager manager, boolean onTheFly) { if (IGNORE_JAVADOC_PERIOD) return; PsiDocTag[] tags = docComment.getTags(); int dotIndex = docComment.getText().indexOf('.'); int tagOffset = 0; if (dotIndex >= 0) { //need to find first valid tag for (PsiDocTag tag : tags) { final String tagName = tag.getName(); final JavadocTagInfo tagInfo = JavadocManager.SERVICE.getInstance(tag.getProject()).getTagInfo(tagName); if (tagInfo != null && tagInfo.isValidInContext(context) && !tagInfo.isInline()) { tagOffset = tag.getTextOffset(); break; } } } if (dotIndex == -1 || tagOffset > 0 && dotIndex + docComment.getTextOffset() > tagOffset) { problems.add(manager.createProblemDescriptor(docComment.getFirstChild(), InspectionsBundle.message("inspection.javadoc.problem.descriptor1"), null, ProblemHighlightType.GENERIC_ERROR_OR_WARNING, onTheFly, false)); } } @Nullable private ArrayList getTagValuesProblems(PsiElement context, PsiDocTag[] tags, InspectionManager inspectionManager, boolean isOnTheFly) { final ArrayList problems = new ArrayList(2); for (PsiDocTag tag : tags) { final JavadocManager manager = JavadocManager.SERVICE.getInstance(tag.getProject()); String tagName = tag.getName(); JavadocTagInfo tagInfo = manager.getTagInfo(tagName); if (tagInfo == null || !tagInfo.isValidInContext(context)) { if (checkTagInfo(inspectionManager, tagInfo, tag, isOnTheFly, problems)) continue; } PsiDocTagValue value = tag.getValueElement(); if (tagInfo != null && !tagInfo.isValidInContext(context)) continue; String message = tagInfo == null ? null : tagInfo.checkTagValue(value); final PsiReference reference = value != null ? value.getReference() : null; if (message == null && reference != null) { PsiElement element = reference.resolve(); if (element == null) { final int textOffset = value.getTextOffset(); if (textOffset == value.getTextRange().getEndOffset()) { problems.add(inspectionManager.createProblemDescriptor(tag, InspectionsBundle.message("inspection.javadoc.problem.name.expected"), null, ProblemHighlightType.GENERIC_ERROR_OR_WARNING, isOnTheFly, true)); } } } if (message != null) { final PsiDocTagValue valueElement = tag.getValueElement(); if (valueElement == null){ problems.add(inspectionManager.createProblemDescriptor(tag, InspectionsBundle.message( "inspection.javadoc.method.problem.missing.tag.description", "" + tag.getName() + ""), (LocalQuickFix)null, ProblemHighlightType.GENERIC_ERROR_OR_WARNING, isOnTheFly)); } else { problems.add(createDescriptor(valueElement, message, inspectionManager, isOnTheFly)); } } checkInlineTags(inspectionManager, problems, tag.getDataElements(), manager, isOnTheFly); } return problems.isEmpty() ? null : problems; } private boolean checkTagInfo(InspectionManager inspectionManager, JavadocTagInfo tagInfo, PsiDocTag tag, boolean isOnTheFly, List problems) { final String tagName = tag.getName(); final StringTokenizer tokenizer = new StringTokenizer(myAdditionalJavadocTags, ", "); while (tokenizer.hasMoreTokens()) { if (Comparing.strEqual(tagName, tokenizer.nextToken())) return true; } final PsiElement nameElement = tag.getNameElement(); if (nameElement != null) { if (tagInfo == null) { problems.add( createDescriptor(nameElement, InspectionsBundle.message("inspection.javadoc.problem.wrong.tag", "" + tagName + ""), new AddUnknownTagToCustoms(tag.getName()), inspectionManager, isOnTheFly)); } else { problems.add(createDescriptor(nameElement, InspectionsBundle.message("inspection.javadoc.problem.disallowed.tag", "" + tagName + ""), new AddUnknownTagToCustoms(tag.getName()), inspectionManager, isOnTheFly)); } } return false; } private void checkInlineTags(@NotNull InspectionManager inspectionManager, @NotNull List problems, @NotNull PsiElement[] dataElements, @NotNull JavadocManager manager, boolean isOnTheFly) { for (PsiElement dataElement : dataElements) { if (dataElement instanceof PsiInlineDocTag) { final PsiInlineDocTag inlineDocTag = (PsiInlineDocTag)dataElement; final PsiElement nameElement = inlineDocTag.getNameElement(); if (manager.getTagInfo(inlineDocTag.getName()) == null) { checkTagInfo(inspectionManager, null, inlineDocTag, isOnTheFly, problems); } if (!IGNORE_POINT_TO_ITSELF) { final PsiDocTagValue value = inlineDocTag.getValueElement(); if (value != null) { final PsiReference reference = value.getReference(); if (reference != null) { final PsiElement ref = reference.resolve(); if (ref != null){ if (PsiTreeUtil.getParentOfType(inlineDocTag, PsiDocCommentOwner.class) == PsiTreeUtil.getParentOfType(ref, PsiDocCommentOwner.class, false)) { if (nameElement != null) { problems.add(createDescriptor(nameElement, InspectionsBundle.message("inspection.javadoc.problem.pointing.to.itself"), inspectionManager, isOnTheFly)); } } } } } } } } } @SuppressWarnings({"SimplifiableIfStatement"}) private boolean isTagRequired(PsiElement context, @NonNls String tag) { if (context instanceof PsiPackage) { return isTagRequired(PACKAGE_OPTIONS, tag); } if (context instanceof PsiClass) { if (PsiTreeUtil.getParentOfType(context, PsiClass.class) != null) { return isTagRequired(INNER_CLASS_OPTIONS, tag); } return isTagRequired(TOP_LEVEL_CLASS_OPTIONS, tag); } if (context instanceof PsiMethod) { return isTagRequired(METHOD_OPTIONS, tag); } if (context instanceof PsiField) { return isTagRequired(FIELD_OPTIONS, tag); } return false; } protected static boolean isTagRequired(Options options, String tag) { return options.REQUIRED_TAGS.contains(tag); } private boolean isJavaDocRequired(PsiModifierListOwner psiElement) { final RefJavaUtil refUtil = RefJavaUtil.getInstance(); int actualAccess = getAccessNumber(refUtil.getAccessModifier(psiElement)); if (psiElement instanceof PsiPackage) { return 1 <= getAccessNumber(PACKAGE_OPTIONS.ACCESS_JAVADOC_REQUIRED_FOR); } if (psiElement instanceof PsiClass) { PsiClass psiClass = (PsiClass)psiElement; if (PsiTreeUtil.getParentOfType(psiClass, PsiClass.class) != null) { return actualAccess <= getAccessNumber(INNER_CLASS_OPTIONS.ACCESS_JAVADOC_REQUIRED_FOR); } return actualAccess <= getAccessNumber(TOP_LEVEL_CLASS_OPTIONS.ACCESS_JAVADOC_REQUIRED_FOR); } if (psiElement instanceof PsiMethod) { psiElement = PsiTreeUtil.getParentOfType(psiElement, PsiClass.class); while (psiElement != null) { actualAccess = Math.max(actualAccess, getAccessNumber(refUtil.getAccessModifier(psiElement))); psiElement = PsiTreeUtil.getParentOfType(psiElement, PsiClass.class); } return actualAccess <= getAccessNumber(METHOD_OPTIONS.ACCESS_JAVADOC_REQUIRED_FOR); } if (psiElement instanceof PsiField) { psiElement = PsiTreeUtil.getParentOfType(psiElement, PsiClass.class); while (psiElement != null) { actualAccess = Math.max(actualAccess, getAccessNumber(refUtil.getAccessModifier(psiElement))); psiElement = PsiTreeUtil.getParentOfType(psiElement, PsiClass.class); } return actualAccess <= getAccessNumber(FIELD_OPTIONS.ACCESS_JAVADOC_REQUIRED_FOR); } return false; } private void checkDuplicateTags(final PsiDocTag[] tags, List problems, final InspectionManager manager, boolean isOnTheFly) { Set documentedParamNames = null; Set documentedExceptions = null; Set uniqueTags = null; for(PsiDocTag tag: tags) { if ("param".equals(tag.getName())) { PsiDocTagValue value = tag.getValueElement(); if (value instanceof PsiDocParamRef) { PsiDocParamRef paramRef = (PsiDocParamRef)value; final PsiReference reference = paramRef.getReference(); if (reference != null) { final String paramName = reference.getCanonicalText(); if (documentedParamNames == null) { documentedParamNames = new HashSet(); } if (documentedParamNames.contains(paramName)) { problems.add(createDescriptor(tag.getNameElement(), InspectionsBundle.message("inspection.javadoc.problem.duplicate.param", paramName), manager, isOnTheFly)); } documentedParamNames.add(paramName); } } } else if (!myIgnoreDuplicatedThrows && ("throws".equals(tag.getName()) || "exception".equals(tag.getName()))) { PsiDocTagValue value = tag.getValueElement(); if (value != null) { final PsiElement firstChild = value.getFirstChild(); if (firstChild != null && firstChild.getFirstChild() instanceof PsiJavaCodeReferenceElement) { PsiJavaCodeReferenceElement refElement = (PsiJavaCodeReferenceElement) firstChild.getFirstChild(); if (refElement != null) { PsiElement element = refElement.resolve(); if (element instanceof PsiClass) { String fqName = ((PsiClass)element).getQualifiedName(); if (documentedExceptions == null) { documentedExceptions = new HashSet(); } if (documentedExceptions.contains(fqName)) { problems.add(createDescriptor(tag.getNameElement(), InspectionsBundle.message("inspection.javadoc.problem.duplicate.throws", fqName), manager, isOnTheFly)); } documentedExceptions.add(fqName); } } } } } else if (ourUniqueTags.contains(tag.getName())) { if (uniqueTags == null) { uniqueTags = new HashSet(); } if (uniqueTags.contains(tag.getName())) { problems.add(createDescriptor(tag.getNameElement(), InspectionsBundle.message("inspection.javadoc.problem.duplicate.tag", tag.getName()), manager, isOnTheFly)); } uniqueTags.add(tag.getName()); } } } private static int getAccessNumber(@NonNls String accessModifier) { if (accessModifier.startsWith("none")) return 0; if (accessModifier.startsWith("public")) return 1; if (accessModifier.startsWith("protected")) return 2; if (accessModifier.startsWith("package")) return 3; if (accessModifier.startsWith("private")) return 4; return 5; } @Override @NotNull public String getDisplayName() { return InspectionsBundle.message("inspection.javadoc.display.name"); } @Override @NotNull public String getGroupDisplayName() { return InspectionsBundle.message("group.names.javadoc.issues"); } @Override @NotNull public String getShortName() { return SHORT_NAME; } @Nullable @Override public String getAlternativeID() { return "javadoc"; } public void setIgnoreEmptyDescriptions(boolean ignoreEmptyDescriptions) { myIgnoreEmptyDescriptions = ignoreEmptyDescriptions; } private class AddUnknownTagToCustoms implements LocalQuickFix { private final String myTag; public AddUnknownTagToCustoms(String tag) { myTag = tag; } @Override @NotNull public String getName() { return QuickFixBundle.message("add.doctag.to.custom.tags", myTag); } @Override @NotNull public String getFamilyName() { return QuickFixBundle.message("fix.javadoc.family"); } @Override public void applyFix(@NotNull Project project, @NotNull ProblemDescriptor descriptor) { if (myTag == null) return; if (!myAdditionalJavadocTags.isEmpty()) { myAdditionalJavadocTags += "," + myTag; } else { myAdditionalJavadocTags = myTag; } final InspectionProfile inspectionProfile = InspectionProjectProfileManager.getInstance(project).getInspectionProfile(); //correct save settings InspectionProfileManager.getInstance().fireProfileChanged(inspectionProfile); //TODO lesya /* try { inspectionProfile.save(); } catch (IOException e) { Messages.showErrorDialog(project, e.getMessage(), CommonBundle.getErrorTitle()); } */ } } }