blob: 1d32bf8ab6eb6641ae7d90c61346bb772711e26a [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.lang.regexp.validation;
import com.intellij.codeInsight.intention.IntentionAction;
import com.intellij.codeInspection.ProblemHighlightType;
import com.intellij.lang.ASTNode;
import com.intellij.lang.annotation.Annotation;
import com.intellij.lang.annotation.AnnotationHolder;
import com.intellij.lang.annotation.Annotator;
import com.intellij.psi.PsiComment;
import com.intellij.psi.PsiElement;
import com.intellij.psi.util.PsiTreeUtil;
import org.intellij.lang.regexp.*;
import org.intellij.lang.regexp.psi.*;
import org.jetbrains.annotations.NotNull;
import java.math.BigInteger;
public final class RegExpAnnotator extends RegExpElementVisitor implements Annotator {
private AnnotationHolder myHolder;
private final RegExpLanguageHosts myLanguageHosts;
public RegExpAnnotator() {
myLanguageHosts = RegExpLanguageHosts.getInstance();
}
public void annotate(@NotNull PsiElement psiElement, @NotNull AnnotationHolder holder) {
assert myHolder == null : "unsupported concurrent annotator invocation";
try {
myHolder = holder;
psiElement.accept(this);
}
finally {
myHolder = null;
}
}
public void visitRegExpCharRange(RegExpCharRange range) {
final RegExpCharRange.Endpoint from = range.getFrom();
final RegExpCharRange.Endpoint to = range.getTo();
final boolean a = from instanceof RegExpChar;
final boolean b = to instanceof RegExpChar;
if (a && b) {
final Character t = ((RegExpChar)to).getValue();
final Character f = ((RegExpChar)from).getValue();
if (t != null && f != null) {
if (t < f) {
myHolder.createErrorAnnotation(range, "Illegal character range (to < from)");
}
else if (t == f) {
myHolder.createWarningAnnotation(range, "Redundant character range");
}
}
}
else if (a != b) {
myHolder.createErrorAnnotation(range, "Character class (e.g. '\\\\w') may not be used inside character range");
}
else if (from.getText().equals(to.getText())) {
myHolder.createWarningAnnotation(range, "Redundant character range");
}
}
public void visitRegExpChar(final RegExpChar ch) {
final Character value = ch.getValue();
if (value == null) {
switch (ch.getType()) {
case CHAR:
myHolder.createErrorAnnotation(ch, "Illegal/unsupported escape sequence");
break;
case HEX:
myHolder.createErrorAnnotation(ch, "Illegal hexadecimal escape sequence");
break;
case OCT:
myHolder.createErrorAnnotation(ch, "Illegal octal escape sequence");
break;
case UNICODE:
myHolder.createErrorAnnotation(ch, "Illegal unicode escape sequence");
break;
case INVALID:
// produces a parser error. already handled by IDEA and possibly suppressed by IntelliLang
break;
}
}
else {
final String text = ch.getUnescapedText();
if (text.startsWith("\\") && myLanguageHosts.isRedundantEscape(ch, text)) {
final ASTNode astNode = ch.getNode().getFirstChildNode();
if (astNode != null && astNode.getElementType() == RegExpTT.REDUNDANT_ESCAPE) {
final Annotation a = myHolder.createWeakWarningAnnotation(ch, "Redundant character escape");
registerFix(a, new RemoveRedundantEscapeAction(ch));
}
}
}
}
public void visitRegExpProperty(RegExpProperty property) {
final ASTNode category = property.getCategoryNode();
if (category == null) {
return;
}
if(!myLanguageHosts.isValidCategory(category.getPsi(), category.getText())) {
final Annotation a = myHolder.createErrorAnnotation(category, "Unknown character category");
if (a != null) {
// IDEA-9381
a.setHighlightType(ProblemHighlightType.LIKE_UNKNOWN_SYMBOL);
}
}
}
public void visitRegExpBackref(final RegExpBackref backref) {
final RegExpGroup group = backref.resolve();
if (group == null) {
final Annotation a = myHolder.createErrorAnnotation(backref, "Unresolved backreference");
if (a != null) {
// IDEA-9381
a.setHighlightType(ProblemHighlightType.LIKE_UNKNOWN_SYMBOL);
}
}
else if (PsiTreeUtil.isAncestor(group, backref, true)) {
myHolder.createWarningAnnotation(backref, "Backreference is nested into the capturing group it refers to");
}
}
public void visitRegExpGroup(RegExpGroup group) {
final RegExpPattern pattern = group.getPattern();
if (pattern != null) {
final RegExpBranch[] branches = pattern.getBranches();
if (isEmpty(branches)) {
// catches "()" as well as "(|)"
myHolder.createWarningAnnotation(group, "Empty group");
}
else if (branches.length == 1) {
final RegExpAtom[] atoms = branches[0].getAtoms();
if (atoms.length == 1 && atoms[0] instanceof RegExpGroup) {
if (group.isSimple()) {
final RegExpGroup innerGroup = (RegExpGroup)atoms[0];
if (group.isCapturing() == innerGroup.isCapturing()) {
myHolder.createWarningAnnotation(group, "Redundant group nesting");
}
}
}
}
}
if (group.isPythonNamedGroup() || group.isRubyNamedGroup()) {
if (!myLanguageHosts.supportsNamedGroupSyntax(group)) {
myHolder.createErrorAnnotation(group, "This named group syntax is not supported");
}
}
}
@Override
public void visitRegExpPyNamedGroupRef(RegExpPyNamedGroupRef groupRef) {
/* the named group itself will be highlighted as unsupported; no need to highlight reference as well
RegExpLanguageHost host = findRegExpHost(groupRef);
if (host == null || !host.supportsPythonNamedGroups()) {
myHolder.createErrorAnnotation(groupRef, "This named group reference syntax is not supported");
return;
}
*/
final RegExpGroup group = groupRef.resolve();
if (group == null) {
final Annotation a = myHolder.createErrorAnnotation(groupRef, "Unresolved backreference");
if (a != null) {
// IDEA-9381
a.setHighlightType(ProblemHighlightType.LIKE_UNKNOWN_SYMBOL);
}
}
else if (PsiTreeUtil.isAncestor(group, groupRef, true)) {
myHolder.createWarningAnnotation(groupRef, "Group reference is nested into the named group it refers to");
}
}
@Override
public void visitComment(PsiComment comment) {
if (comment.getText().startsWith("(?#")) {
if (!myLanguageHosts.supportsPerl5EmbeddedComments(comment)) {
myHolder.createErrorAnnotation(comment, "Embedded comments are not supported");
}
}
}
@Override
public void visitRegExpPyCondRef(RegExpPyCondRef condRef) {
if (!myLanguageHosts.supportsPythonConditionalRefs(condRef)) {
myHolder.createErrorAnnotation(condRef, "Conditional references are not supported");
}
}
private static boolean isEmpty(RegExpBranch[] branches) {
for (RegExpBranch branch : branches) {
if (branch.getAtoms().length > 0) {
return false;
}
}
return true;
}
public void visitRegExpQuantifier(RegExpQuantifier quantifier) {
final RegExpQuantifier.Count count = quantifier.getCount();
if (!(count instanceof RegExpQuantifier.SimpleCount)) {
String min = count.getMin();
String max = count.getMax();
if (max.equals(min)) {
if ("1".equals(max)) { // TODO: is this safe when reluctant or possesive modifier is present?
final Annotation a = myHolder.createWeakWarningAnnotation(quantifier, "Single repetition");
registerFix(a, new SimplifyQuantifierAction(quantifier, null));
}
else {
final ASTNode node = quantifier.getNode();
if (node.findChildByType(RegExpTT.COMMA) != null) {
final Annotation a = myHolder.createWeakWarningAnnotation(quantifier, "Fixed repetition range");
registerFix(a, new SimplifyQuantifierAction(quantifier, "{" + max + "}"));
}
}
}
else if ("0".equals(min) && "1".equals(max)) {
final Annotation a = myHolder.createWeakWarningAnnotation(quantifier, "Repetition range replaceable by '?'");
registerFix(a, new SimplifyQuantifierAction(quantifier, "?"));
}
else if ("0".equals(min) && max.isEmpty()) {
final Annotation a = myHolder.createWeakWarningAnnotation(quantifier, "Repetition range replaceable by '*'");
registerFix(a, new SimplifyQuantifierAction(quantifier, "*"));
}
else if ("1".equals(min) && max.isEmpty()) {
final Annotation a = myHolder.createWeakWarningAnnotation(quantifier, "Repetition range replaceable by '+'");
registerFix(a, new SimplifyQuantifierAction(quantifier, "+"));
}
else if (!min.isEmpty() && !max.isEmpty()) {
try {
BigInteger minInt = new BigInteger(min);
BigInteger maxInt = new BigInteger(max);
if (maxInt.compareTo(minInt) < 0) {
myHolder.createErrorAnnotation(quantifier, "Illegal repetition range");
}
}
catch (NumberFormatException ex) {
myHolder.createErrorAnnotation(quantifier, "Illegal repetition value");
}
}
}
if (quantifier.getType() == RegExpQuantifier.Type.POSSESSIVE) {
if (!myLanguageHosts.supportsPossessiveQuantifiers(quantifier)) {
myHolder.createErrorAnnotation(quantifier, "Nested quantifier in regexp");
}
}
}
private static void registerFix(Annotation a, IntentionAction action) {
if (a != null) {
// IDEA-9381
a.registerFix(action);
}
}
}