This is an automated email from the ASF dual-hosted git repository.

hansva pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/hop.git


The following commit(s) were added to refs/heads/main by this push:
     new f0830868e2 harden javascript get variables button, fixes #3403 (#7886)
f0830868e2 is described below

commit f0830868e2c061e2a6b436315263da46567228fa
Author: Hans Van Akelyen <[email protected]>
AuthorDate: Wed Aug 12 16:34:20 2026 +0200

    harden javascript get variables button, fixes #3403 (#7886)
---
 .../transforms/javascript/ScriptValuesDialog.java  | 120 ++++++------
 .../javascript/ScriptValuesVariableDiscovery.java  | 126 ++++++++++++
 .../javascript/messages/messages_en_US.properties  |   2 +
 .../ScriptValuesVariableDiscoveryTest.java         | 215 +++++++++++++++++++++
 4 files changed, 397 insertions(+), 66 deletions(-)

diff --git 
a/plugins/transforms/javascript/src/main/java/org/apache/hop/pipeline/transforms/javascript/ScriptValuesDialog.java
 
b/plugins/transforms/javascript/src/main/java/org/apache/hop/pipeline/transforms/javascript/ScriptValuesDialog.java
index 431fec0128..dc1c86b477 100644
--- 
a/plugins/transforms/javascript/src/main/java/org/apache/hop/pipeline/transforms/javascript/ScriptValuesDialog.java
+++ 
b/plugins/transforms/javascript/src/main/java/org/apache/hop/pipeline/transforms/javascript/ScriptValuesDialog.java
@@ -18,6 +18,7 @@
 package org.apache.hop.pipeline.transforms.javascript;
 
 import java.math.BigDecimal;
+import java.util.ArrayList;
 import java.util.Collections;
 import java.util.Date;
 import java.util.Hashtable;
@@ -90,19 +91,15 @@ import org.eclipse.swt.widgets.TableItem;
 import org.eclipse.swt.widgets.Text;
 import org.eclipse.swt.widgets.Tree;
 import org.eclipse.swt.widgets.TreeItem;
-import org.mozilla.javascript.CompilerEnvirons;
 import org.mozilla.javascript.Context;
 import org.mozilla.javascript.ContextFactory;
-import org.mozilla.javascript.ErrorReporter;
 import org.mozilla.javascript.EvaluatorException;
 import org.mozilla.javascript.JavaScriptException;
-import org.mozilla.javascript.NodeTransformer;
-import org.mozilla.javascript.Parser;
+import org.mozilla.javascript.RhinoException;
 import org.mozilla.javascript.Script;
 import org.mozilla.javascript.Scriptable;
 import org.mozilla.javascript.ScriptableObject;
 import org.mozilla.javascript.ast.ScriptNode;
