blob: 36d7bc2f17ef1e1ea567856d5acbe45b61412116 [file] [log] [blame]
/*
* 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<String> ourUniqueTags = new HashSet<String>();
@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<ProblemDescriptor> problems =
getRequiredTagProblems(aPackage, docComment.getFirstChild(), tags, manager, isOnTheFly, required);
final List<ProblemDescriptor> 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<ProblemDescriptor> problems = getRequiredTagProblems(psiClass, docComment.getFirstChild(), tags, manager, isOnTheFly, required);
List<ProblemDescriptor> 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<PsiTypeParameter> absentParameters = null;
final PsiTypeParameter[] typeParameters = psiClass.getTypeParameters();
for (PsiTypeParameter typeParameter : typeParameters) {
if (!isFound(tags, typeParameter)) {
if (absentParameters == null) absentParameters = new ArrayList<PsiTypeParameter>(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<ProblemDescriptor> 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<ProblemDescriptor> problems = new ArrayList<ProblemDescriptor>(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", "<code>@param</code>");
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<ProblemDescriptor> problems = new ArrayList<ProblemDescriptor>(2);
ArrayList<ProblemDescriptor> 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<Condition<PsiMember>> point = Extensions.getRootArea().getExtensionPoint(ToolExtensionPoints.JAVADOC_LOCAL);
final Condition<PsiMember>[] addins = point.getExtensions();
for (Condition<PsiMember> 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<ProblemDescriptor> problems = new ArrayList<ProblemDescriptor>(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<PsiParameter> 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<PsiParameter>(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", "<code>@param " + valueElement.getText() + "</code>"),
manager, isOnTheFly));
}
}
}
}
}
if (required && superMethods.length == 0 && isTagRequired(psiMethod, "@throws") && psiMethod.getThrowsList().getReferencedTypes().length > 0) {
final Map<PsiClassType, PsiClass> declaredExceptions = new LinkedHashMap<PsiClassType, PsiClass>();
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<ProblemDescriptor> 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", "<code>@param</code>", "<code>" + param.getName() + "</code>"),
manager, isOnTheFly));
}
}
}
}
}
else
if ("return".equals(tag.getName()) && !myIgnoreEmptyDescriptions) {
if (extractTagDescription(tag).isEmpty()) {
String message = InspectionsBundle.message("inspection.javadoc.method.problem.missing.tag.description", "<code>@return</code>");
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<PsiClassType, PsiClass> declaredExceptions,
@NotNull InspectionManager manager,
@NotNull final List<ProblemDescriptor> 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<PsiClassType> 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", "<code>" + tag.getName() + "</code>"), 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", "<code>@" + tag + "</code> " + 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", "<code>@" + tag + "</code>");
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", "<code>@param</code>", "<code>" + param.getName() + "</code>");
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<PsiParameter>() {
@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<ProblemDescriptor> 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<ProblemDescriptor> 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<ProblemDescriptor> getTagValuesProblems(PsiElement context, PsiDocTag[] tags, InspectionManager inspectionManager,
boolean isOnTheFly) {
final ArrayList<ProblemDescriptor> problems = new ArrayList<ProblemDescriptor>(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", "<code>" + tag.getName() + "</code>"), (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<ProblemDescriptor> 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", "<code>" + tagName + "</code>"),
new AddUnknownTagToCustoms(tag.getName()), inspectionManager, isOnTheFly));
}
else {
problems.add(createDescriptor(nameElement, InspectionsBundle.message("inspection.javadoc.problem.disallowed.tag",
"<code>" + tagName + "</code>"),
new AddUnknownTagToCustoms(tag.getName()), inspectionManager, isOnTheFly));
}
}
return false;
}
private void checkInlineTags(@NotNull InspectionManager inspectionManager,
@NotNull List<ProblemDescriptor> 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<ProblemDescriptor> problems,
final InspectionManager manager, boolean isOnTheFly) {
Set<String> documentedParamNames = null;
Set<String> documentedExceptions = null;
Set<String> 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<String>();
}
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<String>();
}
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<String>();
}
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());
}
*/
}
}
}