RegExp: character range may contain only characters, including named characters

This commit is contained in:
Bas Leijdekkers
2017-02-02 19:00:25 +01:00
parent 10f36e1621
commit 6ca8877ef1
14 changed files with 116 additions and 89 deletions
@@ -18,6 +18,7 @@ package org.intellij.lang.regexp;
import com.intellij.lexer.FlexAdapter;
import com.intellij.lexer.Lexer;
import com.intellij.lexer.LookAheadLexer;
import com.intellij.psi.tree.IElementType;
import org.jetbrains.annotations.NotNull;
import java.util.EnumSet;
@@ -30,7 +31,8 @@ public class RegExpLexer extends LookAheadLexer {
@Override
protected void lookAhead(@NotNull Lexer baseLexer) {
if (!RegExpTT.CHARACTERS.contains(baseLexer.getTokenType())) {
final IElementType tokenType = baseLexer.getTokenType();
if (!RegExpTT.CHARACTERS.contains(tokenType) && tokenType != RegExpTT.RBRACE) {
advanceLexer(baseLexer);
if (baseLexer.getTokenType() == RegExpTT.MINUS) {
advanceAs(baseLexer, RegExpTT.CHARACTER);
@@ -18,7 +18,6 @@ package org.intellij.lang.regexp;
import com.intellij.lang.ASTNode;
import com.intellij.lang.PsiBuilder;
import com.intellij.lang.PsiParser;
import com.intellij.psi.StringEscapesTokenTypes;
import com.intellij.psi.tree.IElementType;
import com.intellij.psi.tree.TokenSet;
import org.jetbrains.annotations.NotNull;
@@ -251,8 +250,8 @@ public class RegExpParser implements PsiParser {
else if (token == RegExpTT.BRACKET_EXPRESSION_BEGIN) {
parseBracketExpression(builder);
}
else if (RegExpTT.CHARACTERS.contains(token)) {
parseSimpleClassdef(builder);
else if (RegExpTT.CHARACTERS.contains(token) || token == RegExpTT.NAMED_CHARACTER) {
parseCharacterRange(builder);
}
else if (token == RegExpTT.CHAR_CLASS) {
final PsiBuilder.Marker m = builder.mark();
@@ -262,9 +261,6 @@ public class RegExpParser implements PsiParser {
else if (token == RegExpTT.PROPERTY) {
parseProperty(builder);
}
else if (token == RegExpTT.NAMED_CHARACTER) {
parseNamedCharacter(builder);
}
else {
return count > 0;
}
@@ -283,49 +279,42 @@ public class RegExpParser implements PsiParser {
marker.done(RegExpElementTypes.POSIX_BRACKET_EXPRESSION);
}
private void parseSimpleClassdef(PsiBuilder builder) {
assert RegExpTT.CHARACTERS.contains(builder.getTokenType());
final PsiBuilder.Marker marker = builder.mark();
makeChar(builder);
private void parseCharacterRange(PsiBuilder builder) {
final PsiBuilder.Marker rangeMarker = builder.mark();
parseCharacter(builder);
if (builder.getTokenType() == RegExpTT.MINUS) {
final PsiBuilder.Marker m = builder.mark();
final PsiBuilder.Marker minusMarker = builder.mark();
builder.advanceLexer();
final IElementType t = builder.getTokenType();
if (RegExpTT.CHARACTERS.contains(t) || t == RegExpTT.CHAR_CLASS) {
m.drop();
makeChar(builder);
marker.done(RegExpElementTypes.CHAR_RANGE);
if (RegExpTT.CHARACTERS.contains(t) || t == RegExpTT.NAMED_CHARACTER) {
minusMarker.drop();
parseCharacter(builder);
rangeMarker.done(RegExpElementTypes.CHAR_RANGE);
}
else {
marker.drop();
m.done(t == RegExpTT.CHAR_CLASS ? RegExpElementTypes.SIMPLE_CLASS : RegExpElementTypes.CHAR);
if (t == RegExpTT.CLASS_END) { // [a-]
return;
rangeMarker.drop();
minusMarker.done(RegExpElementTypes.CHAR);
}
else if (t == RegExpTT.CLASS_BEGIN) { // [a-[b]]
if (parseClassdef(builder)) {
return;
}
else if (t == RegExpTT.CLASS_BEGIN) { // [a-[b]]\
rangeMarker.drop();
minusMarker.done(RegExpElementTypes.CHAR);
parseClassdef(builder);
}
else {
minusMarker.drop();
builder.error("Illegal character range");
rangeMarker.done(RegExpElementTypes.CHAR_RANGE);
}
builder.error("Illegal character range");
}
}
else {
marker.drop();
rangeMarker.drop();
}
}
private static void makeChar(PsiBuilder builder) {
final IElementType t = builder.getTokenType();
final PsiBuilder.Marker m = builder.mark();
builder.advanceLexer();
m.done(t == RegExpTT.CHAR_CLASS ? RegExpElementTypes.SIMPLE_CLASS : RegExpElementTypes.CHAR);
}
/**
* GROUP ::= "(" PATTERN ")" | TERM
* TERM ::= "." | "$" | "^" | CHAR | CLASS | BACKREF
@@ -363,9 +352,9 @@ public class RegExpParser implements PsiParser {
marker.done(RegExpElementTypes.SET_OPTIONS);
}
}
else if (type == StringEscapesTokenTypes.INVALID_CHARACTER_ESCAPE_TOKEN || RegExpTT.CHARACTERS.contains(type)) {
builder.advanceLexer();
marker.done(RegExpElementTypes.CHAR);
else if (RegExpTT.CHARACTERS.contains(type) || type == RegExpTT.NAMED_CHARACTER) {
marker.drop();
parseCharacter(builder);
}
else if (RegExpTT.BOUNDARIES.contains(type)) {
builder.advanceLexer();
@@ -407,10 +396,6 @@ public class RegExpParser implements PsiParser {
marker.drop();
parseProperty(builder);
}
else if (type == RegExpTT.NAMED_CHARACTER) {
marker.drop();
parseNamedCharacter(builder);
}
else if (type == RegExpTT.DOT || type == RegExpTT.CHAR_CLASS) {
builder.advanceLexer();
marker.done(RegExpElementTypes.SIMPLE_CLASS);
@@ -495,13 +480,19 @@ public class RegExpParser implements PsiParser {
marker.done(RegExpElementTypes.PROPERTY);
}
private static void parseNamedCharacter(PsiBuilder builder) {
private static void parseCharacter(PsiBuilder builder) {
final PsiBuilder.Marker marker = builder.mark();
builder.advanceLexer();
checkMatches(builder, RegExpTT.LBRACE, "'{' expected");
checkMatches(builder, RegExpTT.NAME, "Unicode character name expected");
checkMatches(builder, RegExpTT.RBRACE, "'}' expected");
marker.done(RegExpElementTypes.NAMED_CHARACTER);
if (builder.getTokenType() == RegExpTT.NAMED_CHARACTER) {
builder.advanceLexer();
checkMatches(builder, RegExpTT.LBRACE, "'{' expected");
checkMatches(builder, RegExpTT.NAME, "Unicode character name expected");
checkMatches(builder, RegExpTT.RBRACE, "'}' expected");
marker.done(RegExpElementTypes.NAMED_CHARACTER);
}
else {
builder.advanceLexer();
marker.done(RegExpElementTypes.CHAR);
}
}
private static void patternExpected(PsiBuilder builder) {
@@ -1,5 +1,5 @@
/*
* Copyright 2000-2011 JetBrains s.r.o.
* Copyright 2000-2017 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.
@@ -19,6 +19,7 @@ import com.intellij.lang.ASTNode;
import com.intellij.openapi.util.Condition;
import com.intellij.psi.PsiElement;
import org.intellij.lang.regexp.psi.RegExpChar;
import org.intellij.lang.regexp.psi.RegExpNamedCharacter;
import org.jetbrains.annotations.NotNull;
/**
@@ -28,7 +29,8 @@ public class RegExpWordSelectionFilter implements Condition<PsiElement> {
@Override
public boolean value(@NotNull PsiElement element) {
final ASTNode node = element.getNode();
if ((node != null && node.getElementType() == RegExpTT.CHARACTER) || element instanceof RegExpChar) {
if ((node != null && node.getElementType() == RegExpTT.CHARACTER) ||
(element instanceof RegExpChar && !(element instanceof RegExpNamedCharacter))) {
return false;
}
return true;
@@ -20,7 +20,7 @@ import org.jetbrains.annotations.NotNull;
/**
* Represents a simple, escaped, encoded or named character
*/
public interface RegExpChar extends RegExpAtom, RegExpClassElement, RegExpCharRange.Endpoint {
public interface RegExpChar extends RegExpAtom, RegExpClassElement {
/** Character type */
enum Type {
/** a plain character ("a") */
@@ -16,16 +16,16 @@
package org.intellij.lang.regexp.psi;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
/**
* Represents a character range as in [a-z].
*/
public interface RegExpCharRange extends RegExpElement, RegExpClassElement {
interface Endpoint extends RegExpClassElement { }
public interface RegExpCharRange extends RegExpClassElement {
@NotNull
Endpoint getFrom();
RegExpChar getFrom();
@NotNull
Endpoint getTo();
@Nullable
RegExpChar getTo();
}
@@ -20,7 +20,7 @@ import org.jetbrains.annotations.NotNull;
/**
* Represents a simple character class.
*/
public interface RegExpSimpleClass extends RegExpAtom, RegExpClassElement, RegExpCharRange.Endpoint {
public interface RegExpSimpleClass extends RegExpAtom, RegExpClassElement {
enum Kind {
/** . */ ANY,
/** \d */ DIGIT,
@@ -17,35 +17,32 @@ package org.intellij.lang.regexp.psi.impl;
import com.intellij.lang.ASTNode;
import com.intellij.psi.PsiElement;
import com.intellij.psi.tree.TokenSet;
import org.jetbrains.annotations.NotNull;
import org.intellij.lang.regexp.RegExpElementTypes;
import org.intellij.lang.regexp.psi.RegExpChar;
import org.intellij.lang.regexp.psi.RegExpCharRange;
import org.intellij.lang.regexp.psi.RegExpElementVisitor;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
public class RegExpCharRangeImpl extends RegExpElementImpl implements RegExpCharRange {
private static final TokenSet E = TokenSet.create(RegExpElementTypes.CHAR, RegExpElementTypes.SIMPLE_CLASS);
public RegExpCharRangeImpl(ASTNode astNode) {
super(astNode);
}
@Override
@NotNull
public Endpoint getFrom() {
return (Endpoint)getCharNode(0);
}
@NotNull
public Endpoint getTo() {
return (Endpoint)getCharNode(1);
public RegExpChar getFrom() {
return (RegExpChar)getFirstChild();
}
private PsiElement getCharNode(int idx) {
final ASTNode[] ch = getNode().getChildren(E);
assert ch.length == 2;
return ch[idx].getPsi();
@Override
@Nullable
public RegExpChar getTo() {
final PsiElement child = getLastChild();
return child instanceof RegExpChar ? (RegExpChar)child : null;
}
@Override
public void accept(RegExpElementVisitor visitor) {
visitor.visitRegExpCharRange(this);
}
@@ -83,16 +83,10 @@ public final class RegExpAnnotator extends RegExpElementVisitor implements Annot
@Override
public void visitRegExpCharRange(RegExpCharRange range) {
final RegExpCharRange.Endpoint from = range.getFrom();
final RegExpCharRange.Endpoint to = range.getTo();
if (from instanceof RegExpChar && to instanceof RegExpChar) {
checkRange(range, ((RegExpChar)from).getValue(), ((RegExpChar)to).getValue());
}
else if (to instanceof RegExpSimpleClass) {
myHolder.createErrorAnnotation(to, "Character class not allowed inside character range");
}
else if (from.getText().equals(to.getText())) {
myHolder.createWarningAnnotation(range, "Redundant character range");
final RegExpChar from = range.getFrom();
final RegExpChar to = range.getTo();
if (to != null) {
checkRange(range, from.getValue(), to.getValue());
}
}
@@ -149,6 +149,7 @@ public class RegExpParsingTest extends ParsingTestCase {
public void testCharclasses68() throws IOException { doCodeTest("[\\b]"); }
public void testCharClasses69() throws IOException { doCodeTest("\\p{^L}"); }
public void testCharClasses70() throws IOException { doCodeTest("[&&&&a]"); }
public void testCharClasses71() throws IOException { doCodeTest("[a-\\Qz\\E]"); }
public void testGroups1() throws IOException { doCodeTest("()ef"); }
public void testGroups2() throws IOException { doCodeTest("()*"); }
@@ -248,6 +249,7 @@ public class RegExpParsingTest extends ParsingTestCase {
public void testNamedchars12() throws IOException { doCodeTest("\\p{InArabic Extended-A}"); }
public void testNamedchars13() throws IOException { doCodeTest("\\N{Mahjong Tile Winter}"); }
public void testNamedchars14() throws IOException { doCodeTest("[\\N{Mahjong Tile Winter}]"); }
public void testNamedchars15() throws IOException { doCodeTest("[\\N{LATIN SMALL LETTER A}-\\N{LATIN SMALL LETTER Z}]"); }
public void testBackrefs1() throws IOException { doCodeTest("(ac*)c*d[ac]*\\1"); }
public void testBackrefs2() throws IOException { doCodeTest("(.)=\\1"); }
@@ -0,0 +1,14 @@
REGEXP_FILE
RegExpPatternImpl: <[a-\Qz\E]>
RegExpBranchImpl: <[a-\Qz\E]>
RegExpClassImpl: <[a-\Qz\E]>
PsiElement(CLASS_BEGIN)('[')
RegExpCharRangeImpl: <a-\Qz>
RegExpCharImpl: <a>
PsiElement(CHARACTER)('a')
PsiElement(MINUS)('-')
PsiWhiteSpace('\Q')
RegExpCharImpl: <z>
PsiElement(CHARACTER)('z')
PsiWhiteSpace('\E')
PsiElement(CLASS_END)(']')
+5 -3
View File
@@ -3,10 +3,12 @@ REGEXP_FILE
RegExpBranchImpl: <[a-\w]>
RegExpClassImpl: <[a-\w]>
PsiElement(CLASS_BEGIN)('[')
RegExpCharRangeImpl: <a-\w>
RegExpCharRangeImpl: <a->
RegExpCharImpl: <a>
PsiElement(CHARACTER)('a')
PsiElement(MINUS)('-')
RegExpSimpleClassImpl: <\w>
PsiElement(CHAR_CLASS)('\w')
PsiErrorElement:Illegal character range
<empty list>
RegExpSimpleClassImpl: <\w>
PsiElement(CHAR_CLASS)('\w')
PsiElement(CLASS_END)(']')
+5 -5
View File
@@ -6,12 +6,12 @@ REGEXP_FILE
RegExpClassImpl: <[b-&&[cd]]>
PsiElement(CLASS_BEGIN)('[')
RegExpIntersectionImpl: <b-&&[cd]>
RegExpCharImpl: <b>
PsiElement(CHARACTER)('b')
RegExpCharImpl: <->
RegExpCharRangeImpl: <b->
RegExpCharImpl: <b>
PsiElement(CHARACTER)('b')
PsiElement(MINUS)('-')
PsiErrorElement:Illegal character range
<empty list>
PsiErrorElement:Illegal character range
<empty list>
PsiElement(ANDAND)('&&')
RegExpClassImpl: <[cd]>
PsiElement(CLASS_BEGIN)('[')
@@ -0,0 +1,18 @@
REGEXP_FILE
RegExpPatternImpl: <[\N{LATIN SMALL LETTER A}-\N{LATIN SMALL LETTER Z}]>
RegExpBranchImpl: <[\N{LATIN SMALL LETTER A}-\N{LATIN SMALL LETTER Z}]>
RegExpClassImpl: <[\N{LATIN SMALL LETTER A}-\N{LATIN SMALL LETTER Z}]>
PsiElement(CLASS_BEGIN)('[')
RegExpCharRangeImpl: <\N{LATIN SMALL LETTER A}-\N{LATIN SMALL LETTER Z}>
RegExpNamedCharacterImpl: <\N{LATIN SMALL LETTER A}>
PsiElement(NAMED_CHARACTER)('\N')
PsiElement(LBRACE)('{')
PsiElement(NAME)('LATIN SMALL LETTER A')
PsiElement(RBRACE)('}')
PsiElement(MINUS)('-')
RegExpNamedCharacterImpl: <\N{LATIN SMALL LETTER Z}>
PsiElement(NAMED_CHARACTER)('\N')
PsiElement(LBRACE)('{')
PsiElement(NAME)('LATIN SMALL LETTER Z')
PsiElement(RBRACE)('}')
PsiElement(CLASS_END)(']')
@@ -130,6 +130,11 @@ public class RegExpHighlightingTest extends LightCodeInsightFixtureTestCase {
doTest("[<error descr=\"Illegal character range (to < from)\">z-a</error>]");
}
public void testIllegalCharacterRange4() {
IdeaTestUtil.setTestVersion(JavaSdkVersion.JDK_1_9, myFixture.getModule(), getTestRootDisposable());
doTest("[<error descr=\"Illegal character range (to < from)\">\\N{LATIN SMALL LETTER Z}-\\N{LATIN SMALL LETTER A}</error>]");
}
public void testLegalCharacterRange() {
// Cyrillic Capital Letter Zemlya - Unicode Han Character 'to peel, pare' (Unicode Supplementary Character)
// without code point support 0x20731 wraps to 0x731 which would produce a "Illegal character range (to < from)" error