/* * Copyright OpenSearch Contributors * SPDX-License-Identifier: Apache-2.0 */ package org.opensearch.dataprepper.expression.util; import org.antlr.v4.runtime.tree.ParseTree; import org.hamcrest.Description; import org.hamcrest.DiagnosingMatcher; import org.hamcrest.Matcher; import org.opensearch.dataprepper.expression.antlr.DataPrepperExpressionParser; import javax.annotation.Nullable; import static org.hamcrest.CoreMatchers.instanceOf; import static org.hamcrest.CoreMatchers.is; import static org.hamcrest.core.IsEqual.equalTo; import static org.opensearch.dataprepper.expression.util.TerminalNodeMatcher.isTerminalNode; /** * @since 1.3 * *

* ContextMatcher is a custom Hamcrest matcher to assert if a {@link ParseTree} is an instance of the expected * context and assert child node types and count. Should be used with {@link TerminalNodeMatcher}. *

*

* Example
* Given tree: *

 *     Expression
* ├─ ConditionalExpression
* │ ├─ EqualityOperatorExpression
* ├─ <EOF>
*
* * Matcher Assertion *
 *         assertThat(parseTree, hasContext(Expression,
* hasContext(ConditionalExpression, hasContext(EqualityOperatorExpression)),
* hasContext(isTerminalNode())
* )) *
*

*/ public class ContextMatcher extends DiagnosingMatcher { /** * Converts long antlr class names to easily read format. Class names without '$' will not be formatted. * @param object source of class name to format * @return formatted string */ protected static String shortClassString(final Object object) { final String classString = object.getClass().toString(); final int endOfPrefix = classString.indexOf('$'); if (endOfPrefix >= 0) { return classString.substring(endOfPrefix + 1); } else { return classString; } } /** * Creates a string of a parse tree and all parent nodes to give context on where in a tree a node is located. * @param parseTree node to generate location from * @param mismatch Hamcrest Description where context will be appended. */ protected static void describeContextTo(final ParseTree parseTree, final Description mismatch) { if (parseTree != null) { final StringBuilder context = new StringBuilder(parseTree.getText() + " | " + shortClassString(parseTree)); ParseTree parent = parseTree.getParent(); while (parent != null) { context.insert(0, parent.getText() + " | " + shortClassString(parent) + "\n\t\t-> "); parent = parent.getParent(); } mismatch.appendText("\n\t\t" + context + "\n\t\t"); } } /** * Creates matcher to check if tree is operatorType with a single child of type TerminalNode * @param operatorType class type of operator to assert * @return DiagnosingMatcher */ public static DiagnosingMatcher isOperator(final Class operatorType) { return hasContext(operatorType, isTerminalNode()); } /** * @since 1.3 * *

Creates a matcher to check for standard ParseTree root.

*
     * Expression
* ├─ {lhs}
* ├─ <EOF>
*
* * @param lhs matcher to use for the first child of the Expression node * @return DiagnosingMatcher * * @see ContextMatcher#hasContext(Class, DiagnosingMatcher[]) */ public static DiagnosingMatcher isExpression(final DiagnosingMatcher lhs) { return hasContext(DataPrepperExpressionParser.ExpressionContext.class, lhs, isTerminalNode()); } /** * @since 1.3 *

Shortcut for constructor matching Hamcrest standard.

*

* Syntax
*

assertThat(parseTree, hasContext(Expression, [child assertions]))
*

* @param parserRuleContextType used to assert ParseTree branch is instance of parserRuleContextType * @param childrenMatchers assertions to be used on child nodes. Matcher will also assert order and count * @return matcher instance */ @SafeVarargs public static DiagnosingMatcher hasContext( final Class parserRuleContextType, final DiagnosingMatcher... childrenMatchers ) { return new ContextMatcher(parserRuleContextType, childrenMatchers); } private final DiagnosingMatcher[] childrenMatchers; final Matcher isParserRuleContextType; private final Matcher listSizeMatcher; @Nullable private Matcher failedAssertion; @SafeVarargs public ContextMatcher( final Class parserRuleContextType, final DiagnosingMatcher ... childrenMatchers ) { this.childrenMatchers = childrenMatchers; isParserRuleContextType = is(instanceOf(parserRuleContextType)); listSizeMatcher = equalTo(childrenMatchers.length); } /** * @since 1.3 * Asserts number of children equal to the number of childMatchers and, in order each child matches the * corresponding matcher. * @param parseTree ParseTree branch to get children from * @param mismatch Description used for printing Hamcrest mismatch messages * @return true if all assertions pass */ private boolean matchChildren(final ParseTree parseTree, final Description mismatch) { if (listSizeMatcher.matches(parseTree.getChildCount())) { for (int i = 0; i < childrenMatchers.length; i++) { final ParseTree child = parseTree.getChild(i); final DiagnosingMatcher matcher = childrenMatchers[i]; if (!matcher.matches(child)) { mismatch.appendText("Expected context \"" + child.getText() + "\""); mismatch.appendText(" | " + shortClassString(child)); mismatch.appendText(" to match "); mismatch.appendDescriptionOf(matcher); mismatch.appendText("\n\t\t"); matcher.describeMismatch(child, mismatch); failedAssertion = matcher; return false; } } return true; } else { mismatch.appendDescriptionOf(listSizeMatcher) .appendText(" "); listSizeMatcher.describeMismatch(parseTree.getChildCount(), mismatch); failedAssertion = listSizeMatcher; return false; } } /** * @since 1.3 * Asserts ParseTree branch matches assertion and all children match assertions, if any. * @param item ParseTree branch to assert against * @param mismatch Description used for printing Hamcrest mismatch messages * @return true if all assertions pass */ public boolean matches(final Object item, final Description mismatch) { if (isParserRuleContextType.matches(item)) { final ParseTree parseTree = (ParseTree) item; return matchChildren(parseTree, mismatch); } else { mismatch.appendDescriptionOf(isParserRuleContextType) .appendText("\n\t\tfinally "); isParserRuleContextType.describeMismatch(item, mismatch); failedAssertion = isParserRuleContextType; return false; } } /** * @since 1.3 * Called by Hamcrest when match fails to print useful mismatch error message * @param description Where output is collected */ @Override public void describeTo(final Description description) { if (failedAssertion != null) failedAssertion.describeTo(description); } }