diff --git a/org.eclipse.xtext.tests/src/org/eclipse/xtext/xtext/generator/hoisting/HoistingProcessorTest.java b/org.eclipse.xtext.tests/src/org/eclipse/xtext/xtext/generator/hoisting/HoistingProcessorTest.java index e4b28a588..96c486239 100644 --- a/org.eclipse.xtext.tests/src/org/eclipse/xtext/xtext/generator/hoisting/HoistingProcessorTest.java +++ b/org.eclipse.xtext.tests/src/org/eclipse/xtext/xtext/generator/hoisting/HoistingProcessorTest.java @@ -21,6 +21,7 @@ import org.eclipse.xtext.resource.XtextResource; import org.eclipse.xtext.testing.GlobalRegistries; import org.eclipse.xtext.tests.AbstractXtextTests; import org.eclipse.xtext.xtext.generator.parser.antlr.hoisting.HoistingProcessor; +import org.eclipse.xtext.xtext.generator.parser.antlr.hoisting.exceptions.EndlessPrefixException; import org.eclipse.xtext.xtext.generator.parser.antlr.hoisting.exceptions.TokenAnalysisAbortedException; import org.eclipse.xtext.xtext.generator.parser.antlr.hoisting.guards.hoistingGuards.HoistingGuard; import org.junit.After; @@ -1383,4 +1384,25 @@ public class HoistingProcessorTest extends AbstractXtextTests { ")", guard.render()); } + + @Test(expected = EndlessPrefixException.class) + public void testRecursionInNestedAlternatives_bugEndlessLoop_expectEndlessPrefixException() throws Exception { + // @formatter:off + String model = + MODEL_PREAMBLE + + "hoistingDebug\n" + + "S: $$ p0 $$?=> a=A 's'\n" + + " | $$ p1 $$?=> a=A 't';\n" + + "A: $$ p2 $$?=> b=B 'u'\n" + + " | $$ p3 $$?=> b=B 'v';\n" + + "B: $$ p4 $$?=> 'a' 'b' 'c'\n" + + " | $$ p5 $$?=> 'z' s=S;\n"; + // @formatter:off + XtextResource resource = getResourceFromString(model); + Grammar grammar = ((Grammar) resource.getContents().get(0)); + hoistingProcessor.init(grammar); + AbstractRule rule = getRule(grammar, "S"); + + hoistingProcessor.findHoistingGuard(rule.getAlternatives()); + } } diff --git a/org.eclipse.xtext.xtext.generator/src/org/eclipse/xtext/xtext/generator/parser/antlr/hoisting/HoistingProcessor.java b/org.eclipse.xtext.xtext.generator/src/org/eclipse/xtext/xtext/generator/parser/antlr/hoisting/HoistingProcessor.java index db128f4a7..05b56031d 100644 --- a/org.eclipse.xtext.xtext.generator/src/org/eclipse/xtext/xtext/generator/parser/antlr/hoisting/HoistingProcessor.java +++ b/org.eclipse.xtext.xtext.generator/src/org/eclipse/xtext/xtext/generator/parser/antlr/hoisting/HoistingProcessor.java @@ -42,6 +42,7 @@ import org.eclipse.xtext.util.Tuples; import static org.eclipse.xtext.GrammarUtil.*; import org.eclipse.xtext.xtext.generator.parser.antlr.JavaCodeUtils; +import org.eclipse.xtext.xtext.generator.parser.antlr.hoisting.exceptions.EndlessPrefixException; import org.eclipse.xtext.xtext.generator.parser.antlr.hoisting.exceptions.NestedIdenticalPathException; import org.eclipse.xtext.xtext.generator.parser.antlr.hoisting.exceptions.TokenAnalysisAbortedException; import org.eclipse.xtext.xtext.generator.parser.antlr.hoisting.exceptions.UnsupportedConstructException; @@ -223,6 +224,37 @@ public class HoistingProcessor { return flattened; } + private boolean hasEndlessPath(AbstractElement path) { + return hasEndlessPath(path, new ArrayList<>()); + } + private boolean hasEndlessPath(AbstractElement path, final List calls) { + if (isMultipleCardinality(path)) { + return true; + } + + if (path instanceof Assignment) { + return hasEndlessPath(((Assignment) path).getTerminal(), calls); + } else if (path instanceof CompoundElement) { + return ((CompoundElement) path).getElements().stream().anyMatch(p -> hasEndlessPath(p, calls)); + } else if (path instanceof RuleCall) { + if (calls.stream().anyMatch(c -> EcoreUtil.equals(c, path))) { + return true; + } + + List _calls = new ArrayList<>(calls); + _calls.add((RuleCall) path); + return hasEndlessPath(((RuleCall) path).getRule().getAlternatives(), _calls); + } + + return false; + } + + private boolean hasEndlessPrefix(List paths) { + return paths.stream() + .filter(this::hasEndlessPath) + .count() >= 2; // only a problem if more than 2 paths are endless + } + private HoistingGuard findGuardForAlternatives(CompoundElement alternatives, AbstractRule currentRule, boolean skipCache) { if (config.isDebug()) log.info("find guard for alternative"); @@ -293,6 +325,14 @@ public class HoistingProcessor { ).map(p -> new PathGuard(p.getFirst(), p.getSecond())) .collect(AlternativesGuard.collector()); } catch(NestedIdenticalPathException e) { + if (hasEndlessPrefix(paths)) { + // endless paths in combination with a NestedIndenticalPathException means + // there are probably endless prefixes in this element + + // TODO: maybe only use predicates (analog to ANTLR) instead of error-ing out + throw new EndlessPrefixException("endless prefix in more than two paths", currentRule); + } + // nested identical paths // -> flatten paths to alternative and try again // this is very inefficient diff --git a/org.eclipse.xtext.xtext.generator/src/org/eclipse/xtext/xtext/generator/parser/antlr/hoisting/exceptions/EndlessPrefixException.java b/org.eclipse.xtext.xtext.generator/src/org/eclipse/xtext/xtext/generator/parser/antlr/hoisting/exceptions/EndlessPrefixException.java new file mode 100644 index 000000000..be8c069d1 --- /dev/null +++ b/org.eclipse.xtext.xtext.generator/src/org/eclipse/xtext/xtext/generator/parser/antlr/hoisting/exceptions/EndlessPrefixException.java @@ -0,0 +1,46 @@ +/******************************************************************************* + * Copyright (c) 2021 itemis AG (http://www.itemis.eu) and others. + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * SPDX-License-Identifier: EPL-2.0 + *******************************************************************************/ +package org.eclipse.xtext.xtext.generator.parser.antlr.hoisting.exceptions; + +import org.eclipse.xtext.AbstractRule; + +/** + * @author overflow - Initial contribution and API + */ +public class EndlessPrefixException extends TokenAnalysisAbortedException { + private static final long serialVersionUID = 2670032012156746113L; + + public EndlessPrefixException() { + super(); + } + + public EndlessPrefixException(String message, AbstractRule rule) { + super(message, rule); + } + + public EndlessPrefixException(String message, Throwable cause, AbstractRule rule) { + super(message, cause, rule); + } + + public EndlessPrefixException(String message, Throwable cause) { + super(message, cause); + } + + public EndlessPrefixException(String message) { + super(message); + } + + public EndlessPrefixException(Throwable cause, AbstractRule rule) { + super(cause, rule); + } + + public EndlessPrefixException(Throwable cause) { + super(cause); + } +} diff --git a/org.eclipse.xtext/xtend-gen/org/eclipse/xtext/generator/trace/node/GeneratorNodeProcessor.java b/org.eclipse.xtext/xtend-gen/org/eclipse/xtext/generator/trace/node/GeneratorNodeProcessor.java index 9d7dde604..8d78782ed 100644 --- a/org.eclipse.xtext/xtend-gen/org/eclipse/xtext/generator/trace/node/GeneratorNodeProcessor.java +++ b/org.eclipse.xtext/xtend-gen/org/eclipse/xtext/generator/trace/node/GeneratorNodeProcessor.java @@ -104,8 +104,8 @@ public class GeneratorNodeProcessor { return this.contents; } - public char charAt(final int arg0) { - return this.contents.charAt(arg0); + public char charAt(final int index) { + return this.contents.charAt(index); } public IntStream chars() { @@ -120,8 +120,8 @@ public class GeneratorNodeProcessor { return this.contents.length(); } - public CharSequence subSequence(final int arg0, final int arg1) { - return this.contents.subSequence(arg0, arg1); + public CharSequence subSequence(final int start, final int end) { + return this.contents.subSequence(start, end); } }