Skip to content

Commit a318ee7

Browse files
committed
fix ifinite loop caused by php brace matcher in embedded lexer using Php Lexer Language
closes #7803 Exit brace matcher loop for matching semi-colon-colon braces if token sequence can't movePrevious. Add unit test with custom php embedded language for timeout check on braces matcher. Max execution time 3000 ms.
1 parent b4c38d8 commit a318ee7

File tree

2 files changed

+232
-1
lines changed

2 files changed

+232
-1
lines changed

php/php.editor/src/org/netbeans/modules/php/editor/PHPBracesMatcher.java

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -109,7 +109,10 @@ public PHPBracesMatcher(MatcherContext context) {
109109
return new int [] {ts.offset(), ts.offset() + token.length()};
110110
} else if (LexUtilities.textEquals(token.text(), ':')) {
111111
do {
112-
ts.movePrevious();
112+
if ( !ts.movePrevious()) {
113+
//issue 7803 prevent infinit loop for brace matcher in code using embedded language
114+
break;
115+
}
113116
token = LexUtilities.findPreviousToken(ts,
114117
Arrays.asList(PHPTokenId.PHP_IF, PHPTokenId.PHP_ELSE, PHPTokenId.PHP_ELSEIF,
115118
PHPTokenId.PHP_FOR, PHPTokenId.PHP_FOREACH, PHPTokenId.PHP_WHILE, PHPTokenId.PHP_SWITCH,
Lines changed: 228 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,228 @@
1+
/*
2+
* Licensed to the Apache Software Foundation (ASF) under one
3+
* or more contributor license agreements. See the NOTICE file
4+
* distributed with this work for additional information
5+
* regarding copyright ownership. The ASF licenses this file
6+
* to you under the Apache License, Version 2.0 (the
7+
* "License"); you may not use this file except in compliance
8+
* with the License. You may obtain a copy of the License at
9+
*
10+
* http://www.apache.org/licenses/LICENSE-2.0
11+
*
12+
* Unless required by applicable law or agreed to in writing,
13+
* software distributed under the License is distributed on an
14+
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
15+
* KIND, either express or implied. See the License for the
16+
* specific language governing permissions and limitations
17+
* under the License.
18+
*/
19+
package org.netbeans.modules.php.editor.embedding;
20+
21+
import java.time.Duration;
22+
import java.util.Collection;
23+
import java.util.EnumSet;
24+
import javax.swing.text.BadLocationException;
25+
import org.junit.Test;
26+
import org.netbeans.api.editor.mimelookup.MimeLookup;
27+
import org.netbeans.api.lexer.InputAttributes;
28+
import org.netbeans.api.lexer.Language;
29+
import org.netbeans.api.lexer.LanguagePath;
30+
import org.netbeans.api.lexer.Token;
31+
import org.netbeans.api.lexer.TokenId;
32+
import org.netbeans.editor.BaseDocument;
33+
import org.netbeans.lib.lexer.test.TestLanguageProvider;
34+
import org.netbeans.modules.csl.api.test.CslTestBase;
35+
import org.netbeans.modules.csl.spi.DefaultLanguageConfig;
36+
import org.netbeans.modules.editor.bracesmatching.api.BracesMatchingTestUtils;
37+
import org.netbeans.modules.php.api.util.FileUtils;
38+
import org.netbeans.modules.php.editor.csl.PHPLanguage;
39+
import org.netbeans.modules.php.editor.lexer.PHPTokenId;
40+
import org.netbeans.spi.editor.bracesmatching.BracesMatcher;
41+
import org.netbeans.spi.editor.bracesmatching.BracesMatcherFactory;
42+
import org.netbeans.spi.editor.bracesmatching.MatcherContext;
43+
import org.netbeans.spi.lexer.LanguageEmbedding;
44+
import org.netbeans.spi.lexer.LanguageHierarchy;
45+
import org.netbeans.spi.lexer.Lexer;
46+
import org.netbeans.spi.lexer.LexerInput;
47+
import org.netbeans.spi.lexer.LexerRestartInfo;
48+
import org.netbeans.spi.lexer.TokenFactory;
49+
import org.openide.util.Exceptions;
50+
51+
/**
52+
*
53+
* @author bhaidu
54+
*/
55+
public class PHPEmbeddedBracesMatcherTest extends CslTestBase {
56+
57+
public static final String PHP_MIME_TEST = "text/x-php-test"; //NOI18N
58+
59+
public PHPEmbeddedBracesMatcherTest(String testName) {
60+
super(testName);
61+
TestLanguageProvider.register(PhpEmbeddedTestTokenId.language());
62+
TestLanguageProvider.register(new PHPLanguage().getLexerLanguage());
63+
}
64+
65+
@Override
66+
protected DefaultLanguageConfig getPreferredLanguage() {
67+
return new PhpEmbeddedTestLanguage();
68+
}
69+
70+
@Override
71+
protected String getPreferredMimeType() {
72+
return PHP_MIME_TEST;
73+
}
74+
75+
@Test
76+
public void testIssue7803() throws InterruptedException {
77+
BracesMatcherFactory factory = MimeLookup.getLookup(FileUtils.PHP_MIME_TYPE).lookup(BracesMatcherFactory.class);
78+
79+
String testText = "{{(TestClass^:)}}"; //NOI18N
80+
int caretPos = testText.indexOf('^');
81+
testText = testText.substring(0, caretPos) + testText.substring(caretPos + 1);
82+
BaseDocument doc = getDocument(testText);
83+
84+
MatcherContext context = BracesMatchingTestUtils.createMatcherContext(doc, caretPos, false, 1);
85+
BracesMatcher matcher = factory.createMatcher(context);
86+
87+
try {
88+
matcher.findOrigin();
89+
matcher.findMatches();
90+
assertTrue("Passed Embedded Php braces matcher timeout", true);
91+
} catch (BadLocationException ex) {
92+
Exceptions.printStackTrace(ex);
93+
}
94+
}
95+
96+
@Override
97+
protected boolean runInEQ() {
98+
return true;
99+
}
100+
101+
@Override
102+
protected int timeOut() {
103+
return 3000;
104+
}
105+
106+
public static enum PhpEmbeddedTestTokenId implements TokenId {
107+
108+
ANY(null, null),
109+
PHP(null, null);
110+
111+
private final String fixedText;
112+
113+
private final String primaryCategory;
114+
115+
PhpEmbeddedTestTokenId(String fixedText, String primaryCategory) {
116+
this.fixedText = fixedText;
117+
this.primaryCategory = primaryCategory;
118+
}
119+
120+
@Override
121+
public String primaryCategory() {
122+
return primaryCategory;
123+
}
124+
125+
public String fixedText() {
126+
return fixedText;
127+
}
128+
129+
private static final Language<PhpEmbeddedTestTokenId> language = new LanguageHierarchy<PhpEmbeddedTestTokenId>() {
130+
131+
@Override
132+
protected Lexer<PhpEmbeddedTestTokenId> createLexer(LexerRestartInfo<PhpEmbeddedTestTokenId> info) {
133+
return new PhpCustomLanguageLexer(info);
134+
}
135+
136+
@Override
137+
protected Collection<PhpEmbeddedTestTokenId> createTokenIds() {
138+
return EnumSet.allOf(PhpEmbeddedTestTokenId.class);
139+
}
140+
141+
@Override
142+
public String mimeType() {
143+
return PHP_MIME_TEST;
144+
}
145+
146+
@Override
147+
protected LanguageEmbedding<?> embedding(Token<PhpEmbeddedTestTokenId> token,
148+
LanguagePath languagePath, InputAttributes inputAttributes) {
149+
if (token.id().equals(PhpEmbeddedTestTokenId.PHP)) {
150+
return LanguageEmbedding.create(PHPTokenId.languageInPHP(), 0, 0);
151+
}
152+
153+
return null;
154+
}
155+
}.language();
156+
157+
public static Language<PhpEmbeddedTestTokenId> language() {
158+
return language;
159+
}
160+
}
161+
162+
public static class PhpEmbeddedTestLanguage extends DefaultLanguageConfig {
163+
164+
@Override
165+
public Language<PhpEmbeddedTestTokenId> getLexerLanguage() {
166+
return PhpEmbeddedTestTokenId.language();
167+
}
168+
169+
@Override
170+
public String getDisplayName() {
171+
return "Test language with embedded php"; //NOI18N
172+
}
173+
174+
}
175+
176+
public static class PhpCustomLanguageLexer implements Lexer<PhpEmbeddedTestTokenId> {
177+
178+
private final LexerInput input;
179+
private final TokenFactory<PhpEmbeddedTestTokenId> factory;
180+
private boolean embeddedPhpState = true;
181+
182+
public PhpCustomLanguageLexer(LexerRestartInfo<PhpEmbeddedTestTokenId> info) {
183+
this.input = info.input();
184+
this.factory = info.tokenFactory();
185+
}
186+
187+
@Override
188+
public Token<PhpEmbeddedTestTokenId> nextToken() {
189+
if (input.read() == LexerInput.EOF) {
190+
return null;
191+
}
192+
193+
input.read();
194+
195+
if (input.readText().toString().startsWith("{{")) { //NOI18N
196+
embeddedPhpState = true;
197+
return factory.createToken(PhpEmbeddedTestTokenId.ANY);
198+
}
199+
200+
if (embeddedPhpState && readUntil("}}")) { //NOI18N
201+
input.backup(2);
202+
embeddedPhpState = false;
203+
return factory.createToken(PhpEmbeddedTestTokenId.PHP);
204+
}
205+
206+
return factory.createToken(PhpEmbeddedTestTokenId.ANY);
207+
}
208+
209+
private boolean readUntil(String condition) {
210+
int read;
211+
212+
while ((read = input.read()) != LexerInput.EOF && !input.readText().toString().endsWith(condition));
213+
214+
return read != LexerInput.EOF;
215+
}
216+
217+
@Override
218+
public Object state() {
219+
return null;
220+
}
221+
222+
@Override
223+
public void release() {
224+
225+
}
226+
227+
}
228+
}

0 commit comments

Comments
 (0)