Skip to content

Commit

Permalink
Add JavascriptCompilerSettings to allow configuring picky parsing
Browse files Browse the repository at this point in the history
Signed-off-by: Andriy Redko <andriy.redko@aiven.io>
  • Loading branch information
reta committed Dec 14, 2022
1 parent 1207ce0 commit f6c9e36
Show file tree
Hide file tree
Showing 6 changed files with 263 additions and 55 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,12 @@
import java.util.Map;
import java.util.Properties;
import org.antlr.v4.runtime.ANTLRInputStream;
import org.antlr.v4.runtime.BaseErrorListener;
import org.antlr.v4.runtime.CommonTokenStream;
import org.antlr.v4.runtime.DiagnosticErrorListener;
import org.antlr.v4.runtime.RecognitionException;
import org.antlr.v4.runtime.Recognizer;
import org.antlr.v4.runtime.atn.PredictionMode;
import org.antlr.v4.runtime.tree.ParseTree;
import org.apache.lucene.expressions.Expression;
import org.apache.lucene.expressions.js.JavascriptParser.ExpressionContext;
Expand Down Expand Up @@ -120,6 +125,7 @@ private static org.objectweb.asm.commons.Method getAsmMethod(

final String sourceText;
final Map<String, Method> functions;
final JavascriptCompilerSettings settings;

/**
* Compiles the given expression.
Expand All @@ -129,8 +135,7 @@ private static org.objectweb.asm.commons.Method getAsmMethod(
* @throws ParseException on failure to compile
*/
public static Expression compile(String sourceText) throws ParseException {
return new JavascriptCompiler(sourceText)
.compileExpression(JavascriptCompiler.class.getClassLoader());
return compile(sourceText, JavascriptCompilerSettings.DEFAULT);
}

/**
Expand All @@ -148,14 +153,49 @@ public static Expression compile(String sourceText) throws ParseException {
*/
public static Expression compile(
String sourceText, Map<String, Method> functions, ClassLoader parent) throws ParseException {
return compile(sourceText, functions, parent, JavascriptCompilerSettings.DEFAULT);
}

/**
* Compiles the given expression with the supplied custom functions.
*
* <p>Functions must be {@code public static}, return {@code double} and can take from zero to 256
* {@code double} parameters.
*
* @param sourceText The expression to compile
* @param functions map of String names to functions
* @param parent a {@code ClassLoader} that should be used as the parent of the loaded class. It
* must contain all classes referred to by the given {@code functions}.
* @return A new compiled expression
* @throws ParseException on failure to compile
*/
static Expression compile(
String sourceText,
Map<String, Method> functions,
ClassLoader parent,
JavascriptCompilerSettings settings)
throws ParseException {
if (parent == null) {
throw new NullPointerException("A parent ClassLoader must be given.");
}
for (Method m : functions.values()) {
checkFunctionClassLoader(m, parent);
checkFunction(m);
}
return new JavascriptCompiler(sourceText, functions).compileExpression(parent);
return new JavascriptCompiler(sourceText, functions, settings).compileExpression(parent);
}

/**
* Compiles the given expression.
*
* @param sourceText The expression to compile
* @return A new compiled expression
* @throws ParseException on failure to compile
*/
static Expression compile(String sourceText, JavascriptCompilerSettings settings)
throws ParseException {
return new JavascriptCompiler(sourceText, settings)
.compileExpression(JavascriptCompiler.class.getClassLoader());
}

/**
Expand All @@ -174,21 +214,23 @@ private static void unusedTestCompile() throws IOException {
*
* @param sourceText The expression to compile
*/
private JavascriptCompiler(String sourceText) {
this(sourceText, DEFAULT_FUNCTIONS);
private JavascriptCompiler(String sourceText, JavascriptCompilerSettings settings) {
this(sourceText, DEFAULT_FUNCTIONS, settings);
}

/**
* Constructs a compiler for expressions with specific set of functions
*
* @param sourceText The expression to compile
*/
private JavascriptCompiler(String sourceText, Map<String, Method> functions) {
private JavascriptCompiler(
String sourceText, Map<String, Method> functions, JavascriptCompilerSettings settings) {
if (sourceText == null) {
throw new NullPointerException();
}
this.sourceText = sourceText;
this.functions = functions;
this.settings = settings;
}

/**
Expand Down Expand Up @@ -238,10 +280,47 @@ private ParseTree getAntlrParseTree() throws ParseException {
final JavascriptParser javascriptParser =
new JavascriptParser(new CommonTokenStream(javascriptLexer));
javascriptParser.removeErrorListeners();
if (settings.isPicky()) {
setupPicky(javascriptParser);
}
javascriptParser.setErrorHandler(new JavascriptParserErrorStrategy());
return javascriptParser.compile();
}

private void setupPicky(JavascriptParser parser) {
// Diagnostic listener invokes syntaxError on other listeners for ambiguity issues
parser.addErrorListener(new DiagnosticErrorListener(true));
// a second listener to fail the test when the above happens.
parser.addErrorListener(
new BaseErrorListener() {
@Override
public void syntaxError(
final Recognizer<?, ?> recognizer,
final Object offendingSymbol,
final int line,
final int charPositionInLine,
final String msg,
final RecognitionException e) {
throw new RuntimeException(
new ParseException(
"line ("
+ line
+ "), offset ("
+ charPositionInLine
+ "), symbol ("
+ offendingSymbol
+ ") "
+ msg,
charPositionInLine));
}
});

// Enable exact ambiguity detection (costly). we enable exact since its the default for
// DiagnosticErrorListener, life is too short to think about what 'inexact ambiguity' might
// mean.
parser.getInterpreter().setPredictionMode(PredictionMode.LL_EXACT_AMBIG_DETECTION);
}

/** Sends the bytecode of class file to {@link ClassWriter}. */
private void generateClass(
final ParseTree parseTree,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You under the Apache License, Version 2.0
* (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.apache.lucene.expressions.js;

/** Settings for expression compiler for javascript expressions. */
final class JavascriptCompilerSettings {
static final JavascriptCompilerSettings DEFAULT = new JavascriptCompilerSettings();

/**
* Whether to throw exception on ambiguity or other internal parsing issues. This option makes
* things slower too, it is only for debugging.
*/
private boolean picky = false;

/**
* Returns true if the compiler should be picky. This means it runs slower and enables additional
* runtime checks, throwing an exception if there are ambiguities in the grammar or other low
* level parsing problems.
*/
public boolean isPicky() {
return picky;
}

/**
* Set to true if compilation should be picky.
*
* @see #isPicky
*/
public void setPicky(boolean picky) {
this.picky = picky;
}

@Override
public String toString() {
return "[picky=" + picky + "]";
}
}
Loading

0 comments on commit f6c9e36

Please sign in to comment.