-import org.mozilla.javascript.tools.ToolErrorReporter;
 
 public class ScriptValuesDialog extends BaseTransformDialog {
   private static final Class<?> PKG = ScriptValuesMeta.class;
@@ -1200,6 +1197,7 @@ public class ScriptValuesDialog extends 
BaseTransformDialog {
   @SuppressWarnings("deprecation")
   private boolean test(boolean getvars, boolean popup) {
     boolean retval = true;
+    boolean scriptRan = true;
     TextComposite wScript = getStyledTextComp();
     String scr = wScript.getText();
     HopException testException = null;
@@ -1354,59 +1352,47 @@ public class ScriptValuesDialog extends 
BaseTransformDialog {
         try {
 
           Script evalScript = jscx.compileString(scr, "script", 1, null);
-          evalScript.exec(jscx, jsscope);
+
+          try {
+            evalScript.exec(jscx, jsscope);
+          } catch (RhinoException e) {
+            // "Get variables" only needs the script to compile: the names are 
read from a static
+            // parse of the source. Running it here is a best-effort attempt 
to learn their types,
+            // and it runs against the placeholder input values generated 
above, so every script
+            // that really inspects its input -- JSON.parse(field), date 
parsing, ... -- fails at
+            // this point through no fault of the user. Reporting that as a 
failed test used to
+            // leave the user with no variables at all, so keep going and let 
the types default.
+            // See issue #3403.
+            if (!getvars) {
+              throw e;
+            }
+            scriptRan = false;
+          }
 
           if (getvars) {
-            ScriptNode tree = parseVariables(jscx, jsscope, scr, "script", 1, 
null);
-            for (int i = 0; i < tree.getParamAndVarCount(); i++) {
-              String varname = tree.getParamOrVarName(i);
-              if (!varname.equalsIgnoreCase("row")
-                  && !varname.equalsIgnoreCase("pipeline_Status")) {
-                int type = IValueMeta.TYPE_STRING;
-                int length = -1;
-                int precision = -1;
-                Object result = jsscope.get(varname, jsscope);
-                if (result != null) {
-                  String classname = result.getClass().getName();
-                  if (classname.equalsIgnoreCase("java.lang.Byte")) {
-                    // MAX = 127
-                    type = IValueMeta.TYPE_INTEGER;
-                    length = 3;
-                    precision = 0;
-                  } else if (classname.equalsIgnoreCase("java.lang.Integer")) {
-                    // MAX = 2147483647
-                    type = IValueMeta.TYPE_INTEGER;
-                    length = 9;
-                    precision = 0;
-                  } else if (classname.equalsIgnoreCase("java.lang.Long")) {
-                    // MAX = 9223372036854775807
-                    type = IValueMeta.TYPE_INTEGER;
-                    length = 18;
-                    precision = 0;
-                  } else if (classname.equalsIgnoreCase("java.lang.Double")) {
-                    type = IValueMeta.TYPE_NUMBER;
-                    length = 16;
-                    precision = 2;
-
-                  } else if 
(classname.equalsIgnoreCase("org.mozilla.javascript.NativeDate")
-                      || classname.equalsIgnoreCase("java.util.Date")) {
-                    type = IValueMeta.TYPE_DATE;
-                  } else if (classname.equalsIgnoreCase("java.lang.Boolean")) {
-                    type = IValueMeta.TYPE_BOOLEAN;
-                  }
-                }
-                TableItem ti = new TableItem(wFields.table, SWT.NONE);
-                ti.setText(1, varname);
-                ti.setText(2, "");
-                ti.setText(3, ValueMetaFactory.getValueMetaName(type));
-                ti.setText(4, length >= 0 ? ("" + length) : "");
-                ti.setText(5, precision >= 0 ? ("" + precision) : "");
-
-                // If the variable name exists in the input, suggest to 
replace the value
-                //
-                ti.setText(
-                    6, (rowMeta.indexOfValue(varname) >= 0) ? YES_NO_COMBO[1] 
: YES_NO_COMBO[0]);
-              }
+            // Leave the fields already in the grid alone: the button must not 
duplicate them, nor
+            // throw away types the user corrected by hand.
+            //
+            List<String> present = new ArrayList<>();
+            int nrPresent = wFields.nrNonEmpty();
+            for (int i = 0; i < nrPresent; i++) {
+              present.add(wFields.getNonEmpty(i).getText(1));
+            }
+
+            for (ScriptValuesVariableDiscovery.DiscoveredVariable variable :
+                ScriptValuesVariableDiscovery.discover(jscx, jsscope, scr, 
present)) {
+              TableItem ti = new TableItem(wFields.table, SWT.NONE);
+              ti.setText(1, variable.name());
+              ti.setText(2, "");
+              ti.setText(3, 
ValueMetaFactory.getValueMetaName(variable.type()));
+              ti.setText(4, variable.length() >= 0 ? ("" + variable.length()) 
: "");
+              ti.setText(5, variable.precision() >= 0 ? ("" + 
variable.precision()) : "");
+
+              // If the variable name exists in the input, suggest to replace 
the value
+              //
+              ti.setText(
+                  6,
+                  (rowMeta.indexOfValue(variable.name()) >= 0) ? 
YES_NO_COMBO[1] : YES_NO_COMBO[0]);
             }
             wFields.removeEmptyRows();
             wFields.setRowNums();
@@ -1445,6 +1431,17 @@ public class ScriptValuesDialog extends 
BaseTransformDialog {
                     + Const.CR);
             mb.setText("OK");
             mb.open();
+          } else if (!scriptRan) {
+            // The variables were found, but nothing ran to tell us what they 
hold.
+            //
+            MessageBox mb = new MessageBox(shell, SWT.OK | 
SWT.ICON_INFORMATION);
+            mb.setMessage(
+                BaseMessages.getString(
+                    PKG, 
"ScriptValuesDialogMod.GetVariables.TypesUnknown.Message"));
+            mb.setText(
+                BaseMessages.getString(
+                    PKG, 
"ScriptValuesDialogMod.GetVariables.TypesUnknown.Title"));
+            mb.open();
           }
         } else {
           new ErrorDialog(
@@ -1925,15 +1922,6 @@ public class ScriptValuesDialog extends 
BaseTransformDialog {
       String sourceName,
       int lineno,
       Object securityDomain) {
-    CompilerEnvirons evn = new CompilerEnvirons();
-    evn.initFromContext(cx);
-    evn.setOptimizationLevel(-1);
-    evn.setGeneratingSource(true);
-    evn.setGenerateDebugInfo(true);
-    ErrorReporter errorReporter = new ToolErrorReporter(false);
-    Parser p = new Parser(evn, errorReporter);
-    ScriptNode tree = p.parse(source, "", 0); // IOException
-    new NodeTransformer().transform(tree, evn);
-    return tree;
+    return ScriptValuesVariableDiscovery.parse(cx, source);
   }
 }
diff --git 
a/plugins/transforms/javascript/src/main/java/org/apache/hop/pipeline/transforms/javascript/ScriptValuesVariableDiscovery.java
 
b/plugins/transforms/javascript/src/main/java/org/apache/hop/pipeline/transforms/javascript/ScriptValuesVariableDiscovery.java
new file mode 100644
index 0000000000..fcc92310c3
--- /dev/null
+++ 
b/plugins/transforms/javascript/src/main/java/org/apache/hop/pipeline/transforms/javascript/ScriptValuesVariableDiscovery.java
@@ -0,0 +1,126 @@
+/*
+ * 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.hop.pipeline.transforms.javascript;
+
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.LinkedHashSet;
+import java.util.List;
+import java.util.Set;
+import org.apache.hop.core.row.IValueMeta;
+import org.mozilla.javascript.CompilerEnvirons;
+import org.mozilla.javascript.Context;
+import org.mozilla.javascript.ErrorReporter;
+import org.mozilla.javascript.NodeTransformer;
+import org.mozilla.javascript.Parser;
+import org.mozilla.javascript.Scriptable;
+import org.mozilla.javascript.ast.ScriptNode;
+import org.mozilla.javascript.tools.ToolErrorReporter;
+
+/**
+ * Finds the variables a script declares so the "Get variables" button can 
propose them as output
+ * fields.
+ *
+ * <p>The names come from a static parse of the source, so they are available 
for every script that
+ * compiles, whether or not it can actually run. The types are a best-effort 
guess read from a scope
+ * in which the script was executed against the placeholder input values the 
dialog generates. That
+ * execution is allowed to fail: a script which really inspects its input, say 
{@code
+ * JSON.parse(field)}, throws on placeholder data through no fault of the 
user, and the variables
+ * must still be offered. Anything whose value is unknown keeps the default 
String type.
+ */
+final class ScriptValuesVariableDiscovery {
+
+  /** Variables the transform manages itself, never proposed as output fields. 
*/
+  private static final Set<String> RESERVED = Set.of("row", "pipeline_status");
+
+  private ScriptValuesVariableDiscovery() {
+    // utility class
+  }
+
+  /**
+   * A variable declared by the script, with the output field type guessed for 
it. A length or
+   * precision of -1 means "not applicable", matching the empty cell the 
dialog shows.
+   */
+  record DiscoveredVariable(String name, int type, int length, int precision) 
{}
+
+  static List<DiscoveredVariable> discover(Context context, Scriptable scope, 
String script) {
+    return discover(context, scope, script, List.of());
+  }
+
+  /**
+   * @param context Rhino context used to parse the script
+   * @param scope scope the script ran in, used to guess types; may be null or 
only partly filled in
+   *     when the script failed halfway
+   * @param script the script source
+   * @param exclude variable names to leave out, typically the fields already 
in the grid so that
+   *     pressing the button twice does not duplicate them
+   */
+  static List<DiscoveredVariable> discover(
+      Context context, Scriptable scope, String script, Collection<String> 
exclude) {
+    ScriptNode tree = parse(context, script);
+
+    Set<String> seen = new LinkedHashSet<>(exclude);
+    List<DiscoveredVariable> variables = new ArrayList<>();
+    for (int i = 0; i < tree.getParamAndVarCount(); i++) {
+      String name = tree.getParamOrVarName(i);
+      if (RESERVED.contains(name.toLowerCase()) || !seen.add(name)) {
+        continue;
+      }
+      variables.add(describe(name, scope == null ? null : scope.get(name, 
scope)));
+    }
+    return variables;
+  }
+
+  /** Parse the source into an AST without running it. */
+  static ScriptNode parse(Context context, String script) {
+    CompilerEnvirons environment = new CompilerEnvirons();
+    environment.initFromContext(context);
+    environment.setOptimizationLevel(-1);
+    environment.setGeneratingSource(true);
+    environment.setGenerateDebugInfo(true);
+    ErrorReporter errorReporter = new ToolErrorReporter(false);
+    Parser parser = new Parser(environment, errorReporter);
+    ScriptNode tree = parser.parse(script, "", 0);
+    new NodeTransformer().transform(tree, environment);
+    return tree;
+  }
+
+  /**
+   * Map the Java wrapper Rhino used for a value onto a Hop type. Anything 
unrecognised stays a
+   * String: that covers NOT_FOUND and Undefined, which is what a variable 
looks like when the
+   * script never ran or never reached its assignment.
+   */
+  private static DiscoveredVariable describe(String name, Object value) {
+    if (value == null) {
+      return new DiscoveredVariable(name, IValueMeta.TYPE_STRING, -1, -1);
+    }
+    return switch (value.getClass().getName()) {
+        // MAX = 127
+      case "java.lang.Byte" -> new DiscoveredVariable(name, 
IValueMeta.TYPE_INTEGER, 3, 0);
+        // MAX = 2147483647
+      case "java.lang.Integer" -> new DiscoveredVariable(name, 
IValueMeta.TYPE_INTEGER, 9, 0);
+        // MAX = 9223372036854775807
+      case "java.lang.Long" -> new DiscoveredVariable(name, 
IValueMeta.TYPE_INTEGER, 18, 0);
+      case "java.lang.Double" -> new DiscoveredVariable(name, 
IValueMeta.TYPE_NUMBER, 16, 2);
+      case "org.mozilla.javascript.NativeDate", "java.util.Date" ->
+          new DiscoveredVariable(name, IValueMeta.TYPE_DATE, -1, -1);
+      case "java.lang.Boolean" -> new DiscoveredVariable(name, 
IValueMeta.TYPE_BOOLEAN, -1, -1);
+      default -> new DiscoveredVariable(name, IValueMeta.TYPE_STRING, -1, -1);
+    };
+  }
+}
diff --git 
a/plugins/transforms/javascript/src/main/resources/org/apache/hop/pipeline/transforms/javascript/messages/messages_en_US.properties
 
b/plugins/transforms/javascript/src/main/resources/org/apache/hop/pipeline/transforms/javascript/messages/messages_en_US.properties
index 046208ce9d..5a5cb5e48e 100644
--- 
a/plugins/transforms/javascript/src/main/resources/org/apache/hop/pipeline/transforms/javascript/messages/messages_en_US.properties
+++ 
b/plugins/transforms/javascript/src/main/resources/org/apache/hop/pipeline/transforms/javascript/messages/messages_en_US.properties
@@ -40,6 +40,8 @@ ScriptValuesDialogMod.Fields.Label=Fields
 ScriptValuesDialogMod.FileFunctions.Label=File Functions
 ScriptValuesDialogMod.GettingFields.Label=Getting fields...please wait
 ScriptValuesDialogMod.GetVariables.Button=\ &Get variables 
+ScriptValuesDialogMod.GetVariables.TypesUnknown.Title=Variables added
+ScriptValuesDialogMod.GetVariables.TypesUnknown.Message=The variables declared 
by the script were added to the fields below.\n\nTheir types could not be 
detected because the script did not run to completion on the sample values Hop 
generates for the input fields. This is normal for a script that inspects its 
input, for example with JSON.parse(). Please check the Type column.
 ScriptValuesDialogMod.InputFields.Label=Input fields
 ScriptValuesDialogMod.Javascript.Label=Java script \:
 ScriptValuesDialogMod.JavascriptFunctions.Label=Java script functions \:
diff --git 
a/plugins/transforms/javascript/src/test/java/org/apache/hop/pipeline/transforms/javascript/ScriptValuesVariableDiscoveryTest.java
 
b/plugins/transforms/javascript/src/test/java/org/apache/hop/pipeline/transforms/javascript/ScriptValuesVariableDiscoveryTest.java
new file mode 100644
index 0000000000..b034de05c0
--- /dev/null
+++ 
b/plugins/transforms/javascript/src/test/java/org/apache/hop/pipeline/transforms/javascript/ScriptValuesVariableDiscoveryTest.java
@@ -0,0 +1,215 @@
+/*
+ * 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.hop.pipeline.transforms.javascript;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+import java.util.List;
+import java.util.Map;
+import java.util.function.Function;
+import java.util.stream.Collectors;
+import org.apache.hop.core.row.IValueMeta;
+import 
org.apache.hop.pipeline.transforms.javascript.ScriptValuesVariableDiscovery.DiscoveredVariable;
+import org.junit.jupiter.api.AfterEach;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.DisplayName;
+import org.junit.jupiter.api.Test;
+import org.mozilla.javascript.Context;
+import org.mozilla.javascript.EvaluatorException;
+import org.mozilla.javascript.RhinoException;
+import org.mozilla.javascript.Scriptable;
+
+/**
+ * Covers the variable discovery behind the "Get variables" button of the 
JavaScript transform
+ * dialog.
+ */
+class ScriptValuesVariableDiscoveryTest {
+
+  /**
+   * The placeholder ScriptValuesDialog binds to every incoming String field 
so it can run the
+   * script without real data.
+   */
+  private static final String SAMPLE_STRING_VALUE =
+      "test value test value test value test value test value "
+          + "test value test value test value test value test value";
+
+  private Context cx;
+  private Scriptable scope;
+
+  @BeforeEach
+  void enterRhino() {
+    cx = Context.enter();
+    cx.setOptimizationLevel(-1);
+    scope = cx.initStandardObjects();
+  }
+
+  @AfterEach
+  void exitRhino() {
+    Context.exit();
+  }
+
+  private List<DiscoveredVariable> discover(String script) {
+    return ScriptValuesVariableDiscovery.discover(cx, scope, script);
+  }
+
+  private static List<String> namesOf(List<DiscoveredVariable> variables) {
+    return variables.stream().map(DiscoveredVariable::name).toList();
+  }
+
+  private static Map<String, DiscoveredVariable> 
byName(List<DiscoveredVariable> variables) {
+    return variables.stream()
+        .collect(Collectors.toMap(DiscoveredVariable::name, 
Function.identity()));
+  }
+
+  @Test
+  @DisplayName("issue #3403: variables are found even though the script cannot 
run on sample data")
+  void variablesAreFoundWhenScriptFailsOnSampleData() {
+    // The script from the issue report. It is perfectly valid, but it parses 
its input field, so
+    // it always blows up on the placeholder value the dialog feeds it.
+    //
+    String script =
+        """
+        //Script to flatten JSON keys for Hop
+
+        var input_json = JSON.parse(tabsFlat);
+        var output_json = [];
+
+        for (var key in input_json) {
+            var value = input_json[key];
+            output_json.push({
+                field1: key,
+                field2: value
+            });
+        }
+
+        var flattened_json = JSON.stringify(output_json);
+        """;
+
+    scope.put("tabsFlat", scope, Context.toObject(SAMPLE_STRING_VALUE, scope));
+
+    // The script compiles: nothing is wrong with it.
+    //
+    var compiled = cx.compileString(script, "script", 1, null);
+
+    // Running it on the placeholder value is what fails, and it is not the 
user's fault.
+    //
+    RhinoException failure = assertThrows(RhinoException.class, () -> 
compiled.exec(cx, scope));
+    assertTrue(
+        failure.details().contains("Unexpected token"),
+        "expected the JSON.parse failure from the issue, got: " + 
failure.details());
+
+    // The variables must be offered regardless. Before the fix this returned 
nothing at all and
+    // the user had to type flattened_json by hand.
+    //
+    assertEquals(
+        List.of("input_json", "output_json", "key", "value", "flattened_json"),
+        namesOf(discover(script)));
+  }
+
+  @Test
+  @DisplayName("a script that never ran yields String typed variables rather 
than none")
+  void unknownValuesFallBackToString() {
+    List<DiscoveredVariable> variables = discover("var a; var b = 1; var c = 
'x';");
+
+    // Nothing was executed, so no value is known for any of them.
+    //
+    for (DiscoveredVariable variable : variables) {
+      assertEquals(IValueMeta.TYPE_STRING, variable.type(), variable.name());
+      assertEquals(-1, variable.length(), variable.name());
+      assertEquals(-1, variable.precision(), variable.name());
+    }
+    assertEquals(List.of("a", "b", "c"), namesOf(variables));
+  }
+
+  @Test
+  @DisplayName("types are guessed from the scope when the script did run")
+  void typesAreReadFromTheExecutedScope() {
+    String script =
+        """
+        var aNumber = 42.5;
+        var aBoolean = true;
+        var aDate = new Date();
+        var aString = 'text';
+        var neverAssigned;
+        """;
+    cx.evaluateString(scope, script, "script", 1, null);
+
+    Map<String, DiscoveredVariable> variables = byName(discover(script));
+
+    assertEquals(IValueMeta.TYPE_NUMBER, variables.get("aNumber").type());
+    assertEquals(16, variables.get("aNumber").length());
+    assertEquals(2, variables.get("aNumber").precision());
+
+    assertEquals(IValueMeta.TYPE_BOOLEAN, variables.get("aBoolean").type());
+    assertEquals(IValueMeta.TYPE_DATE, variables.get("aDate").type());
+    assertEquals(IValueMeta.TYPE_STRING, variables.get("aString").type());
+    assertEquals(IValueMeta.TYPE_STRING, 
variables.get("neverAssigned").type());
+  }
+
+  @Test
+  @DisplayName("integral java values coming back from java calls map onto 
Integer fields")
+  void javaIntegralTypesMapOntoIntegerFields() {
+    String script = "var aByte; var anInteger; var aLong;";
+    scope.put("aByte", scope, Byte.valueOf((byte) 1));
+    scope.put("anInteger", scope, Integer.valueOf(2));
+    scope.put("aLong", scope, Long.valueOf(3L));
+
+    Map<String, DiscoveredVariable> variables = byName(discover(script));
+
+    assertEquals(IValueMeta.TYPE_INTEGER, variables.get("aByte").type());
+    assertEquals(3, variables.get("aByte").length());
+    assertEquals(0, variables.get("aByte").precision());
+
+    assertEquals(IValueMeta.TYPE_INTEGER, variables.get("anInteger").type());
+    assertEquals(9, variables.get("anInteger").length());
+
+    assertEquals(IValueMeta.TYPE_INTEGER, variables.get("aLong").type());
+    assertEquals(18, variables.get("aLong").length());
+  }
+
+  @Test
+  @DisplayName("the transform's own variables are never proposed as output 
fields")
+  void reservedVariablesAreSkipped() {
+    assertEquals(List.of("mine"), namesOf(discover("var row; var 
pipeline_Status; var mine = 1;")));
+  }
+
+  @Test
+  @DisplayName("fields already in the grid are not proposed again")
+  void alreadyPresentVariablesAreSkipped() {
+    String script = "var first = 1; var second = 2; var third = 3;";
+
+    List<DiscoveredVariable> variables =
+        ScriptValuesVariableDiscovery.discover(cx, scope, script, 
List.of("first", "third"));
+
+    assertEquals(List.of("second"), namesOf(variables));
+  }
+
+  @Test
+  @DisplayName("a variable declared more than once is proposed once")
+  void repeatedDeclarationsAreCollapsed() {
+    assertEquals(List.of("total"), namesOf(discover("var total = 1; var total 
= 2;")));
+  }
+
+  @Test
+  @DisplayName("a genuine syntax error is still reported")
+  void syntaxErrorsStillFail() {
+    assertThrows(EvaluatorException.class, () -> discover("var broken = {;"));
+  }
+}

Reply via email to