blob: 86505486a1ab790573e43e9f0a6d82947ba4b3d3 [file] [log] [blame]
/*
* Copyright 2006 Sascha Weinreuter
*
* 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 org.intellij.plugins.intelliLang.pattern;
import com.intellij.codeInspection.LocalInspectionTool;
import com.intellij.codeInspection.LocalQuickFix;
import com.intellij.codeInspection.ProblemDescriptor;
import com.intellij.codeInspection.ProblemsHolder;
import com.intellij.ide.DataManager;
import com.intellij.openapi.actionSystem.DataContext;
import com.intellij.openapi.project.Project;
import com.intellij.openapi.util.AsyncResult;
import com.intellij.openapi.util.Comparing;
import com.intellij.openapi.util.Key;
import com.intellij.openapi.util.text.StringUtil;
import com.intellij.psi.*;
import com.intellij.psi.util.CachedValue;
import com.intellij.psi.util.CachedValueProvider;
import com.intellij.psi.util.CachedValuesManager;
import com.intellij.psi.util.PsiTreeUtil;
import com.intellij.refactoring.JavaRefactoringActionHandlerFactory;
import com.intellij.refactoring.RefactoringActionHandler;
import com.intellij.util.Consumer;
import com.intellij.util.SmartList;
import org.intellij.plugins.intelliLang.Configuration;
import org.intellij.plugins.intelliLang.util.AnnotateFix;
import org.intellij.plugins.intelliLang.util.AnnotationUtilEx;
import org.intellij.plugins.intelliLang.util.PsiUtilEx;
import org.intellij.plugins.intelliLang.util.SubstitutedExpressionEvaluationHelper;
import org.jetbrains.annotations.NonNls;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import javax.swing.*;
import java.awt.*;
import java.awt.event.ItemEvent;
import java.awt.event.ItemListener;
import java.text.MessageFormat;
import java.util.List;
import java.util.regex.Pattern;
import java.util.regex.PatternSyntaxException;
/**
* Inspection that validates if string literals, compile-time constants or
* substituted expressions match the pattern of the context they're used in.
*/
public class PatternValidator extends LocalInspectionTool {
private static final Key<CachedValue<Pattern>> COMPLIED_PATTERN = Key.create("COMPILED_PATTERN");
public static final String PATTERN_VALIDATION = "Pattern Validation";
public static final String LANGUAGE_INJECTION = "Language Injection";
public boolean CHECK_NON_CONSTANT_VALUES = true;
private final Configuration myConfiguration;
public PatternValidator() {
myConfiguration = Configuration.getInstance();
}
@Override
public boolean isEnabledByDefault() {
return true;
}
@Override
@NotNull
public String getGroupDisplayName() {
return PATTERN_VALIDATION;
}
@Override
@NotNull
public String getDisplayName() {
return "Validate Annotated Patterns";
}
@Override
@Nullable
public JComponent createOptionsPanel() {
final JPanel jPanel = new JPanel(new BorderLayout());
final JCheckBox jCheckBox = new JCheckBox("Flag non compile-time constant expressions");
jCheckBox.setToolTipText(
"If checked, the inspection will flag expressions with unknown values " + "and offer to add a substitution (@Subst) annotation");
jCheckBox.setSelected(CHECK_NON_CONSTANT_VALUES);
jCheckBox.addItemListener(new ItemListener() {
@Override
public void itemStateChanged(ItemEvent e) {
CHECK_NON_CONSTANT_VALUES = jCheckBox.isSelected();
}
});
jPanel.add(jCheckBox, BorderLayout.NORTH);
return jPanel;
}
@Override
@NotNull
@NonNls
public String getShortName() {
return "PatternValidation";
}
@Override
@NotNull
public PsiElementVisitor buildVisitor(@NotNull final ProblemsHolder holder, boolean isOnTheFly) {
return new JavaElementVisitor() {
@Override
public final void visitReferenceExpression(PsiReferenceExpression expression) {
visitExpression(expression);
}
@Override
public void visitExpression(PsiExpression expression) {
final PsiElement element = expression.getParent();
if (element instanceof PsiExpressionList) {
// this checks method arguments
check(expression, holder, false);
}
else if (element instanceof PsiNameValuePair) {
final PsiNameValuePair valuePair = (PsiNameValuePair)element;
final String name = valuePair.getName();
if (name == null || name.equals(PsiAnnotation.DEFAULT_REFERENCED_METHOD_NAME)) {
// check whether @Subst complies with pattern
check(expression, holder, true);
}
}
}
@Override
public void visitReturnStatement(PsiReturnStatement statement) {
final PsiExpression returnValue = statement.getReturnValue();
if (returnValue != null) {
check(returnValue, holder, false);
}
}
@Override
public void visitVariable(PsiVariable var) {
final PsiExpression initializer = var.getInitializer();
if (initializer != null) {
// variable/field initializer
check(initializer, holder, false);
}
}
@Override
public void visitAssignmentExpression(PsiAssignmentExpression expression) {
final PsiExpression e = expression.getRExpression();
if (e != null) {
check(e, holder, false);
}
visitExpression(expression);
}
private void check(@NotNull PsiExpression expression, ProblemsHolder holder, boolean isAnnotationValue) {
if (expression instanceof PsiConditionalExpression) {
final PsiConditionalExpression expr = (PsiConditionalExpression)expression;
PsiExpression e = expr.getThenExpression();
if (e != null) {
check(e, holder, isAnnotationValue);
}
e = expr.getElseExpression();
if (e != null) {
check(e, holder, isAnnotationValue);
}
}
else {
final PsiType type = expression.getType();
// optimiziation: only check expressions of type String
if (type != null && PsiUtilEx.isString(type)) {
final PsiModifierListOwner element;
if (isAnnotationValue) {
final PsiAnnotation psiAnnotation = PsiTreeUtil.getParentOfType(expression, PsiAnnotation.class);
if (psiAnnotation != null && myConfiguration.getAdvancedConfiguration().getSubstAnnotationClass().equals(psiAnnotation.getQualifiedName())) {
element = PsiTreeUtil.getParentOfType(expression, PsiModifierListOwner.class);
}
else {
return;
}
}
else {
element = AnnotationUtilEx.getAnnotatedElementFor(expression, AnnotationUtilEx.LookupType.PREFER_CONTEXT);
}
if (element != null && PsiUtilEx.isLanguageAnnotationTarget(element)) {
PsiAnnotation[] annotations = AnnotationUtilEx.getAnnotationFrom(element, myConfiguration.getAdvancedConfiguration().getPatternAnnotationPair(), true);
checkExpression(expression, annotations, holder);
}
}
}
}
};
}
private void checkExpression(PsiExpression expression, final PsiAnnotation[] annotations, ProblemsHolder holder) {
if (annotations.length == 0) return;
final PsiAnnotation psiAnnotation = annotations[0];
// cache compiled pattern with annotation
CachedValue<Pattern> p = psiAnnotation.getUserData(COMPLIED_PATTERN);
if (p == null) {
final CachedValueProvider<Pattern> provider = new CachedValueProvider<Pattern>() {
@Override
public Result<Pattern> compute() {
final String pattern = AnnotationUtilEx.calcAnnotationValue(psiAnnotation, "value");
Pattern p = null;
if (pattern != null) {
try {
p = Pattern.compile(pattern);
}
catch (PatternSyntaxException e) {
// pattern stays null
}
}
return Result.create(p, (Object[])annotations);
}
};
p = CachedValuesManager.getManager(expression.getProject()).createCachedValue(provider, false);
psiAnnotation.putUserData(COMPLIED_PATTERN, p);
}
final Pattern pattern = p.getValue();
if (pattern == null) return;
List<PsiExpression> nonConstantElements = new SmartList<PsiExpression>();
final Object result = new SubstitutedExpressionEvaluationHelper(expression.getProject()).computeExpression(
expression, myConfiguration.getAdvancedConfiguration().getDfaOption(), false, nonConstantElements);
final String o = result == null ? null : String.valueOf(result);
if (o != null) {
if (!pattern.matcher(o).matches()) {
if (annotations.length > 1) {
// the last element contains the element's actual annotation
final String fqn = annotations[annotations.length - 1].getQualifiedName();
assert fqn != null;
final String name = StringUtil.getShortName(fqn);
holder.registerProblem(expression, MessageFormat.format("Expression ''{0}'' doesn''t match ''{1}'' pattern: {2}", o, name,
pattern.pattern()));
}
else {
holder.registerProblem(expression,
MessageFormat.format("Expression ''{0}'' doesn''t match pattern: {1}", o, pattern.pattern()));
}
}
}
else if (CHECK_NON_CONSTANT_VALUES) {
for (PsiExpression expr : nonConstantElements) {
final PsiElement e;
if (expr instanceof PsiReferenceExpression) {
e = ((PsiReferenceExpression)expr).resolve();
}
else if (expr instanceof PsiMethodCallExpression) {
e = ((PsiMethodCallExpression)expr).getMethodExpression().resolve();
}
else {
e = expr;
}
final PsiModifierListOwner owner = e instanceof PsiModifierListOwner? (PsiModifierListOwner)e : null;
LocalQuickFix quickFix;
if (owner != null && PsiUtilEx.isLanguageAnnotationTarget(owner)) {
PsiAnnotation[] resolvedAnnos = AnnotationUtilEx.getAnnotationFrom(owner, myConfiguration.getAdvancedConfiguration().getPatternAnnotationPair(), true);
if (resolvedAnnos.length == 2 && annotations.length == 2 && Comparing.strEqual(resolvedAnnos[1].getQualifiedName(), annotations[1].getQualifiedName())) {
// both target and source annotated indirectly with the same anno
return;
}
final String classname = myConfiguration.getAdvancedConfiguration().getSubstAnnotationPair().first;
final AnnotateFix fix = new AnnotateFix((PsiModifierListOwner)e, classname);
quickFix = fix.canApply() ? fix : new IntroduceVariableFix(expr);
}
else {
quickFix = new IntroduceVariableFix(expr);
}
holder.registerProblem(expr, "Unsubstituted expression", quickFix);
}
}
}
private static class IntroduceVariableFix implements LocalQuickFix {
private final PsiExpression myExpr;
public IntroduceVariableFix(PsiExpression expr) {
myExpr = expr;
}
@Override
@NotNull
public String getName() {
return "Introduce Variable";
}
@Override
@NotNull
public String getFamilyName() {
return getName();
}
@Override
public void applyFix(@NotNull final Project project, @NotNull ProblemDescriptor descriptor) {
final RefactoringActionHandler handler = JavaRefactoringActionHandlerFactory.getInstance().createIntroduceVariableHandler();
final AsyncResult<DataContext> dataContextContainer = DataManager.getInstance().getDataContextFromFocus();
dataContextContainer.doWhenDone(new Consumer<DataContext>() {
@Override
public void consume(DataContext dataContext) {
handler.invoke(project, new PsiElement[]{myExpr}, dataContext);
}
});
// how to automatically annotate the variable after it has been introduced?
}
}
}