package org.eclipse.emf.test.ecore.xcore.legacy_xpect_runner; import java.lang.reflect.Method; import java.lang.reflect.Modifier; import java.math.BigInteger; import java.util.Collections; import java.util.Iterator; import java.util.List; import java.util.Stack; import java.util.regex.Matcher; import java.util.regex.Pattern; import org.eclipse.emf.common.util.URI; import org.eclipse.xtext.nodemodel.ILeafNode; import org.eclipse.xtext.nodemodel.INode; import org.eclipse.xtext.parsetree.reconstr.impl.NodeIterator; import org.eclipse.xtext.resource.XtextResource; import org.eclipse.xtext.util.Exceptions; import org.eclipse.xtext.util.Pair; import org.eclipse.xtext.util.Strings; import org.eclipse.xtext.util.Wrapper; import org.eclipse.xtext.util.formallang.FollowerFunctionImpl; import org.eclipse.xtext.util.formallang.Nfa; import org.eclipse.xtext.util.formallang.NfaUtil; import org.eclipse.xtext.util.formallang.NfaUtil.BacktrackHandler; import org.eclipse.xtext.util.formallang.StringProduction; import org.eclipse.xtext.util.formallang.StringProduction.ElementType; import org.eclipse.xtext.util.formallang.StringProduction.ProdElement; import org.junit.Test; import com.google.common.collect.HashMultimap; import com.google.common.collect.Lists; import com.google.common.collect.Multimap; /** * This class will be removed in the next release after 2.4.2 * * @author Moritz Eysholdt - Initial contribution and API */ @SuppressWarnings("restriction") public class XpectParameterProvider implements IParameterProvider { protected static class AssignedProduction extends StringProduction { public AssignedProduction(String production) { super(production); } @Override protected ProdElement parsePrim(Stack<Pair<Token, String>> tokens) { Pair<Token, String> current = tokens.pop(); switch (current.getFirst()) { case PL: ProdElement result1 = parseAlt(tokens); if (tokens.peek().getFirst().equals(Token.PR)) tokens.pop(); else throw new RuntimeException("')' expected, but " + tokens.peek().getFirst() + " found"); parseCardinality(tokens, result1); return result1; case STRING: ProdElement result2 = createElement(ElementType.TOKEN); result2.setValue(current.getSecond()); parseCardinality(tokens, result2); return result2; case ID: ProdElement result3 = createElement(ElementType.TOKEN); result3.setName(current.getSecond()); Pair<Token, String> eq = tokens.pop(); if (eq.getFirst() == Token.EQ) { Pair<Token, String> val = tokens.pop(); switch (val.getFirst()) { case ID: case STRING: result3.setValue(val.getSecond()); break; default: throw new RuntimeException("Unexpected token " + current.getFirst()); } } else throw new RuntimeException("Unexpected token " + eq.getFirst() + ", expected '='"); parseCardinality(tokens, result3); return result3; default: throw new RuntimeException("Unexpected token " + current.getFirst()); } } } protected static class BacktrackItem { protected int offset; protected ProdElement token; protected String value; public BacktrackItem(int offset) { super(); this.offset = offset; } public BacktrackItem(int offset, ProdElement token, String value) { super(); this.offset = offset; this.token = token; this.value = value; } @Override public String toString() { return token + ":" + value; } } protected static class Expectation implements IExpectation { protected String expectation; protected String indentation = null; protected int lenght; protected int offset; public Expectation(int offset, int lenght, String expectation) { super(); this.offset = offset; this.lenght = lenght; this.expectation = expectation; } public Expectation(int offset, int lenght, String expectation, String indentation) { super(); this.offset = offset; this.lenght = lenght; this.expectation = expectation; this.indentation = indentation; } public String getExpectation() { return expectation; } public String getIndentation() { return indentation; } public int getLength() { return lenght; } public int getOffset() { return offset; } } protected enum Token { ID("[a-zA-Z][a-zA-Z0-9_]*"), // INT("[0-9]+"), // OFFSET("'([^']*)'|[^\\s]+"), // STRING("'([^']*)'"), // TEXT("[^\\s]+"); public final Pattern pattern; private Token(String regex) { this.pattern = Pattern.compile("^" + regex); } } public final static String PARAM_OFFSET = "offset"; public final static String PARAM_RESOURCE = "resource"; protected static final Pattern WS = Pattern.compile("^[\\s]+"); protected static Pattern XPECT_PATTERN = Pattern .compile("(\\S)?XPECT(_CLASS|_IMPORT)?\\s+([a-zA-Z0-9]*)"); public void collectParameters(Class<?> testClass, XtextResource resource, IParameterAcceptor acceptor) { collectTestMethods(testClass, resource, acceptor); for (ILeafNode leaf : resource.getParseResult().getRootNode() .getLeafNodes()) if (leaf.isHidden() && leaf.getText().contains("XPECT")) parseLeaf(testClass, resource, leaf, acceptor); } protected void collectTestMethods(Class<?> testClass, XtextResource res, IParameterAcceptor acceptor) { for (Method meth : testClass.getMethods()) { if (Modifier.isPublic(meth.getModifiers()) && !Modifier.isStatic(meth.getModifiers())) { Test annotation = meth.getAnnotation(Test.class); if (annotation != null) acceptor.acceptTest(null, meth.getName(), getDefaultParams(res, 0), null, false); } } } protected Iterable<Object> convertValue(XtextResource res, INode ctx, int offset, Token token, String value) { switch (token) { case OFFSET: int add = value.indexOf('|'); if (add >= 0) value = value.substring(0, add) + value.substring(add + 1); else add = 0; String text = ctx.getRootNode().getText(); int result = text.indexOf(value, offset); if (result >= 0) { int off = result + add; return Lists.newArrayList(off, new Offset(res, off)); } else throw new RuntimeException("OFFSET '" + value + "' not found"); case INT: List<Object> r = Lists.newArrayList(); try { r.add(Integer.valueOf(value)); } catch (NumberFormatException e) { } try { r.add(new BigInteger(value)); } catch (NumberFormatException e) { } r.add(value); return r; case ID: case STRING: case TEXT: return Collections.<Object> singleton(value); } return Collections.<Object> singleton(value); } protected Multimap<String, Object> getDefaultParams(XtextResource res, int offset) { Multimap<String, Object> result = HashMultimap.create(); result.put(PARAM_RESOURCE, res); result.put(PARAM_OFFSET, offset); result.put(PARAM_OFFSET, new Offset(res, offset)); return result; } protected String getIndentation(INode ctx, int offset) { String text = ctx.getRootNode().getText(); int nl = text.lastIndexOf("\n", offset); if (nl < 0) nl = 0; StringBuilder result = new StringBuilder(); for (int i = nl + 1; i < text.length() && Character.isWhitespace(text.charAt(i)); i++) result.append(text.charAt(i)); return result.toString(); } protected int getOffsetOfNextSemanticNode(INode node) { Iterator<INode> it = new NodeIterator(node); while (it.hasNext()) { INode n = it.next(); if (n instanceof ILeafNode && !((ILeafNode) n).isHidden()) return n.getOffset(); } return node.getEndOffset(); } protected Nfa<ProdElement> getParameterNfa(String syntax) { AssignedProduction prod = new AssignedProduction(syntax); FollowerFunctionImpl<ProdElement, String> ff = new FollowerFunctionImpl<ProdElement, String>( prod); ProdElement start = prod.new ProdElement(ElementType.TOKEN); ProdElement stop = prod.new ProdElement(ElementType.TOKEN); Nfa<ProdElement> result = new NfaUtil().create(prod, ff, start, stop); return result; } protected String getParameterSyntax(Class<?> testClass, String methodName) { try { Method method = testClass.getMethod(methodName); ParameterSyntax annotation = method .getAnnotation(ParameterSyntax.class); if (annotation != null) return annotation.value(); } catch (SecurityException e) { Exceptions.throwUncheckedException(e); } catch (NoSuchMethodException e) { Exceptions.throwUncheckedException(e); } return null; } protected void parseLeaf(Class<?> testClass, XtextResource resource, ILeafNode leaf, IParameterAcceptor acceptor) { String text = leaf.getText(); Matcher matcher = XPECT_PATTERN.matcher(text); int offset = 0; while (offset < text.length() && matcher.find(offset)) { if (matcher.group(2) == null) { int newOffset; if ((newOffset = parseXpect(testClass, resource, leaf, text, matcher.group(3), matcher.end(), acceptor, matcher.group(1) != null)) >= 0) offset = newOffset; else offset = matcher.end(); } else if ("_IMPORT".equals(matcher.group(2))) { offset = parseXpectImport(resource, text, matcher.end(2), acceptor); } // } else { // int newOffset; // if ((newOffset = parseXpectTest(testClass, text, matcher.end(), // acceptor)) >= 0) // offset = newOffset; // else // offset = matcher.end(); // } } } protected int parseString(String text, int offset, Wrapper<String> value) { if (offset < text.length() && text.charAt(offset) == '"') { int i = offset + 1; while (offset < text.length() && text.charAt(i - 1) == '\\' || text.charAt(i) != '"') i++; if (text.charAt(i) == '"') { value.set(text.substring(offset + 1, i - 1)); return i; } } return -1; } protected int parseStringOrText(String text, int offset, Wrapper<String> value) { int newOffset = parseString(text, offset, value); if (newOffset < 0) newOffset = parseText(text, offset, value); return newOffset; } protected int parseText(String text, int offset, Wrapper<String> value) { int i = offset; while (i < text.length()) switch (text.charAt(i)) { case ' ': case '\t': case '\r': case '\n': value.set(text.substring(offset, i)); return i; default: i++; } value.set(text.substring(offset, i)); return i; } protected int parseXpect(Class<?> testClass, XtextResource res, INode ctx, String text, String method, int offset, IParameterAcceptor acceptor, boolean ignore) { int newOffset; Multimap<String, Object> params = HashMultimap.create(); Wrapper<Expectation> expectation = new Wrapper<Expectation>(null); offset = skipWhitespace(text, offset); if ((newOffset = parseXpectParams(testClass, res, ctx, method, text, offset, params)) >= 0) offset = newOffset; offset = skipWhitespace(text, offset); if ((newOffset = parseXpectSLExpectation(ctx, text, offset, expectation)) >= 0) offset = newOffset; else if ((newOffset = parseXpectMLExpectation(ctx, text, offset, expectation)) >= 0) offset = newOffset; acceptor.acceptTest(null, method, params, expectation.get(), ignore); return offset; } protected int parseXpectImport(XtextResource res, String text, int offset, IParameterAcceptor acceptor) { offset = skipWhitespace(text, offset); int end = text.indexOf("\n", offset); String fileName = text.substring(offset, end).trim(); URI uri = URI.createURI(fileName); if (uri.isRelative() && !res.getURI().isRelative()) uri = uri.resolve(res.getURI()); acceptor.acceptImportURI(uri); return end; } protected int parseXpectMLExpectation(INode node, String text, int offset, Wrapper<Expectation> expectation) { if (offset + 3 < text.length() && text.substring(offset, offset + 3).equals("---")) { String indentation = getIndentation(node, node.getOffset() + offset); int start = text.indexOf('\n', offset + 3); int end = text.indexOf("---", offset + 3); if (start >= 0 && end >= 0) { String substring = text.substring(start + 1, end); end = substring.lastIndexOf('\n'); if (end >= 0) { String exp = substring.substring(0, end); int len = exp.length(); if (exp.startsWith(indentation)) exp = exp.substring(indentation.length()); exp = exp.replace("\n" + indentation, "\n"); expectation.set(new Expectation(node.getOffset() + start + 1, len, exp, indentation)); return end + start + 1; } } } return -1; } protected int parseXpectParams(Class<?> testClass, XtextResource res, INode node, String methodName, final String text, int offset, Multimap<String, Object> params) { int semanticOffset = getOffsetOfNextSemanticNode(node); params.putAll(getDefaultParams(res, semanticOffset)); String paramSyntax = getParameterSyntax(testClass, methodName); if (Strings.isEmpty(paramSyntax)) return -1; Nfa<ProdElement> nfa = getParameterNfa(paramSyntax); List<BacktrackItem> trace = new NfaUtil().backtrack(nfa, new BacktrackItem(offset), new BacktrackHandler<ProdElement, BacktrackItem>() { public BacktrackItem handle(ProdElement state, BacktrackItem previous) { if (Strings.isEmpty(state.getValue())) return previous; if (Strings.isEmpty(state.getName())) { if (text.regionMatches(previous.offset, state .getValue(), 0, state.getValue().length())) { int newOffset = previous.offset + state.getValue().length(); Matcher ws = WS.matcher(text).region(newOffset, text.length()); int childOffset = ws.find() ? ws.end() : newOffset; return new BacktrackItem(childOffset, state, state.getValue()); } } else { Token t = Token.valueOf(state.getValue()); Matcher matcher = t.pattern.matcher(text).region( previous.offset, text.length()); if (matcher.find()) { Matcher ws = WS.matcher(text).region( matcher.end(), text.length()); int childOffset = ws.find() ? ws.end() : matcher.end(); String value = matcher.groupCount() > 0 && matcher.group(1) != null ? matcher .group(1) : matcher.group(0); return new BacktrackItem(childOffset, state, value); } } return null; } public boolean isSolution(BacktrackItem result) { return true; } public Iterable<ProdElement> sortFollowers( BacktrackItem result, Iterable<ProdElement> followers) { return followers; } }); if (trace != null && !trace.isEmpty()) { for (BacktrackItem item : trace) if (item.token != null && item.token.getName() != null) { String key = item.token.getName(); params.removeAll(key); params.putAll( key, convertValue(res, node, semanticOffset, Token.valueOf(item.token.getValue()), item.value)); } return trace.get(trace.size() - 1).offset; } return -1; } protected int parseXpectSLExpectation(INode node, String text, int offset, Wrapper<Expectation> expectation) { if (offset + 3 < text.length() && text.substring(offset, offset + 3).equals("-->")) { int begin = offset + 3; if (text.charAt(begin) == '\r' || text.charAt(begin) == '\n') { expectation .set(new Expectation(node.getOffset() + begin, 0, "")); return begin; } else if (Character.isWhitespace(text.charAt(begin))) begin++; int end = text.indexOf('\n', begin); if (end < 0) end = text.length(); String exp = text.substring(begin, end); expectation.set(new Expectation(node.getOffset() + begin, exp .length(), exp)); return end; } return -1; } // protected int parseXpectTest(Wrapper<Class<?>> test, String text, int // offset, IParameterAcceptor acceptor) { // int index = text.indexOf("\n", offset); // if (index > offset) { // String name = text.substring(offset, index).trim(); // try { // Class<?> clazz = Class.forName(name); // acceptor.acceptTestClass(clazz); // } catch (ClassNotFoundException e) { // Exceptions.throwUncheckedException(e); // } // return index; // } // return -1; // } protected int skipWhitespace(String text, int offset) { int i = offset; while (i < text.length()) if (Character.isWhitespace(text.charAt(i))) i++; else return i; return i; } }