/* * Copyright (c) 2005 Jet Brains. All Rights Reserved. */ package com.intellij.codeInspection.javaDoc; import com.intellij.ExtensionPoints; import com.intellij.codeInsight.CodeInsightUtil; import com.intellij.codeInsight.daemon.HighlightDisplayKey; import com.intellij.codeInsight.daemon.QuickFixBundle; import com.intellij.codeInspection.*; import com.intellij.codeInspection.ex.BaseLocalInspectionTool; import com.intellij.codeInspection.reference.RefJavaUtil; import com.intellij.lang.ASTNode; import com.intellij.openapi.diagnostic.Logger; import com.intellij.openapi.editor.Editor; import com.intellij.openapi.editor.ScrollType; import com.intellij.openapi.extensions.ExtensionPoint; import com.intellij.openapi.extensions.Extensions; import com.intellij.openapi.fileEditor.FileEditorManager; import com.intellij.openapi.project.Project; import com.intellij.openapi.util.*; 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.impl.source.jsp.jspJava.JspClass; import com.intellij.psi.impl.source.jsp.jspJava.JspHolderMethod; import com.intellij.psi.javadoc.*; import com.intellij.psi.util.InheritanceUtil; import com.intellij.psi.util.PsiTreeUtil; import com.intellij.ui.DocumentAdapter; import com.intellij.ui.FieldPanel; import com.intellij.ui.Gray; import com.intellij.ui.IdeBorderFactory; import com.intellij.ui.components.JBTabbedPane; import com.intellij.util.IJSwingUtilities; import com.intellij.util.IncorrectOperationException; import com.intellij.util.containers.ContainerUtil; import com.intellij.util.ui.UIUtil; import org.jdom.Element; import org.jetbrains.annotations.NonNls; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; import javax.swing.*; import javax.swing.event.ChangeEvent; import javax.swing.event.ChangeListener; import javax.swing.event.DocumentEvent; import javax.swing.text.BadLocationException; import javax.swing.text.Document; import java.awt.*; import java.awt.event.ActionEvent; import java.awt.event.ActionListener; import java.util.*; public class JavaDocLocalInspection extends BaseLocalInspectionTool { private static final String REQUIRED_JAVADOC_IS_ABSENT = InspectionsBundle.message("inspection.javadoc.problem.descriptor"); @NonNls private static final String NONE = "none"; @NonNls private static final String PUBLIC = "public"; @NonNls private static final String PROTECTED = "protected"; @NonNls private static final String PACKAGE_LOCAL = "package"; @NonNls private static final String PRIVATE = "private"; @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"); } 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; } public void readExternal(Element element) throws InvalidDataException { DefaultJDOMExternalizer.readExternal(this, element); } public void writeExternal(Element element) throws WriteExternalException { DefaultJDOMExternalizer.writeExternal(this, element); } } @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; public boolean IGNORE_DUPLICATED_THROWS = false; public boolean IGNORE_POINT_TO_ITSELF = false; public String myAdditionalJavadocTags = ""; private static final Logger LOG = Logger.getInstance("com.intellij.codeInspection.javaDoc.JavaDocLocalInspection"); private class OptionsPanel extends JPanel { private JPanel createOptionsPanel(String[] modifiers, String[] tags, Options options) { JPanel pane = new JPanel(new GridLayout(1, tags == null ? 1 : 2)); pane.add(createScopePanel(modifiers, options)); if (tags != null) { pane.add(createTagsPanel(tags, options)); } pane.validate(); return pane; } private JPanel createTagsPanel(String[] tags, Options options) { JPanel panel = new JPanel(new GridBagLayout()); panel.setBorder(BorderFactory.createCompoundBorder(IdeBorderFactory.createTitledBorder( InspectionsBundle.message("inspection.javadoc.required.tags.option.title"), true), BorderFactory.createEmptyBorder(0, 3, 3, 3))); GridBagConstraints gc = new GridBagConstraints(); gc.weightx = 1; gc.weighty = 0; gc.fill = GridBagConstraints.HORIZONTAL; gc.anchor = GridBagConstraints.NORTHWEST; for (int i = 0; i < tags.length; i++) { JCheckBox box = new JCheckBox(tags[i]); gc.gridy = i; if (i == tags.length - 1) gc.weighty = 1; panel.add(box, gc); box.setSelected(isTagRequired(options, tags[i])); box.addChangeListener(new MyChangeListener(box, options, tags[i])); } return panel; } private class MyChangeListener implements ChangeListener { private final JCheckBox myCheckBox; private final Options myOptions; private final String myTagName; public MyChangeListener(JCheckBox checkBox, Options options, String tagName) { myCheckBox = checkBox; myOptions = options; myTagName = tagName; } public void stateChanged(ChangeEvent e) { if (myCheckBox.isSelected()) { if (!isTagRequired(myOptions,myTagName)) { myOptions.REQUIRED_TAGS += myTagName; } } else { myOptions.REQUIRED_TAGS = myOptions.REQUIRED_TAGS.replaceAll(myTagName, ""); } } } private JPanel createScopePanel(final String[] modifiers, final Options options) { JPanel panel = new JPanel(new BorderLayout()); panel.setBorder(BorderFactory.createCompoundBorder(IdeBorderFactory.createTitledBorder( InspectionsBundle.message("inspection.scope.for.title"), true), BorderFactory.createEmptyBorder(0, 3, 3, 3))); final Hashtable sliderLabels = new Hashtable(); for (int i = 0; i < modifiers.length; i++) { sliderLabels.put(i + 1, new JLabel(modifiers[i])); } final JSlider slider = new JSlider(SwingConstants.VERTICAL, 1, modifiers.length, 1); slider.setLabelTable(sliderLabels); slider.putClientProperty(UIUtil.JSLIDER_ISFILLED, Boolean.TRUE); slider.setPreferredSize(new Dimension(80, 50)); slider.setPaintLabels(true); slider.setSnapToTicks(true); slider.addChangeListener(new ChangeListener() { public void stateChanged(ChangeEvent e) { int value = slider.getValue(); options.ACCESS_JAVADOC_REQUIRED_FOR = modifiers[value - 1]; for (Integer key : sliderLabels.keySet()) { sliderLabels.get(key).setForeground(key.intValue() <= value ? Color.black : Gray._100); } } }); Color fore = Color.black; for (int i = 0; i < modifiers.length; i++) { sliderLabels.get(i + 1).setForeground(fore); if (modifiers[i].equals(options.ACCESS_JAVADOC_REQUIRED_FOR)) { slider.setValue(i + 1); fore = Gray._100; } } panel.add(slider, BorderLayout.WEST); return panel; } public OptionsPanel() { super(new GridBagLayout()); GridBagConstraints gc = new GridBagConstraints(0, GridBagConstraints.RELATIVE, 2, 1, 1, 0, GridBagConstraints.NORTH, GridBagConstraints.HORIZONTAL, new Insets(0,0,0,0),0,0 ); add(createAdditionalJavadocTagsPanel(), gc); JTabbedPane tabs = new JBTabbedPane(SwingConstants.BOTTOM); @NonNls String[] tags = new String[]{"@author", "@version", "@since", "@param"}; tabs.add(InspectionsBundle.message("inspection.javadoc.option.tab.title"), createOptionsPanel(new String[]{NONE, PUBLIC, PACKAGE_LOCAL}, tags, TOP_LEVEL_CLASS_OPTIONS)); tags = new String[]{"@return", "@param", InspectionsBundle.message("inspection.javadoc.throws.or.exception.option")}; tabs.add(InspectionsBundle.message("inspection.javadoc.option.tab.title.method"), createOptionsPanel(new String[]{NONE, PUBLIC, PROTECTED, PACKAGE_LOCAL, PRIVATE}, tags, METHOD_OPTIONS)); tabs.add(InspectionsBundle.message("inspection.javadoc.option.tab.title.field"), createOptionsPanel(new String[]{NONE, PUBLIC, PROTECTED, PACKAGE_LOCAL, PRIVATE}, null, FIELD_OPTIONS)); tabs.add(InspectionsBundle.message("inspection.javadoc.option.tab.title.inner.class"), createOptionsPanel(new String[]{NONE, PUBLIC, PROTECTED, PACKAGE_LOCAL, PRIVATE}, null, INNER_CLASS_OPTIONS)); add(tabs, gc); final JCheckBox checkBox = new JCheckBox(InspectionsBundle.message("inspection.javadoc.option.ignore.deprecated"), IGNORE_DEPRECATED); checkBox.addActionListener(new ActionListener() { public void actionPerformed(ActionEvent e) { IGNORE_DEPRECATED = checkBox.isSelected(); } }); gc.gridwidth = 1; add(checkBox, gc); final JCheckBox periodCheckBox = new JCheckBox(InspectionsBundle.message("inspection.javadoc.option.ignore.period"), IGNORE_JAVADOC_PERIOD); periodCheckBox.addActionListener(new ActionListener() { public void actionPerformed(ActionEvent e) { IGNORE_JAVADOC_PERIOD = periodCheckBox.isSelected(); } }); add(periodCheckBox, gc); final JCheckBox ignoreDuplicateThrowsCheckBox = new JCheckBox("Ignore duplicate throws tag", IGNORE_DUPLICATED_THROWS); ignoreDuplicateThrowsCheckBox.addActionListener(new ActionListener() { public void actionPerformed(ActionEvent e) { IGNORE_DUPLICATED_THROWS = ignoreDuplicateThrowsCheckBox.isSelected(); } }); add(ignoreDuplicateThrowsCheckBox, gc); final JCheckBox ignorePointToItselfCheckBox = new JCheckBox("Ignore javadoc pointing to itself", IGNORE_POINT_TO_ITSELF); ignorePointToItselfCheckBox.addActionListener(new ActionListener() { public void actionPerformed(ActionEvent e) { IGNORE_POINT_TO_ITSELF = ignorePointToItselfCheckBox.isSelected(); } }); add(ignorePointToItselfCheckBox, gc); } public FieldPanel createAdditionalJavadocTagsPanel(){ FieldPanel additionalTagsPanel = new FieldPanel(InspectionsBundle.message("inspection.javadoc.label.text"), InspectionsBundle.message("inspection.javadoc.dialog.title"), null, null); additionalTagsPanel.setPreferredSize(new Dimension(150, additionalTagsPanel.getPreferredSize().height)); additionalTagsPanel.getTextField().getDocument().addDocumentListener(new DocumentAdapter() { protected void textChanged(DocumentEvent e) { final Document document = e.getDocument(); try { final String text = document.getText(0, document.getLength()); if (text != null) { myAdditionalJavadocTags = text.trim(); } } catch (BadLocationException e1) { LOG.error(e1); } } }); additionalTagsPanel.setText(myAdditionalJavadocTags); return additionalTagsPanel; } } public JComponent createOptionsPanel() { return new OptionsPanel(); } private static ProblemDescriptor createDescriptor(@NotNull PsiElement element, String template, InspectionManager manager, boolean onTheFly) { return manager.createProblemDescriptor(element, template, onTheFly, (LocalQuickFix [])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 String tag, String value) { myTag = tag; myValue = value; } public AddMissingTagFix(String tag) { this(tag, ""); } @NotNull public String getName() { return InspectionsBundle.message("inspection.javadoc.problem.add.tag", myTag, myValue); } public void applyFix(@NotNull Project project, @NotNull ProblemDescriptor descriptor) { final PsiElementFactory factory = JavaPsiFacade.getInstance(project).getElementFactory(); try { final PsiDocCommentOwner owner = PsiTreeUtil.getParentOfType(descriptor.getEndElement(), PsiDocCommentOwner.class); if (owner != null) { if (!CodeInsightUtil.preparePsiElementsForWrite(owner)) return; final PsiDocComment docComment = owner.getDocComment(); final PsiDocTag tag = factory.createDocTagFromText("@" + myTag + " " + myValue); if (docComment != null) { PsiElement addedTag; final PsiElement anchor = getAnchor(descriptor); if (anchor != null) { addedTag = docComment.addBefore(tag, anchor); } else { addedTag = docComment.add(tag); } moveCaretTo(addedTag); } } } catch (IncorrectOperationException e) { LOG.error(e); } } @Nullable protected PsiElement getAnchor(ProblemDescriptor descriptor) { return null; } private static void moveCaretTo(final PsiElement newCaretPosition) { Project project = newCaretPosition.getProject(); final PsiFile psiFile = newCaretPosition.getContainingFile(); final Editor editor = FileEditorManager.getInstance(project).getSelectedTextEditor(); if (editor != null && IJSwingUtilities.hasFocus(editor.getComponent())) { final PsiFile file = PsiDocumentManager.getInstance(project).getPsiFile(editor.getDocument()); if (file == psiFile) { editor.getCaretModel().moveToOffset(newCaretPosition.getTextRange().getEndOffset()); editor.getScrollingModel().scrollToCaret(ScrollType.RELATIVE); } } } @NotNull public String getFamilyName() { return InspectionsBundle.message("inspection.javadoc.problem.add.tag.family"); } } @Nullable public ProblemDescriptor[] checkClass(@NotNull PsiClass psiClass, @NotNull InspectionManager manager, boolean isOnTheFly) { if (psiClass instanceof PsiAnonymousClass) return null; if (psiClass instanceof JspClass) 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(); @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(psiClass, 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)); } } } ArrayList tagProblems = getTagValuesProblems(psiClass, tags, manager, isOnTheFly); if (tagProblems != null) { problems.addAll(tagProblems); } checkForPeriodInDoc(docComment, problems, manager, isOnTheFly); checkInlineTags(manager, problems, docComment.getDescriptionElements(), JavadocManager.SERVICE.getInstance(docComment.getProject()), isOnTheFly); checkForBadCharacters(docComment, problems, 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).length() == 0) { problems.add(createDescriptor(elementToHighlight, InspectionsBundle.message(absentDescriptionKeys[i]), 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(nameIdentifier, psiTypeParameter, manager, isOnTheFly)); } } } return problems.isEmpty() ? null : problems.toArray(new ProblemDescriptor[problems.size()]); } private static ProblemDescriptor createMissingParamTagDescriptor(final PsiIdentifier nameIdentifier, final PsiTypeParameter psiTypeParameter, final InspectionManager manager, boolean isOnTheFly) { String message = InspectionsBundle.message("inspection.javadoc.problem.missing.tag", "@param"); return createDescriptor(nameIdentifier, message, new AddMissingTagFix("param", "<" + psiTypeParameter.getName() + ">"), manager, isOnTheFly); } @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(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()]); } @Nullable public ProblemDescriptor[] checkMethod(@NotNull PsiMethod psiMethod, @NotNull InspectionManager manager, boolean isOnTheFly) { if (psiMethod instanceof JspHolderMethod) return null; if (IGNORE_DEPRECATED && (psiMethod.isDeprecated() || psiMethod.getContainingClass().isDeprecated())) { 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(ExtensionPoints.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; } } final ArrayList 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 != 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) { final PsiIdentifier psiIdentifier = psiMethod.getNameIdentifier(); if (psiIdentifier != null) { problems.add(createMissingTagDescriptor(psiIdentifier, "return", manager, isOnTheFly)); } } if (absentParameters != null) { for (PsiParameter psiParameter : absentParameters) { final PsiIdentifier nameIdentifier = psiMethod.getNameIdentifier(); if (nameIdentifier != null) { problems.add(createMissingParamTagDescriptor(nameIdentifier, psiParameter, manager, isOnTheFly)); } } } 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().length() == 0; } 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 HashMap(); 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(psiMethod, manager, declaredException, isOnTheFly)); } } } ArrayList tagProblems = getTagValuesProblems(psiMethod, tags, manager, isOnTheFly); if (tagProblems != null) { problems.addAll(tagProblems); } checkForPeriodInDoc(docComment, problems, manager, isOnTheFly); checkForBadCharacters(docComment, problems, manager, isOnTheFly); for (PsiDocTag tag : tags) { if ("param".equals(tag.getName())) { if (extractTagDescription(tag).length() == 0) { 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())) { if (extractTagDescription(tag).length() == 0) { 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 static void processThrowsTags(final PsiDocTag[] tags, final Map declaredExceptions, final InspectionManager mananger, @NotNull final ArrayList 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 (extractThrowsTagDescription(tag).length() == 0) { problems.add(createDescriptor(tag.getNameElement(), InspectionsBundle.message("inspection.javadoc.method.problem.missing.tag.description", "" + tag.getName() + ""), mananger, isOnTheFly)); } it.remove(); } } } } } } @Nullable private static ProblemDescriptor createMissingThrowsTagDescriptor(final PsiMethod method, 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(); final PsiIdentifier nameIdentifier = method.getNameIdentifier(); return nameIdentifier != null ? createDescriptor(nameIdentifier, message,new AddMissingTagFix(tag, firstDeclaredException), manager, isOnTheFly) : null; } 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; } @NotNull public String getName() { return InspectionsBundle.message("inspection.javadoc.problem.add.param.tag", myName); } @Nullable protected PsiElement getAnchor(ProblemDescriptor descriptor) { PsiElement element = descriptor.getPsiElement(); PsiElement parent = element == null ? null : element.getParent(); if (!(parent instanceof PsiMethod)) return null; PsiParameter[] parameters = ((PsiMethod)parent).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; final PsiMethod psiMethod = PsiTreeUtil.getParentOfType(myParam, PsiMethod.class); LOG.assertTrue(psiMethod != null); final PsiDocComment docComment = psiMethod.getDocComment(); LOG.assertTrue(docComment != 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 void checkForBadCharacters(PsiDocComment docComment, final ArrayList 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(PsiDocComment docComment, ArrayList 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 final PsiDocCommentOwner owner = PsiTreeUtil.getParentOfType(docComment, PsiDocCommentOwner.class); for (PsiDocTag tag : tags) { final String tagName = tag.getName(); final JavadocTagInfo tagInfo = JavadocManager.SERVICE.getInstance(tag.getProject()).getTagInfo(tagName); if (tagInfo != null && tagInfo.isValidInContext(owner) && !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(PsiDocCommentOwner context, PsiDocTag[] tags, InspectionManager inspectionManager, boolean isOnTheFly) { final ArrayList problems = new ArrayList(2); nextTag: 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 nextTag; } PsiDocTagValue value = tag.getValueElement(); final JavadocTagInfo info = manager.getTagInfo(tagName); if (info != null && !info.isValidInContext(context)) continue; String message = info == null ? null : info.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() + ""), null, ProblemHighlightType.GENERIC_ERROR_OR_WARNING, isOnTheFly, true)); } 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, ArrayList 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(final InspectionManager inspectionManager, final ArrayList problems, final PsiElement[] dataElements, final 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 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; } private 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 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, ArrayList 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 (!IGNORE_DUPLICATED_THROWS && ("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 (JavaDocLocalInspection.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; } @NotNull public String getDisplayName() { return InspectionsBundle.message("inspection.javadoc.display.name"); } @NotNull public String getGroupDisplayName() { return InspectionsBundle.message("group.names.javadoc.issues"); } @NotNull public String getShortName() { return SHORT_NAME; } private class AddUnknownTagToCustoms implements LocalQuickFix { private final String myTag; public AddUnknownTagToCustoms(String tag) { myTag = tag; } @NotNull public String getName() { return QuickFixBundle.message("add.doctag.to.custom.tags", myTag); } @NotNull public String getFamilyName() { return QuickFixBundle.message("fix.javadoc.family"); } public void applyFix(@NotNull Project project, @NotNull ProblemDescriptor descriptor) { if (myTag == null) return; if (myAdditionalJavadocTags.length() > 0) { myAdditionalJavadocTags += "," + myTag; } else { myAdditionalJavadocTags = myTag; } final InspectionProfile inspectionProfile = InspectionProjectProfileManager.getInstance(project).getInspectionProfile(); //correct save settings ((ModifiableModel)inspectionProfile).isProperSetting(HighlightDisplayKey.find(SHORT_NAME)); InspectionProfileManager.getInstance().fireProfileChanged(inspectionProfile); //TODO lesya /* try { inspectionProfile.save(); } catch (IOException e) { Messages.showErrorDialog(project, e.getMessage(), CommonBundle.getErrorTitle()); } */ } } }